Example #1
0
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()
Example #2
0
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()