From d6523c1a64f9b7fd672b73965bb6aa275c7f0ee3 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 25 Jan 2025 00:02:53 +0100 Subject: [PATCH 1/8] Fix MaskedInput selection replace with invalid value --- src/textual/widgets/_masked_input.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 560258f332..ca0d9f1adf 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -610,6 +610,18 @@ def insert_text_at_cursor(self, text: str) -> None: else: self.restricted() + def replace(self, text: str, start: int, end: int): + """Replace the text between the start and end locations with the given text. + + Args: + text: Text to replace the existing text with. + start: Start index to replace (inclusive). + end: End index to replace (inclusive). + """ + + self.cursor_position = start + self.insert_text_at_cursor(text) + def clear(self) -> None: """Clear the masked input.""" self.value, self.cursor_position = self._template.insert_separators("", 0) From 46df6f9aaa9caeb9cbdc8821febd664c3930eff6 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 25 Jan 2025 08:56:00 +0100 Subject: [PATCH 2/8] Fix _Template.insert_text_at_cursor() type annotation --- src/textual/widgets/_masked_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index ca0d9f1adf..13280cc830 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -200,7 +200,7 @@ def insert_separators(self, value: str, cursor_position: int) -> tuple[str, int] cursor_position += 1 return value, cursor_position - def insert_text_at_cursor(self, text: str) -> str | None: + def insert_text_at_cursor(self, text: str) -> tuple[str, int] | None: """Inserts `text` at current cursor position. If not present in `text`, any expected separator is automatically inserted at the correct position. From 8be6eb8aacbb0447901952cffcd58c7aff4de6bf Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 25 Jan 2025 08:58:32 +0100 Subject: [PATCH 3/8] Keep the cursor position for bad inputs --- src/textual/widgets/_masked_input.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 13280cc830..aec6df760b 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -619,8 +619,14 @@ def replace(self, text: str, start: int, end: int): end: End index to replace (inclusive). """ + old_cursor_position = self.cursor_position self.cursor_position = start - self.insert_text_at_cursor(text) + new_value = self._template.insert_text_at_cursor(text) + if new_value is not None: + self.value, self.cursor_position = new_value + else: + self.cursor_position = old_cursor_position + self.restricted() def clear(self) -> None: """Clear the masked input.""" From 88a791421f553211ba92667b8c6779e88194ab72 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 25 Jan 2025 20:47:19 +0100 Subject: [PATCH 4/8] Fix replace logic --- src/textual/widgets/_masked_input.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index aec6df760b..09c65aa3d3 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -623,7 +623,9 @@ def replace(self, text: str, start: int, end: int): self.cursor_position = start new_value = self._template.insert_text_at_cursor(text) if new_value is not None: - self.value, self.cursor_position = new_value + value, cursor_position = new_value + self.value = value[:cursor_position] + value[end:] + self.cursor_position = cursor_position else: self.cursor_position = old_cursor_position self.restricted() From 346ec6f2153d0ce41a476ff0077d9c531ee151ae Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 1 Feb 2025 07:46:14 +0100 Subject: [PATCH 5/8] Fix replace for partial selection --- src/textual/widgets/_masked_input.py | 43 +++++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 09c65aa3d3..fe5b396158 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -200,7 +200,7 @@ def insert_separators(self, value: str, cursor_position: int) -> tuple[str, int] cursor_position += 1 return value, cursor_position - def insert_text_at_cursor(self, text: str) -> tuple[str, int] | None: + def insert_text_at_cursor(self, text: str, value: str, cursor_position: int) -> tuple[str, int] | None: """Inserts `text` at current cursor position. If not present in `text`, any expected separator is automatically inserted at the correct position. @@ -211,8 +211,6 @@ def insert_text_at_cursor(self, text: str) -> tuple[str, int] | None: A tuple in the form `(value, cursor_position)` with the new control value and current cursor position if `text` matches the template, None otherwise. """ - value = self.input.value - cursor_position = self.input.cursor_position separators = set( [ char_definition.char @@ -604,7 +602,7 @@ def insert_text_at_cursor(self, text: str) -> None: text: New text to insert. """ - new_value = self._template.insert_text_at_cursor(text) + new_value = self._template.insert_text_at_cursor(text, self.value, self.cursor_position) if new_value is not None: self.value, self.cursor_position = new_value else: @@ -619,16 +617,33 @@ def replace(self, text: str, start: int, end: int): end: End index to replace (inclusive). """ - old_cursor_position = self.cursor_position - self.cursor_position = start - new_value = self._template.insert_text_at_cursor(text) - if new_value is not None: - value, cursor_position = new_value - self.value = value[:cursor_position] + value[end:] - self.cursor_position = cursor_position - else: - self.cursor_position = old_cursor_position - self.restricted() + previous_cursor_position = self.cursor_position + value = self.value + cursor_position = start + for char in text: + new_value_cursor_position = self._template.insert_text_at_cursor(char, value, cursor_position) + if new_value_cursor_position is None: + self.value = value + self.cursor_position = previous_cursor_position + self.restricted() + return + + new_value, new_cursor_position = new_value_cursor_position + if new_cursor_position >= end: + self.value = new_value[:end] + value[end:] + self.cursor_position = new_cursor_position + return + + value = new_value + cursor_position = new_cursor_position + + self.value = value + self.cursor_position = end + while self.cursor_position > cursor_position: + self._template.move_cursor(-1) + self._template.delete_at_position() + + self.cursor_position = cursor_position def clear(self) -> None: """Clear the masked input.""" From 9537a9d2291c16d8b4474ad53ef177dc8362aac6 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 1 Feb 2025 08:05:46 +0100 Subject: [PATCH 6/8] Clean up the code --- src/textual/widgets/_masked_input.py | 33 +++++++++++----------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index fe5b396158..9ffb1c4c58 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -200,7 +200,7 @@ def insert_separators(self, value: str, cursor_position: int) -> tuple[str, int] cursor_position += 1 return value, cursor_position - def insert_text_at_cursor(self, text: str, value: str, cursor_position: int) -> tuple[str, int] | None: + def insert_text_at_cursor(self, text: str) -> tuple[str, int] | None: """Inserts `text` at current cursor position. If not present in `text`, any expected separator is automatically inserted at the correct position. @@ -211,6 +211,8 @@ def insert_text_at_cursor(self, text: str, value: str, cursor_position: int) -> A tuple in the form `(value, cursor_position)` with the new control value and current cursor position if `text` matches the template, None otherwise. """ + value = self.input.value + cursor_position = self.input.cursor_position separators = set( [ char_definition.char @@ -602,7 +604,7 @@ def insert_text_at_cursor(self, text: str) -> None: text: New text to insert. """ - new_value = self._template.insert_text_at_cursor(text, self.value, self.cursor_position) + new_value = self._template.insert_text_at_cursor(text) if new_value is not None: self.value, self.cursor_position = new_value else: @@ -617,33 +619,24 @@ def replace(self, text: str, start: int, end: int): end: End index to replace (inclusive). """ - previous_cursor_position = self.cursor_position - value = self.value - cursor_position = start + self.cursor_position = start for char in text: - new_value_cursor_position = self._template.insert_text_at_cursor(char, value, cursor_position) + if self.cursor_position >= end: + return + new_value_cursor_position = self._template.insert_text_at_cursor(char) if new_value_cursor_position is None: - self.value = value - self.cursor_position = previous_cursor_position self.restricted() return - new_value, new_cursor_position = new_value_cursor_position - if new_cursor_position >= end: - self.value = new_value[:end] + value[end:] - self.cursor_position = new_cursor_position - return + self.value, self.cursor_position = new_value_cursor_position - value = new_value - cursor_position = new_cursor_position + last_cursor_position = self.cursor_position - self.value = value - self.cursor_position = end - while self.cursor_position > cursor_position: - self._template.move_cursor(-1) + while self.cursor_position < end: self._template.delete_at_position() + self._template.move_cursor(1) - self.cursor_position = cursor_position + self.cursor_position = last_cursor_position def clear(self) -> None: """Clear the masked input.""" From a40592c2e349ad5712385c491df95e3a9683bc40 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Sat, 1 Feb 2025 08:13:56 +0100 Subject: [PATCH 7/8] Handle selection starting on a separator --- src/textual/widgets/_masked_input.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 9ffb1c4c58..2fcb82572d 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -620,6 +620,12 @@ def replace(self, text: str, start: int, end: int): """ self.cursor_position = start + # Handle case where cursor start on a separator + self._template.move_cursor(1) + self._template.move_cursor(-1) + if self.cursor_position < start: + self._template.move_cursor(1) + for char in text: if self.cursor_position >= end: return @@ -627,15 +633,12 @@ def replace(self, text: str, start: int, end: int): if new_value_cursor_position is None: self.restricted() return - self.value, self.cursor_position = new_value_cursor_position last_cursor_position = self.cursor_position - while self.cursor_position < end: self._template.delete_at_position() self._template.move_cursor(1) - self.cursor_position = last_cursor_position def clear(self) -> None: From 8a2482ac5bd025d528943bc2c9d14d919e666599 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse Date: Fri, 7 Feb 2025 10:30:18 +0100 Subject: [PATCH 8/8] Fix infinite loop when the full input text is selected --- src/textual/widgets/_masked_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_masked_input.py b/src/textual/widgets/_masked_input.py index 2fcb82572d..31be028060 100644 --- a/src/textual/widgets/_masked_input.py +++ b/src/textual/widgets/_masked_input.py @@ -636,7 +636,7 @@ def replace(self, text: str, start: int, end: int): self.value, self.cursor_position = new_value_cursor_position last_cursor_position = self.cursor_position - while self.cursor_position < end: + while self.cursor_position < min(end, len(self.value)): self._template.delete_at_position() self._template.move_cursor(1) self.cursor_position = last_cursor_position