class TextBox(Focusable, Caret, Hoverable): underline: Rectangle = None _on_enter: Callable = None ignore_enter: bool = False style: dict = {} def __init__(self, width: int, height: int = 0, multiline: bool = False, dpi: object = None, batch: Batch = None, group: Group = None, wrap_lines: bool = True, x: int = 0, y: int = 0, underlined: bool = True, caret_color: tuple = (0, 0, 0), numbers_only: bool = False, font_name=None, font_size=None, font_color=(255, 255, 255, 2555), max_chars=0) -> None: self.document = FormattedDocument() self.layout = IncrementalTextLayout(self.document, width, height, multiline, dpi, batch, group, wrap_lines) self.numbers_only = numbers_only self.style['color'] = font_color self.max_chars = max_chars if font_name: self.style['font_name'] = font_name if font_size: self.style['font_size'] = font_size self.reset_style() if not height: # If the dev didn't specify a height, make the height equal to the height of the font font = pyglet.font.load( font_name or self.document.get_style('font'), font_size or self.document.get_style('font_size')) self.height = font.ascent - font.descent self._hover_cursor = self.get_window().CURSOR_TEXT super().__init__(self.layout, color=caret_color) # TODO: Allow the dev to specify how x and y are treated self.x = x - self.width // 2 self.y = y - self.height // 2 if underlined: self.underline = Rectangle( x=self.x, y=self.y, width=self.width, height=1, ) def reset_style(self): self.document.set_style(0, len(self.document.text), self.style) @property def x(self): return self.layout.x @x.setter def x(self, value): if self.underline: self.underline.x = value self.layout.x = value @property def y(self): return self.layout.y @y.setter def y(self, value): print(value) if self.underline: self.underline.y = value self.layout.y = value @property def width(self): return self.layout.width @width.setter def width(self, value): if self.underline: self.underline.width = value self.layout.width = value @property def height(self): return self.layout.height @height.setter def height(self, value): print(value) self.layout.height = value def draw(self): if self.focused: self.on_activate() else: self.on_deactivate() self.layout.draw() if self.underline: self.underline.draw() def on_text(self, text: str): # Only type inside when the user is focused on the textbox # print(text) if not self.focused: return if ord(text) == 13: if self.ignore_enter: # Enter self.ignore_enter = False return else: return self.on_enter() # if self.max_chars and len(self.value) >= self.max_chars: # return # if self.numbers_only and not text.isnumeric(): # return res = super().on_text(text) print('res', res, 'text', text) self.reset_style() return res def _on_enter_base(self) -> None: """ The event handler that will be called by default if none are defined """ pass @property def value(self): return self.document.text @value.setter def value(self, val): self.document.text = val self.reset_style() @property def on_enter(self): if self._on_enter: return self._on_enter return self._on_enter_base @on_enter.setter def on_enter(self, func): def new_on_enter(): if self.focused: func() self._on_enter = new_on_enter
class Editable(Control): def __init__(self, value="", x=0, y=0, width=125, height=30, padding=(0, 0), wrap=True, id=None, **kwargs): """ An editable text box. When clicked, it has the focus and can receive keyboard events. With wrap=True, several lines of text will wrap around the width. Optional parameters can include fill, font, fontsize, fontweight. """ txt = Text( value or " ", **{ "fill": _popdefault(kwargs, "fill", Color(0, 0.9)), "font": _popdefault(kwargs, "font", theme["fontname"]), "fontsize": _popdefault(kwargs, "fontsize", theme["fontsize"]), "fontweight": _popdefault(kwargs, "fontweight", theme["fontweight"]), "lineheight": _popdefault(kwargs, "lineheight", 1), "align": LEFT, } ) kwargs["width"] = width kwargs["height"] = height Control.__init__(self, x=x, y=y, id=id, **kwargs) self.reserved = kwargs.get("reserved", [ENTER, TAB]) self._padding = padding self._i = 0 # Index of character on which the mouse is pressed. self._empty = value == "" and True or False self._editor = IncrementalTextLayout(txt._label.document, width, height, multiline=wrap) self._editor.content_valign = wrap and "top" or "center" self._editor.selection_background_color = (170, 200, 230, 255) self._editor.selection_color = txt._label.color self._editor.caret = Caret(self._editor) self._editor.caret.visible = False self._editing = False # When True, cursor is blinking and text can be edited. Editable._pack(self) # On init, call Editable._pack(), not the derived Field._pack(). def _pack(self): self._editor.x = self._padding[0] self._editor.y = self._padding[1] self._editor.width = max(0, self.width - self._padding[0] * 2) self._editor.height = max(0, self.height - self._padding[1] * 2) def _get_value(self): # IncrementalTextLayout in Pyglet 1.1.4 has a bug with empty strings. # We keep track of empty strings with Editable._empty to avoid this. return not self._empty and self._editor.document.text or u"" def _set_value(self, string): self._editor.begin_update() self._editor.document.text = string or " " self._editor.end_update() self._empty = string == "" and True or False value = property(_get_value, _set_value) def _get_editing(self): return self._editing def _set_editing(self, b): self._editing = b self._editor.caret.visible = b global EDITING if b is False and EDITING == self: EDITING = None if b is True: EDITING = self # Cursor is blinking and text can be edited. # Visit all layers on the canvas. # Remove the caret from all other Editable controls. for layer in self.root.canvas and self.root.canvas.layers or []: layer.traverse( visit=lambda layer: isinstance(layer, Editable) and layer != self and setattr(layer, "editing", False) ) editing = property(_get_editing, _set_editing) @property def selection(self): # Yields a (start, stop)-tuple with the indices of the current selected text. return (self._editor.selection_start, self._editor.selection_end) @property def selected(self): # Yields True when text is currently selected. return self.selection[0] != self.selection[1] @property def cursor(self): # Yields the index at the text cursor (caret). return self._editor.caret.position def index(self, x, y): """ Returns the index of the character in the text at position x, y. """ x0, y0 = self.absolute_position() i = self._editor.get_position_from_point(x - x0, y - y0) if self._editor.get_point_from_position(0)[0] > x - x0: # Pyglet bug? i = 0 if self._empty: i = 0 return i def on_mouse_enter(self, mouse): mouse.cursor = TEXT def on_mouse_press(self, mouse): i = self._i = self.index(mouse.x, mouse.y) self._editor.set_selection(0, 0) self.editing = True self._editor.caret.position = i Control.on_mouse_press(self, mouse) def on_mouse_release(self, mouse): if not self.dragged: self._editor.caret.position = self.index(mouse.x, mouse.y) Control.on_mouse_release(self, mouse) def on_mouse_drag(self, mouse): i = self.index(mouse.x, mouse.y) self._editor.selection_start = max(min(self._i, i), 0) self._editor.selection_end = min(max(self._i, i), len(self.value)) self._editor.caret.visible = False Control.on_mouse_drag(self, mouse) def on_mouse_doubleclick(self, mouse): # Select the word at the mouse position. # Words are delimited by non-alphanumeric characters. i = self.index(mouse.x, mouse.y) delimiter = lambda ch: not (ch.isalpha() or ch.isdigit()) if i < len(self.value) and delimiter(self.value[i]): self._editor.set_selection(i, i + 1) if i == len(self.value) and self.value != "" and delimiter(self.value[i - 1]): self._editor.set_selection(i - 1, i) a = _find(lambda (i, ch): delimiter(ch), enumerate(reversed(self.value[:i]))) b = _find(lambda (i, ch): delimiter(ch), enumerate(self.value[i:])) a = a and i - a[0] or 0 b = b and i + b[0] or len(self.value) self._editor.set_selection(a, b) def on_key_press(self, keys): if self._editing: self._editor.caret.visible = True i = self._editor.caret.position if keys.code == LEFT: # The left arrow moves the text cursor to the left. self._editor.caret.position = max(i - 1, 0) elif keys.code == RIGHT: # The right arrow moves the text cursor to the right. self._editor.caret.position = min(i + 1, len(self.value)) elif keys.code in (UP, DOWN): # The up arrows moves the text cursor to the previous line. # The down arrows moves the text cursor to the next line. y = keys.code == UP and -1 or +1 n = self._editor.get_line_count() i = self._editor.get_position_on_line( max(self._editor.get_line_from_position(i) + y, 0), self._editor.get_point_from_position(i)[0] ) self._editor.caret.position = i elif keys.code == TAB and TAB in self.reserved: # The tab key navigates away from the control. self._editor.caret.position = 0 self.editing = False elif keys.code == ENTER and ENTER in self.reserved: # The enter key executes on_action() and navigates away from the control. self._editor.caret.position = 0 self.editing = False self.on_action() elif keys.code == BACKSPACE and self.selected: # The backspace key removes the current text selection. self.value = self.value[: self.selection[0]] + self.value[self.selection[1] :] self._editor.caret.position = max(self.selection[0], 0) elif keys.code == BACKSPACE and i > 0: # The backspace key removes the character at the text cursor. self.value = self.value[: i - 1] + self.value[i:] self._editor.caret.position = max(i - 1, 0) elif keys.char: if self.selected: # Typing replaces any text currently selected. self.value = self.value[: self.selection[0]] + self.value[self.selection[1] :] self._editor.caret.position = i = max(self.selection[0], 0) ch = keys.char ch = ch.replace("\r", "\n\r") self.value = self.value[:i] + ch + self.value[i:] self._editor.caret.position = min(i + 1, len(self.value)) self._editor.set_selection(0, 0) def draw(self): self._editor.draw()
class Editable(Control): def __init__(self, value="", x=0, y=0, width=125, height=30, padding=(0,0), wrap=True, id=None, **kwargs): """ An editable text box. When clicked, it has the focus and can receive keyboard events. With wrap=True, several lines of text will wrap around the width. Optional parameters can include fill, font, fontsize, fontweight. """ txt = Text(value or " ", **{ "fill" : _popdefault(kwargs, "fill", Color(0,0.9)), "font" : _popdefault(kwargs, "font", theme["fontname"]), "fontsize" : _popdefault(kwargs, "fontsize", theme["fontsize"]), "fontweight" : _popdefault(kwargs, "fontweight", theme["fontweight"]), "lineheight" : _popdefault(kwargs, "lineheight", 1), "align" : LEFT }) kwargs["width"] = width kwargs["height"] = height Control.__init__(self, x=x, y=y, id=id, **kwargs) self.reserved = kwargs.get("reserved", [ENTER, TAB]) self._padding = padding self._i = 0 # Index of character on which the mouse is pressed. self._empty = value == "" and True or False self._editor = IncrementalTextLayout(txt._label.document, width, height, multiline=wrap) self._editor.content_valign = wrap and "top" or "center" self._editor.selection_background_color = (170, 200, 230, 255) self._editor.selection_color = txt._label.color self._editor.caret = Caret(self._editor) self._editor.caret.visible = False self._editing = False # When True, cursor is blinking and text can be edited. Editable._pack(self) # On init, call Editable._pack(), not the derived Field._pack(). def _pack(self): self._editor.x = self._padding[0] self._editor.y = self._padding[1] self._editor.width = max(0, self.width - self._padding[0] * 2) self._editor.height = max(0, self.height - self._padding[1] * 2) def _get_value(self): # IncrementalTextLayout in Pyglet 1.1.4 has a bug with empty strings. # We keep track of empty strings with Editable._empty to avoid this. return not self._empty and self._editor.document.text or u"" def _set_value(self, string): self._editor.begin_update() self._editor.document.text = string or " " self._editor.end_update() self._empty = string == "" and True or False value = property(_get_value, _set_value) def _get_editing(self): return self._editing def _set_editing(self, b): self._editing = b self._editor.caret.visible = b global EDITING if b is False and EDITING == self: EDITING = None if b is True: EDITING = self # Cursor is blinking and text can be edited. # Visit all layers on the canvas. # Remove the caret from all other Editable controls. for layer in (self.root.canvas and self.root.canvas.layers or []): layer.traverse(visit=lambda layer: \ isinstance(layer, Editable) and layer != self and \ setattr(layer, "editing", False)) editing = property(_get_editing, _set_editing) @property def selection(self): # Yields a (start, stop)-tuple with the indices of the current selected text. return (self._editor.selection_start, self._editor.selection_end) @property def selected(self): # Yields True when text is currently selected. return self.selection[0] != self.selection[1] @property def cursor(self): # Yields the index at the text cursor (caret). return self._editor.caret.position def index(self, x, y): """ Returns the index of the character in the text at position x, y. """ x0, y0 = self.absolute_position() i = self._editor.get_position_from_point(x-x0, y-y0) if self._editor.get_point_from_position(0)[0] > x-x0: # Pyglet bug? i = 0 if self._empty: i = 0 return i def on_mouse_enter(self, mouse): mouse.cursor = TEXT def on_mouse_press(self, mouse): i = self._i = self.index(mouse.x, mouse.y) self._editor.set_selection(0, 0) self.editing = True self._editor.caret.position = i Control.on_mouse_press(self, mouse) def on_mouse_release(self, mouse): if not self.dragged: self._editor.caret.position = self.index(mouse.x, mouse.y) Control.on_mouse_release(self, mouse) def on_mouse_drag(self, mouse): i = self.index(mouse.x, mouse.y) self._editor.selection_start = max(min(self._i, i), 0) self._editor.selection_end = min(max(self._i, i), len(self.value)) self._editor.caret.visible = False Control.on_mouse_drag(self, mouse) def on_mouse_doubleclick(self, mouse): # Select the word at the mouse position. # Words are delimited by non-alphanumeric characters. i = self.index(mouse.x, mouse.y) delimiter = lambda ch: not (ch.isalpha() or ch.isdigit()) if i < len(self.value) and delimiter(self.value[i]): self._editor.set_selection(i, i+1) if i == len(self.value) and self.value != "" and delimiter(self.value[i-1]): self._editor.set_selection(i-1, i) a = _find(lambda (i,ch): delimiter(ch), enumerate(reversed(self.value[:i]))) b = _find(lambda (i,ch): delimiter(ch), enumerate(self.value[i:])) a = a and i-a[0] or 0 b = b and i+b[0] or len(self.value) self._editor.set_selection(a, b) def on_key_press(self, key): if self._editing: self._editor.caret.visible = True i = self._editor.caret.position if key.code == LEFT: # The left arrow moves the text cursor to the left. self._editor.caret.position = max(i-1, 0) elif key.code == RIGHT: # The right arrow moves the text cursor to the right. self._editor.caret.position = min(i+1, len(self.value)) elif key.code in (UP, DOWN): # The up arrows moves the text cursor to the previous line. # The down arrows moves the text cursor to the next line. y = key.code == UP and -1 or +1 n = self._editor.get_line_count() i = self._editor.get_position_on_line( max(self._editor.get_line_from_position(i)+y, 0), self._editor.get_point_from_position(i)[0]) self._editor.caret.position = i elif key.code == TAB and TAB in self.reserved: # The tab key navigates away from the control. self._editor.caret.position = 0 self.editing = False elif key.code == ENTER and ENTER in self.reserved: # The enter key executes on_action() and navigates away from the control. self._editor.caret.position = 0 self.editing = False self.on_action() elif key.code == BACKSPACE and self.selected: # The backspace key removes the current text selection. self.value = self.value[:self.selection[0]] + self.value[self.selection[1]:] self._editor.caret.position = max(self.selection[0], 0) elif key.code == BACKSPACE and i > 0: # The backspace key removes the character at the text cursor. self.value = self.value[:i-1] + self.value[i:] self._editor.caret.position = max(i-1, 0) elif key.char: if self.selected: # Typing replaces any text currently selected. self.value = self.value[:self.selection[0]] + self.value[self.selection[1]:] self._editor.caret.position = i = max(self.selection[0], 0) ch = key.char ch = ch.replace("\r", "\n\r") self.value = self.value[:i] + ch + self.value[i:] self._editor.caret.position = min(i+1, len(self.value)) self._editor.set_selection(0, 0) def draw(self): self._editor.draw()