gaupol/entries.py

Source code for module gaupol.entries from file gaupol/entries.py.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# -*- coding: utf-8 -*-

# Copyright (C) 2005 Osmo Salomaa
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Entry for time data in format ``[-]HH:MM:SS.SSS``."""

import aeidon
import functools
import gaupol
import re

from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import Gtk

__all__ = ("TimeEntry",)


def _blocked(function):
    """Decorator for methods to be run blocked to avoid recursion."""
    @functools.wraps(function)
    def wrapper(entry, *args, **kwargs):
        entry.handler_block(entry._delete_handler)
        entry.handler_block(entry._insert_handler)
        value = function(entry, *args, **kwargs)
        entry.handler_unblock(entry._insert_handler)
        entry.handler_unblock(entry._delete_handler)
        return value
    return wrapper


class TimeEntry(Gtk.Entry):

    """
    Entry for time data in format ``[-]HH:MM:SS.SSS``.

    :ivar _delete_handler: Handler for "delete-text" signal
    :ivar _insert_handler: Handler for "insert-text" signal

    This widget uses :func:`GLib.idle_add` a lot, which means that clients may
    need to call :func:`Gtk.main_iteration` to ensure proper updating.
    """
    _re_digit = re.compile(r"\d")
    _re_time = re.compile(r"^-?\d\d:[0-5]\d:[0-5]\d\.\d\d\d$")

    def __init__(self):
        """Initialize a :class:`TimeEntry` instance."""
        GObject.GObject.__init__(self)
        self._delete_handler = None
        self._insert_handler = None
        self.set_width_chars(13)
        self.set_max_length(13)
        self._init_signal_handlers()

    def _init_signal_handlers(self):
        """Initialize signal handlers."""
        aeidon.util.connect(self, self, "cut-clipboard")
        aeidon.util.connect(self, self, "key-press-event")
        aeidon.util.connect(self, self, "toggle-overwrite")
        self._delete_handler = aeidon.util.connect(self, self, "delete-text")
        self._insert_handler = aeidon.util.connect(self, self, "insert-text")

    @_blocked
    def _insert_text(self, value):
        """Insert `value` as text after validation."""
        pos = self.get_position()
        text = self.get_text()
        if pos == 0 and value.startswith("-"):
            text = (text if text.startswith("-") else "-{}".format(text))
        length = len(value)
        text = text[:pos] + value + text[pos+length:]
        text = text.replace(",", ".")
        if not self._re_time.match(text): return
        self.set_text(text)
        self.set_position(pos)
        if length != 1: return
        self.set_position(pos+1)
        if len(text) > pos+1 and text[pos+1] in (":", "."):
            self.set_position(pos+2)

    def _on_cut_clipboard(self, entry):
        """Change "cut-clipboard" signal to "copy-clipboard"."""
        self.stop_emission("cut-clipboard")
        self.emit("copy-clipboard")

    def _on_delete_text(self, entry, start_pos, end_pos):
        """Do not allow deleting text."""
        self.stop_emission("delete-text")
        self.set_position(start_pos)

    def _on_key_press_event(self, entry, event):
        """Change numbers to zero if Backspace or Delete pressed."""
        keys = (Gdk.KEY_BackSpace, Gdk.KEY_Delete)
        if not event.keyval in keys: return
        self.stop_emission("key-press-event")
        if self.get_selection_bounds():
            gaupol.util.idle_add(self._zero_selection)
        elif event.keyval == Gdk.KEY_BackSpace:
            gaupol.util.idle_add(self._zero_previous)
        elif event.keyval == Gdk.KEY_Delete:
            gaupol.util.idle_add(self._zero_next)

    def _on_insert_text(self, entry, text, length, pos):
        """Insert `text` after validation."""
        self.stop_emission("insert-text")
        gaupol.util.idle_add(self._insert_text, text)

    def _on_toggle_overwrite(self, entry):
        """Do not allow toggling overwrite."""
        self.stop_emission("toggle-overwrite")

    @_blocked
    def _zero_next(self):
        """Change the next digit to zero."""
        pos = self.get_position()
        text = self.get_text()
        if pos >= len(text): return
        if pos == 0 and text.startswith("-"):
            self.set_text(text[1:])
            return self.set_position(0)
        if not text[pos].isdigit(): return
        self.set_text(text[:pos] + "0" + text[pos+1:])
        self.set_position(pos)

    @_blocked
    def _zero_previous(self):
        """Change the previous digit to zero."""
        pos = self.get_position()
        text = self.get_text()
        if pos <= 0: return
        if pos == 1 and text.startswith("-"):
            self.set_text(text[1:])
            return self.set_position(0)
        if not text[pos-1].isdigit():
            return self.set_position(pos-1)
        self.set_text(text[:pos-1] + "0" + text[pos:])
        self.set_position(pos-1)

    @_blocked
    def _zero_selection(self):
        """Change digits in selection to zero."""
        if not self.get_selection_bounds(): return
        a, z = self.get_selection_bounds()
        text = self.get_text()
        zero = self._re_digit.sub("0", text[a:z])
        self.set_text(text[:a] + zero + text[z:])
        self.set_position(a)