From 59c450a3a4e2e2de76d1017733bd2bd5aeb87089 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sun, 3 Mar 2024 19:39:17 +0000 Subject: [PATCH 1/5] Improve the text box entry keyboard controls also fix a couple of other minor bugs noticed in testing --- pygame_gui/core/text/text_box_layout.py | 25 +- pygame_gui/core/text/text_box_layout_row.py | 17 + pygame_gui/core/ui_element.py | 10 +- pygame_gui/data/default_theme.json | 7 + pygame_gui/elements/ui_text_box.py | 2 +- pygame_gui/elements/ui_text_entry_box.py | 339 +++++++++++++------- pygame_gui/windows/ui_file_dialog.py | 2 + 7 files changed, 281 insertions(+), 121 deletions(-) diff --git a/pygame_gui/core/text/text_box_layout.py b/pygame_gui/core/text/text_box_layout.py index a07ad02a..5ed5046b 100644 --- a/pygame_gui/core/text/text_box_layout.py +++ b/pygame_gui/core/text/text_box_layout.py @@ -96,7 +96,7 @@ def __init__(self, self._add_row_to_layout(current_row, True) self.edit_buffer = 2 - self.cursor_text_row = None + self.cursor_text_row: Optional[TextBoxLayoutRow] = None self.last_horiz_cursor_row_pos = 0 self.selection_colour = pygame.Color(128, 128, 200, 255) @@ -585,6 +585,29 @@ def vert_align_bottom_all_rows(self, y_padding): row.vert_align_items_to_row() new_y -= row.height + def set_cursor_to_end_of_current_row(self): + """ + Set the edit cursor position in the text layout to the end of the current row and returns the overall + position in the text + + :return: the overall position of the cursor in the text layout, after setting it to the end of the current row + """ + if self.cursor_text_row is not None: + is_last_row = self.cursor_text_row == self.layout_rows[-1] + self.cursor_text_row.set_cursor_to_end(is_last_row) + return self.get_cursor_index() + + def set_cursor_to_start_of_current_row(self): + """ + Set the edit cursor position in the text layout to the end of the current row and returns the overall + position in the text + + :return: the overall position of the cursor in the text layout, after setting it to the end of the current row + """ + if self.cursor_text_row is not None: + self.cursor_text_row.set_cursor_to_start() + return self.get_cursor_index() + def set_cursor_position(self, cursor_pos): """ Set the edit cursor position in the text layout. diff --git a/pygame_gui/core/text/text_box_layout_row.py b/pygame_gui/core/text/text_box_layout_row.py index 10590d46..bfb452cd 100644 --- a/pygame_gui/core/text/text_box_layout_row.py +++ b/pygame_gui/core/text/text_box_layout_row.py @@ -470,6 +470,23 @@ def set_cursor_position(self, cursor_pos): self._setup_offset_position_from_edit_cursor() + def set_cursor_to_end(self, is_last_row): + end_pos = self.letter_count + # we need to ignore the trailing space on line-wrapped rows, + # or we will spill over to the row below + if not is_last_row and len(self.items) > 0: + last_chunk = self.items[-1] + if (isinstance(last_chunk, TextLineChunkFTFont) and + (len(last_chunk.text) > 0 and last_chunk.text[-1] == " ")): + end_pos -= 1 + if isinstance(last_chunk, LineBreakLayoutRect): + end_pos -= 1 + end_pos = max(0, end_pos) + self.set_cursor_position(end_pos) + + def set_cursor_to_start(self): + self.set_cursor_position(0) + def get_cursor_index(self) -> int: """ Get the current character index of the cursor diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py index 5eef478a..8b804a1d 100644 --- a/pygame_gui/core/ui_element.py +++ b/pygame_gui/core/ui_element.py @@ -560,7 +560,7 @@ def _update_absolute_rect_position_from_anchors(self, recalculate_margins=False) new_width = new_right - new_left new_width, new_height = self._get_clamped_to_minimum_dimensions((new_width, new_height)) if (new_height != self.relative_rect.height) or (new_width != self.relative_rect.width): - self._set_dimensions((new_width, new_height)) + self.set_dimensions((new_width, new_height)) def _update_relative_rect_position_from_anchors(self, recalculate_margins=False): """ @@ -764,7 +764,7 @@ def set_minimum_dimensions(self, dimensions: Union[pygame.math.Vector2, (self.rect.height < self.minimum_dimensions[1])): new_width = max(self.minimum_dimensions[0], self.rect.width) new_height = max(self.minimum_dimensions[1], self.rect.height) - self._set_dimensions((new_width, new_height)) + self.set_dimensions((new_width, new_height)) def set_dimensions(self, dimensions: Union[pygame.math.Vector2, Tuple[int, int], @@ -800,9 +800,9 @@ def set_dimensions(self, dimensions: Union[pygame.math.Vector2, self._set_dimensions(dimensions, clamp_to_container) def _set_dimensions(self, dimensions: Union[pygame.math.Vector2, - Tuple[int, int], - Tuple[float, float]], - clamp_to_container: bool = False): + Tuple[int, int], + Tuple[float, float]], + clamp_to_container: bool = False): """ Method to directly set the dimensions of an element. Dimensions must be positive values. diff --git a/pygame_gui/data/default_theme.json b/pygame_gui/data/default_theme.json index 5b5843fa..2ec33eb3 100644 --- a/pygame_gui/data/default_theme.json +++ b/pygame_gui/data/default_theme.json @@ -231,6 +231,13 @@ "rect_width": "170" } }, + "file_dialog.button.tool_tip": + { + "misc": + { + "rect_width": "-1" + } + }, "vertical_scroll_bar": { "prototype": "#default_shape_style" diff --git a/pygame_gui/elements/ui_text_box.py b/pygame_gui/elements/ui_text_box.py index 61d493f7..aa35f0c8 100644 --- a/pygame_gui/elements/ui_text_box.py +++ b/pygame_gui/elements/ui_text_box.py @@ -588,7 +588,7 @@ def parse_html_into_style_data(self): text_direction=self.parser.default_style['direction']) self.text_box_layout.set_cursor_colour(self.text_cursor_colour) self.parser.empty_layout_queue() - if not self.dynamic_height: + if self.dynamic_height: self.text_box_layout.view_rect.height = self.text_box_layout.layout_rect.height self._align_all_text_rows() diff --git a/pygame_gui/elements/ui_text_entry_box.py b/pygame_gui/elements/ui_text_entry_box.py index c9c8f4a8..a91a844b 100644 --- a/pygame_gui/elements/ui_text_entry_box.py +++ b/pygame_gui/elements/ui_text_entry_box.py @@ -132,6 +132,9 @@ def focus(self): super().focus() key.set_repeat(500, 25) + self.text_box_layout.set_cursor_position(self.edit_position) + self.cursor_has_moved_recently = True + def update(self, time_delta: float): """ Called every update loop of our UI Manager. Largely handles text drag selection and @@ -338,84 +341,49 @@ def _process_edit_pos_move_key(self, event: Event) -> bool: """ consumed_event = False - # modded versions of LEFT and RIGHT must go first otherwise the simple - # cases will absorb the events - if (event.key == K_HOME or - (event.key == K_LEFT and - (event.mod & KMOD_CTRL or event.mod & KMOD_META))): - try: - next_pos = self.edit_position - self.html_text[:self.edit_position][::-1].index("\n") - except ValueError: - next_pos = 0 - # include case when shift held down to select everything to start - # of current line. - if abs(self.select_range[0] - self.select_range[1]) > 0: - if event.mod & KMOD_SHIFT: - if self.edit_position == self.select_range[1]: - # undo selection to right, create to left - self.select_range = [next_pos, self.select_range[0]] - else: - # extend left - self.select_range = [next_pos, self.select_range[1]] - else: - self.select_range = [0, 0] + # 4 left/right cases: + # 1. jump left or right one character (LEFT or RIGHT on their own) + # 2. jump to end/start of word (CTRL + LEFT or RIGHT) + # 3. jump to end/start of line (HOME or END) + # 4. jump to end/start of document (CTRL + HOME or END) + # plus 1 up or down a line/text row case (UP & DOWN arrows, CTRL makes no difference here in notepad, + # but in IDE it scrolls if possible) + # All cases can have selection attached with shift held + + if event.key == K_LEFT: + if event.mod & KMOD_CTRL or event.mod & KMOD_META: + self._jump_edit_pos_to_start_of_word(should_select=(event.mod & KMOD_SHIFT)) else: - if event.mod & KMOD_SHIFT: - self.select_range = [next_pos, self.edit_position] - else: - self.select_range = [0, 0] - self.edit_position = next_pos - self.cursor_has_moved_recently = True + self._jump_edit_pos_one_character_left(should_select=(event.mod & KMOD_SHIFT)) consumed_event = True - elif (event.key == K_END or - (event.key == K_RIGHT and - (event.mod & KMOD_CTRL or event.mod & KMOD_META))): - try: - next_pos = self.edit_position + self.html_text[self.edit_position:].index("\n") - except ValueError: - next_pos = len(self.html_text) - # include case when shift held down to select everything to end - # of current line. - if abs(self.select_range[0] - self.select_range[1]) > 0: - if event.mod & KMOD_SHIFT: - if self.edit_position == self.select_range[0]: - # undo selection to left, create to right - self.select_range = [self.select_range[1], next_pos] - else: - # extend right - self.select_range = [self.select_range[0], next_pos] - else: - self.select_range = [0, 0] + elif event.key == K_RIGHT: + if event.mod & KMOD_CTRL or event.mod & KMOD_META: + self._jump_edit_pos_to_end_of_word(should_select=(event.mod & KMOD_SHIFT)) else: - if event.mod & KMOD_SHIFT: - self.select_range = [self.edit_position, next_pos] - else: - self.select_range = [0, 0] - self.edit_position = next_pos - self.cursor_has_moved_recently = True + self._jump_edit_pos_one_character_right(should_select=(event.mod & KMOD_SHIFT)) consumed_event = True - elif event.key == K_LEFT and event.mod & KMOD_SHIFT: - # keyboard-based selection - if abs(self.select_range[0] - self.select_range[1]) > 0: - # existing selection, so edit_position should correspond to one - # end of it - if self.edit_position == self.select_range[0]: - # try to extend to the left - self.select_range = [max(0, self.edit_position - 1), - self.select_range[1]] - elif self.edit_position == self.select_range[1]: - # reduce to the left - self.select_range = [self.select_range[0], - max(0, self.edit_position - 1)] + elif event.key == K_UP: + self._jump_edit_pos_one_row_up(should_select=(event.mod & KMOD_SHIFT)) + consumed_event = True + elif event.key == K_DOWN: + self._jump_edit_pos_one_row_down(should_select=(event.mod & KMOD_SHIFT)) + consumed_event = True + elif event.key == K_HOME: + if event.mod & KMOD_CTRL or event.mod & KMOD_META: + self._jump_edit_pos_to_start_of_all_text(should_select=(event.mod & KMOD_SHIFT)) else: - self.select_range = [max(0, self.edit_position - 1), - self.edit_position] - if self.edit_position > 0: - self.edit_position -= 1 - self.cursor_has_moved_recently = True + self._jump_edit_pos_to_start_of_line(should_select=(event.mod & KMOD_SHIFT)) + consumed_event = True + elif event.key == K_END: + if event.mod & KMOD_CTRL or event.mod & KMOD_META: + self._jump_edit_pos_to_end_of_all_text(should_select=(event.mod & KMOD_SHIFT)) + else: + self._jump_edit_pos_to_end_of_line(should_select=(event.mod & KMOD_SHIFT)) consumed_event = True - elif event.key == K_RIGHT and event.mod & KMOD_SHIFT: - # keyboard-based selection + return consumed_event + + def _jump_edit_pos_one_character_right(self, should_select=False): + if should_select: if abs(self.select_range[0] - self.select_range[1]) > 0: if self.edit_position == self.select_range[1]: # try to extend to the right @@ -431,29 +399,45 @@ def _process_edit_pos_move_key(self, event: Event) -> bool: if self.edit_position < len(self.html_text): self.edit_position += 1 self.cursor_has_moved_recently = True - consumed_event = True - elif event.key == K_UP and event.mod & KMOD_SHIFT: - # keyboard-based selection - new_pos = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index) + else: + if abs(self.select_range[0] - self.select_range[1]) > 0: + self.edit_position = max(self.select_range[0], self.select_range[1]) + self.select_range = [0, 0] + self.cursor_has_moved_recently = True + elif self.edit_position < len(self.html_text): + self.edit_position += 1 + self.cursor_has_moved_recently = True + + def _jump_edit_pos_one_character_left(self, should_select=False): + if should_select: if abs(self.select_range[0] - self.select_range[1]) > 0: # existing selection, so edit_position should correspond to one # end of it if self.edit_position == self.select_range[0]: # try to extend to the left - self.select_range = [max(0, new_pos), + self.select_range = [max(0, self.edit_position - 1), self.select_range[1]] elif self.edit_position == self.select_range[1]: # reduce to the left self.select_range = [self.select_range[0], - max(0, new_pos)] + max(0, self.edit_position - 1)] else: - self.select_range = [max(0, new_pos), + self.select_range = [max(0, self.edit_position - 1), self.edit_position] if self.edit_position > 0: - self.edit_position = new_pos + self.edit_position -= 1 self.cursor_has_moved_recently = True - elif event.key == K_DOWN and event.mod & KMOD_SHIFT: - # keyboard-based selection + else: + if abs(self.select_range[0] - self.select_range[1]) > 0: + self.edit_position = min(self.select_range[0], self.select_range[1]) + self.select_range = [0, 0] + self.cursor_has_moved_recently = True + elif self.edit_position > 0: + self.edit_position -= 1 + self.cursor_has_moved_recently = True + + def _jump_edit_pos_one_row_down(self, should_select=False): + if should_select: new_pos = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index) if abs(self.select_range[0] - self.select_range[1]) > 0: if self.edit_position == self.select_range[1]: @@ -470,26 +454,37 @@ def _process_edit_pos_move_key(self, event: Event) -> bool: if self.edit_position < len(self.html_text): self.edit_position = new_pos self.cursor_has_moved_recently = True - consumed_event = True - elif event.key == K_LEFT: + else: if abs(self.select_range[0] - self.select_range[1]) > 0: - self.edit_position = min(self.select_range[0], self.select_range[1]) + self.edit_position = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index) self.select_range = [0, 0] self.cursor_has_moved_recently = True - elif self.edit_position > 0: - self.edit_position -= 1 + else: + self.edit_position = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index) self.cursor_has_moved_recently = True - consumed_event = True - elif event.key == K_RIGHT: + self.vertical_cursor_movement = True + + def _jump_edit_pos_one_row_up(self, should_select=False): + if should_select: + new_pos = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index) if abs(self.select_range[0] - self.select_range[1]) > 0: - self.edit_position = max(self.select_range[0], self.select_range[1]) - self.select_range = [0, 0] - self.cursor_has_moved_recently = True - elif self.edit_position < len(self.html_text): - self.edit_position += 1 + # existing selection, so edit_position should correspond to one + # end of it + if self.edit_position == self.select_range[0]: + # try to extend to the left + self.select_range = [max(0, new_pos), + self.select_range[1]] + elif self.edit_position == self.select_range[1]: + # reduce to the left + self.select_range = [self.select_range[0], + max(0, new_pos)] + else: + self.select_range = [max(0, new_pos), + self.edit_position] + if self.edit_position > 0: + self.edit_position = new_pos self.cursor_has_moved_recently = True - consumed_event = True - elif event.key == K_UP: + else: if abs(self.select_range[0] - self.select_range[1]) > 0: self.edit_position = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index) self.select_range = [0, 0] @@ -498,30 +493,146 @@ def _process_edit_pos_move_key(self, event: Event) -> bool: self.edit_position = self.text_box_layout.get_cursor_pos_move_up_one_row(self.last_horiz_cursor_index) self.cursor_has_moved_recently = True self.vertical_cursor_movement = True - consumed_event = True - elif event.key == K_DOWN: - if abs(self.select_range[0] - self.select_range[1]) > 0: - self.edit_position = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index) + + def _jump_edit_pos_to_start_of_word(self, should_select=False): + try: + text_to_search = self.html_text[max(0, self.edit_position-1)::-1] + match_result = re.search(r'\b\w+\b', text_to_search) + if match_result is not None: + next_pos = self.edit_position - match_result.regs[-1][1] + else: + next_pos = self.edit_position + except ValueError: + next_pos = 0 + # include case when shift held down to select everything to start + # of current line. + if abs(self.select_range[0] - self.select_range[1]) > 0: + if should_select: + if self.edit_position == self.select_range[1]: + # undo selection to right, create to left + self.select_range = [next_pos, self.select_range[0]] + else: + # extend left + self.select_range = [next_pos, self.select_range[1]] + else: self.select_range = [0, 0] - self.cursor_has_moved_recently = True + else: + if should_select: + self.select_range = [next_pos, self.edit_position] else: - self.edit_position = self.text_box_layout.get_cursor_pos_move_down_one_row(self.last_horiz_cursor_index) - self.cursor_has_moved_recently = True - self.vertical_cursor_movement = True - consumed_event = True - elif event.key == K_HOME: + self.select_range = [0, 0] + self.edit_position = next_pos + self.cursor_has_moved_recently = True + + def _jump_edit_pos_to_end_of_word(self, should_select=False): + try: + match_result = re.search(r'\b\w+\b', self.html_text[self.edit_position:]) + if match_result is not None: + next_pos = self.edit_position + match_result.regs[0][1] + else: + next_pos = self.edit_position + except ValueError: + next_pos = len(self.html_text) + # include case when shift held down to select everything to end + # of current line. + if abs(self.select_range[0] - self.select_range[1]) > 0: + if should_select: + if self.edit_position == self.select_range[0]: + # undo selection to left, create to right + self.select_range = [self.select_range[1], next_pos] + else: + # extend right + self.select_range = [self.select_range[0], next_pos] + else: + self.select_range = [0, 0] + else: + if should_select: + self.select_range = [self.edit_position, next_pos] + else: + self.select_range = [0, 0] + self.edit_position = next_pos + self.cursor_has_moved_recently = True + + def _jump_edit_pos_to_start_of_line(self, should_select=False): + try: + next_pos = self.text_box_layout.set_cursor_to_start_of_current_row() + except ValueError: + next_pos = 0 + # include case when shift held down to select everything to start + # of current line. + if abs(self.select_range[0] - self.select_range[1]) > 0: + if should_select: + if self.edit_position == self.select_range[1]: + # undo selection to right, create to left + self.select_range = [next_pos, self.select_range[0]] + else: + # extend left + self.select_range = [next_pos, self.select_range[1]] + else: + self.select_range = [0, 0] + else: + if should_select: + self.select_range = [next_pos, self.edit_position] + else: + self.select_range = [0, 0] + self.edit_position = next_pos + self.cursor_has_moved_recently = True + + def _jump_edit_pos_to_end_of_line(self, should_select=False): + try: + next_pos = self.text_box_layout.set_cursor_to_end_of_current_row() + except ValueError: + next_pos = len(self.html_text) + # include case when shift held down to select everything to end + # of current line. + if abs(self.select_range[0] - self.select_range[1]) > 0: + if should_select: + if self.edit_position == self.select_range[0]: + # undo selection to left, create to right + self.select_range = [self.select_range[1], next_pos] + else: + # extend right + self.select_range = [self.select_range[0], next_pos] + else: + self.select_range = [0, 0] + else: + if should_select: + self.select_range = [self.edit_position, next_pos] + else: + self.select_range = [0, 0] + self.edit_position = next_pos + self.cursor_has_moved_recently = True + + def _jump_edit_pos_to_start_of_all_text(self, should_select=False): + if should_select: + if abs(self.select_range[0] - self.select_range[1]) > 0: + if self.edit_position == self.select_range[0]: + self.select_range = [0, self.select_range[1]] + else: + self.select_range = [0, self.select_range[0]] + else: + self.select_range = [0, self.edit_position] + else: if abs(self.select_range[0] - self.select_range[1]) > 0: self.select_range = [0, 0] - self.edit_position = 0 - self.cursor_has_moved_recently = True - consumed_event = True - elif event.key == K_END: + self.edit_position = 0 + self.cursor_has_moved_recently = True + + def _jump_edit_pos_to_end_of_all_text(self, should_select=False): + end_of_all_pos = len(self.html_text) + if should_select: + if abs(self.select_range[0] - self.select_range[1]) > 0: + if self.edit_position == self.select_range[0]: + self.select_range = [self.select_range[1], end_of_all_pos] + else: + self.select_range = [self.select_range[0], end_of_all_pos] + else: + self.select_range = [self.edit_position, end_of_all_pos] + else: if abs(self.select_range[0] - self.select_range[1]) > 0: self.select_range = [0, 0] - self.edit_position = len(self.html_text) - self.cursor_has_moved_recently = True - consumed_event = True - return consumed_event + self.edit_position = end_of_all_pos + self.cursor_has_moved_recently = True def _process_mouse_button_event(self, event: Event) -> bool: """ diff --git a/pygame_gui/windows/ui_file_dialog.py b/pygame_gui/windows/ui_file_dialog.py index 8ac1a43e..7764439a 100644 --- a/pygame_gui/windows/ui_file_dialog.py +++ b/pygame_gui/windows/ui_file_dialog.py @@ -70,6 +70,8 @@ def __init__(self, self.current_file_path = None # type: Union[Path, None] if allowed_suffixes is None: self.allowed_suffixes = {""} + else: + self.allowed_suffixes = allowed_suffixes if initial_file_path is not None: pathed_initial_file_path = Path(initial_file_path) if pathed_initial_file_path.exists() and not pathed_initial_file_path.is_file(): From 680bb97b1b3ccffa21e8aed7893613235a902d98 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sun, 3 Mar 2024 20:54:09 +0000 Subject: [PATCH 2/5] Improve the text box entry keyboard controls also fix a couple of other minor bugs noticed in testing --- pygame_gui/core/ui_element.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pygame_gui/core/ui_element.py b/pygame_gui/core/ui_element.py index 8b804a1d..98c71515 100644 --- a/pygame_gui/core/ui_element.py +++ b/pygame_gui/core/ui_element.py @@ -556,8 +556,16 @@ def _update_absolute_rect_position_from_anchors(self, recalculate_margins=False) self.rect.left = new_left self.rect.top = new_top - new_height = new_bottom - new_top - new_width = new_right - new_left + if self.dynamic_height: + new_height = new_bottom - new_top + else: + new_height = max(0, new_bottom - new_top) + + if self.dynamic_width: + new_width = new_right - new_left + else: + new_width = max(0, new_right - new_left) + new_width, new_height = self._get_clamped_to_minimum_dimensions((new_width, new_height)) if (new_height != self.relative_rect.height) or (new_width != self.relative_rect.width): self.set_dimensions((new_width, new_height)) From 3978bf5a3efc7a45733ed203ab01cd7e61b3d2d5 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Sun, 3 Mar 2024 22:10:13 +0000 Subject: [PATCH 3/5] tests for text entry box controls --- tests/test_elements/test_ui_text_entry_box.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_elements/test_ui_text_entry_box.py b/tests/test_elements/test_ui_text_entry_box.py index a5c5acb5..d248060a 100644 --- a/tests/test_elements/test_ui_text_entry_box.py +++ b/tests/test_elements/test_ui_text_entry_box.py @@ -1171,6 +1171,16 @@ def test_process_event_text_right_actually_move(self, _init_pygame: None, assert processed_key_event + text_entry.edit_position = 0 + text_entry.focus() + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.edit_position == 3 + def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1187,6 +1197,13 @@ def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: U assert processed_key_event assert text_entry.edit_position == 2 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.edit_position == 0 + def test_process_event_home(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1204,6 +1221,14 @@ def test_process_event_home(self, _init_pygame: None, default_ui_manager: UIMana assert text_entry.select_range == [0, 0] assert text_entry.edit_position == 0 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_HOME, + 'mod': pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 0 + text_entry.set_text('daniel\nmulti-line\ntext') text_entry.edit_position = 3 text_entry.focus() @@ -1217,6 +1242,14 @@ def test_process_event_home(self, _init_pygame: None, default_ui_manager: UIMana assert text_entry.select_range == [0, 1] assert text_entry.edit_position == 0 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_HOME, + 'mod': pygame.KMOD_SHIFT | pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.select_range == [0, 1] + assert text_entry.edit_position == 0 + def test_process_event_end(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1234,6 +1267,14 @@ def test_process_event_end(self, _init_pygame: None, default_ui_manager: UIManag assert text_entry.select_range == [0, 0] assert text_entry.edit_position == 3 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_END, + 'mod': pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 3 + text_entry.set_text('daniel\nmulti-line\ntext') text_entry.edit_position = 3 text_entry.focus() @@ -1247,6 +1288,14 @@ def test_process_event_end(self, _init_pygame: None, default_ui_manager: UIManag assert text_entry.select_range == [1, len("daniel")] assert text_entry.edit_position == len("daniel") + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_END, + 'mod': pygame.KMOD_SHIFT | pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.select_range == [1, len("daniel\nmulti-line\ntext")] + assert text_entry.edit_position == len("daniel\nmulti-line\ntext") + def test_process_event_text_right_select_range(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): From 4442f6034af83ab381fa2b80f0e7744d3f835449 Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Mon, 4 Mar 2024 19:44:54 +0000 Subject: [PATCH 4/5] Add more tests for text entry box controls --- tests/test_elements/test_ui_text_entry_box.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/tests/test_elements/test_ui_text_entry_box.py b/tests/test_elements/test_ui_text_entry_box.py index d248060a..34c2f34d 100644 --- a/tests/test_elements/test_ui_text_entry_box.py +++ b/tests/test_elements/test_ui_text_entry_box.py @@ -1170,6 +1170,42 @@ def test_process_event_text_right_actually_move(self, _init_pygame: None, 'mod': 0})) assert processed_key_event + assert text_entry.edit_position == 3 + + text_entry.edit_position = 1 + text_entry.focus() + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [1, 2] + assert text_entry.edit_position == 2 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [1, 3] + assert text_entry.edit_position == 3 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [1, 2] + assert text_entry.edit_position == 2 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': 0})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 2 text_entry.edit_position = 0 text_entry.focus() @@ -1181,6 +1217,17 @@ def test_process_event_text_right_actually_move(self, _init_pygame: None, assert processed_key_event assert text_entry.edit_position == 3 + text_entry.edit_position = 0 + text_entry.focus() + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_CTRL | pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [0, 3] + assert text_entry.edit_position == 3 + def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1197,6 +1244,38 @@ def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: U assert processed_key_event assert text_entry.edit_position == 2 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [1, 2] + assert text_entry.edit_position == 1 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [0, 2] + assert text_entry.edit_position == 0 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [1, 2] + assert text_entry.edit_position == 1 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': 0})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 1 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_LEFT, 'mod': pygame.KMOD_CTRL})) @@ -1204,6 +1283,75 @@ def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: U assert processed_key_event assert text_entry.edit_position == 0 + text_entry.edit_position = 3 + text_entry.focus() + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_CTRL | pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [0, 3] + assert text_entry.edit_position == 0 + + def test_process_event_text_down(self, _init_pygame: None, default_ui_manager: UIManager, + _display_surface_return_none: None): + text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), + manager=default_ui_manager) + + text_entry.set_text('daniel\nmulti-line\ntext') + text_entry.edit_position = 3 + text_entry.focus() + text_entry.update(0.1) + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_DOWN, + 'mod': 0})) + + assert processed_key_event + assert text_entry.edit_position == 10 + + text_entry.edit_position = 3 + text_entry.focus() + text_entry.update(0.1) + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_DOWN, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [3, 10] + assert text_entry.edit_position == 10 + + def test_process_event_text_up(self, _init_pygame: None, default_ui_manager: UIManager, + _display_surface_return_none: None): + text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), + manager=default_ui_manager) + + text_entry.set_text('daniel\nmulti-line\ntext') + text_entry.edit_position = 10 + text_entry.focus() + text_entry.update(0.1) + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_UP, + 'mod': 0})) + + assert processed_key_event + assert text_entry.edit_position == 3 + + text_entry.edit_position = 10 + text_entry.focus() + text_entry.update(0.1) + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_UP, + 'mod': pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [3, 10] + assert text_entry.edit_position == 3 + def test_process_event_home(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), From 860bb786693068a648a39912d3cbfcae6eea245c Mon Sep 17 00:00:00 2001 From: Dan Lawrence Date: Mon, 4 Mar 2024 20:59:37 +0000 Subject: [PATCH 5/5] Add more tests for text entry box controls --- pygame_gui/elements/ui_text_entry_box.py | 5 +- tests/test_elements/test_ui_text_entry_box.py | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/pygame_gui/elements/ui_text_entry_box.py b/pygame_gui/elements/ui_text_entry_box.py index a91a844b..35fd2687 100644 --- a/pygame_gui/elements/ui_text_entry_box.py +++ b/pygame_gui/elements/ui_text_entry_box.py @@ -499,7 +499,7 @@ def _jump_edit_pos_to_start_of_word(self, should_select=False): text_to_search = self.html_text[max(0, self.edit_position-1)::-1] match_result = re.search(r'\b\w+\b', text_to_search) if match_result is not None: - next_pos = self.edit_position - match_result.regs[-1][1] + next_pos = max(self.edit_position - match_result.regs[-1][1], 0) else: next_pos = self.edit_position except ValueError: @@ -515,6 +515,7 @@ def _jump_edit_pos_to_start_of_word(self, should_select=False): # extend left self.select_range = [next_pos, self.select_range[1]] else: + next_pos = self.select_range[0] self.select_range = [0, 0] else: if should_select: @@ -528,7 +529,7 @@ def _jump_edit_pos_to_end_of_word(self, should_select=False): try: match_result = re.search(r'\b\w+\b', self.html_text[self.edit_position:]) if match_result is not None: - next_pos = self.edit_position + match_result.regs[0][1] + next_pos = min(self.edit_position + match_result.regs[0][1], len(self.html_text)) else: next_pos = self.edit_position except ValueError: diff --git a/tests/test_elements/test_ui_text_entry_box.py b/tests/test_elements/test_ui_text_entry_box.py index 34c2f34d..aa88e907 100644 --- a/tests/test_elements/test_ui_text_entry_box.py +++ b/tests/test_elements/test_ui_text_entry_box.py @@ -1228,6 +1228,30 @@ def test_process_event_text_right_actually_move(self, _init_pygame: None, assert text_entry.select_range == [0, 3] assert text_entry.edit_position == 3 + text_entry.set_text('dan dan') + text_entry.edit_position = 0 + text_entry.select_range = [0, 0] + text_entry.focus() + + text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_CTRL | pygame.KMOD_SHIFT})) + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_CTRL | pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [0, 7] + assert text_entry.edit_position == 7 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_RIGHT, + 'mod': pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 7 + def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1294,6 +1318,30 @@ def test_process_event_text_left(self, _init_pygame: None, default_ui_manager: U assert text_entry.select_range == [0, 3] assert text_entry.edit_position == 0 + text_entry.set_text('dan dan') + text_entry.edit_position = 7 + text_entry.select_range = [0, 0] + text_entry.focus() + + text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_CTRL | pygame.KMOD_SHIFT})) + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_CTRL | pygame.KMOD_SHIFT})) + + assert processed_key_event + assert text_entry.select_range == [0, 7] + assert text_entry.edit_position == 0 + + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_LEFT, + 'mod': pygame.KMOD_CTRL})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 0 + def test_process_event_text_down(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1323,6 +1371,13 @@ def test_process_event_text_down(self, _init_pygame: None, default_ui_manager: U assert text_entry.select_range == [3, 10] assert text_entry.edit_position == 10 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_DOWN, + 'mod': 0})) + assert text_entry.select_range == [0, 0] + assert processed_key_event + assert text_entry.edit_position == 10 + def test_process_event_text_up(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30), @@ -1352,6 +1407,14 @@ def test_process_event_text_up(self, _init_pygame: None, default_ui_manager: UIM assert text_entry.select_range == [3, 10] assert text_entry.edit_position == 3 + processed_key_event = text_entry.process_event(pygame.event.Event(pygame.KEYDOWN, + {'key': pygame.K_UP, + 'mod': 0})) + + assert processed_key_event + assert text_entry.select_range == [0, 0] + assert text_entry.edit_position == 3 + def test_process_event_home(self, _init_pygame: None, default_ui_manager: UIManager, _display_surface_return_none: None): text_entry = UITextEntryBox(relative_rect=pygame.Rect(100, 100, 200, 30),