File: widget.py

package info (click to toggle)
python-asciimatics 1.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,488 kB
  • sloc: python: 15,713; sh: 8; makefile: 2
file content (336 lines) | stat: -rw-r--r-- 11,332 bytes parent folder | download
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
"""
This module allows you to create interactive text user interfaces.  For more details see
http://asciimatics.readthedocs.io/en/latest/widgets.html
"""
from abc import ABCMeta, abstractmethod

from logging import getLogger
from wcwidth import wcswidth
from asciimatics.screen import Screen
from asciimatics.widgets.utilities import _split_text

# Logging
logger = getLogger(__name__)


class Widget(metaclass=ABCMeta):
    """
    A Widget is a re-usable component that can be used to create a simple GUI.
    """

    #: Widgets with this constant for the required height will be re-sized to
    #: fit the available vertical space in the Layout.
    FILL_FRAME = -135792468

    #: Widgets with this constant for the required height will be re-sized to
    #: fit the maximum space used by any other column in the Layout.
    FILL_COLUMN = -135792467

    __slots__ = ["_name", "_label", "_frame", "_value", "_has_focus", "_x",
                 "_y", "_h", "_w", "_offset", "_display_label", "_is_tab_stop",
                 "_is_disabled", "_is_valid", "_custom_colour", "_on_focus",
                 "_on_blur", "string_len", "_readonly"]

    def __init__(self, name, tab_stop=True, disabled=False, on_focus=None, on_blur=None):
        """
        :param name: The name of this Widget.
        :param tab_stop: Whether this widget should take focus or not when tabbing around the Frame.
        :param disabled: Whether this Widget should be disabled or not.
        :param on_focus: Optional callback whenever this widget gets the focus.
        :param on_blur: Optional callback whenever this widget loses the focus.
        """
        super().__init__()
        # Internal properties
        self._name = name
        self._label = None
        self._frame = None
        self._value = None
        self._has_focus = False
        self._x = self._y = 0
        self._w = self._h = 0
        self._offset = 0
        self._display_label = None
        self._is_tab_stop = tab_stop
        self._is_disabled = disabled
        self._is_valid = True
        self._custom_colour = None
        self._on_focus = on_focus
        self._on_blur = on_blur
        self._readonly = False

        # Helper function to optimise string length calculations - default for now and pick
        # the optimal version when we know whether we need unicode support or not.
        self.string_len = wcswidth

    @property
    def frame(self):
        """
        The Frame that contains this Widget.
        """
        return self._frame

    @property
    def is_valid(self):
        """
        Whether this widget has passed its data validation or not.
        """
        return self._is_valid

    @property
    def is_tab_stop(self):
        """
        Whether this widget is a valid tab stop for keyboard navigation.
        """
        return self._is_tab_stop

    @property
    def is_visible(self):
        """
        Whether this widget is visible on the Canvas or not.
        """
        return not (self._y + self._h <= self._frame.canvas.start_line or
                    self._y >= self._frame.canvas.start_line + self._frame.canvas.height)

    @property
    def disabled(self):
        """
        Whether this widget is disabled or not.
        """
        return self._is_disabled

    @disabled.setter
    def disabled(self, new_value):
        self._is_disabled = new_value

    @property
    def custom_colour(self):
        """
        A custom colour to use instead of the normal calculated one when drawing this widget.

        This must be a key name from the palette dictionary.
        """
        return self._custom_colour

    @custom_colour.setter
    def custom_colour(self, new_value):
        self._custom_colour = new_value

    @property
    def frame_update_count(self):
        """
        The number of frames before this Widget should be updated.
        """
        return 0

    @property
    def width(self):
        """
        The width of this Widget (excluding any labels).

        Only valid after the Frame has been fixed in place.
        """
        return self._w - self._offset

    def register_frame(self, frame):
        """
        Register the Frame that owns this Widget.

        :param frame: The owning Frame.
        """
        self._frame = frame
        self.string_len = wcswidth if self._frame.canvas.unicode_aware else len

    def set_layout(self, x, y, offset, w, h):
        """
        Set the size and position of the Widget.

        This should not be called directly.  It is used by the :py:obj:`.Layout` class to arrange
        all widgets within the Frame.

        :param x: The x position of the widget.
        :param y: The y position of the widget.
        :param offset: The allowed label size for the widget.
        :param w: The width of the widget.
        :param h: The height of the widget.
        """
        self._x = x
        self._y = y
        self._offset = offset
        self._w = w
        self._h = h

    def get_location(self):
        """
        Return the absolute location of this widget on the Screen, taking into account the
        current state of the Frame that is displaying it and any label offsets of the Widget.

        :returns: A tuple of the form (<X coordinate>, <Y coordinate>).
        """
        origin = self._frame.canvas.origin
        return (self._x + origin[0] + self._offset,
                self._y + origin[1] - self._frame.canvas.start_line)

    def focus(self):
        """
        Call this to give this Widget the input focus.
        """
        logger.debug("Widget focus: %s", self)
        self._has_focus = True
        self._frame.move_to(self._x, self._y, self._h)
        if self._on_focus is not None:
            self._on_focus()

    def is_mouse_over(self, event, include_label=True, width_modifier=0):
        """
        Check if the specified mouse event is over this widget.

        :param event: The MouseEvent to check.
        :param include_label: Include space reserved for the label when checking.
        :param width_modifier: Adjustment to width (e.g. for scroll bars).
        :returns: True if the mouse is over the active parts of the widget.
        """
        # Disabled widgets should not react to the mouse.
        logger.debug("Widget: %s (%d, %d) (%d, %d)", self, self._x, self._y, self._w, self._h)
        if self._is_disabled:
            return False

        # Check this part of the canvas is visible - can't be clicked if not visible.
        if (event.y < self._frame.canvas.start_line or
                event.y >= self._frame.canvas.start_line + self._frame.canvas.height):
            return False

        # Check for any overlap
        if self._y <= event.y < self._y + self._h:
            if ((include_label and self._x <= event.x < self._x + self._w - width_modifier) or
                    (self._x + self._offset <= event.x < self._x + self._w - width_modifier)):
                return True

        return False

    def blur(self):
        """
        Call this to take the input focus from this Widget.
        """
        logger.debug("Widget blur: %s", self)
        self._has_focus = False
        if self._on_blur is not None:
            self._on_blur()

    def _draw_label(self):
        """
        Draw the label for this widget if needed.
        """
        if self._label is not None:
            # Break the label up as required.
            if self._display_label is None:
                # noinspection PyTypeChecker
                self._display_label = _split_text(
                    self._label, self._offset, self._h, self._frame.canvas.unicode_aware)

            # Draw the  display label.
            (colour, attr, background) = self._frame.palette["label"]
            for i, text in enumerate(self._display_label):
                self._frame.canvas.paint(
                    text, self._x, self._y + i, colour, attr, background)

    def _draw_cursor(self, char, frame_no, x, y):
        """
        Draw a flashing cursor for this widget.

        :param char: The character to use for the cursor (when not a block)
        :param frame_no: The current frame number.
        :param x: The x coordinate for the cursor.
        :param y: The y coordinate for the cursor.
        """
        (colour, attr, background) = self._pick_colours("readonly" if self._readonly else "edit_text")
        if frame_no % 10 < 5 or self._frame.reduce_cpu:
            attr |= Screen.A_REVERSE
        self._frame.canvas.print_at(char, x, y, colour, attr, background)

    def _pick_palette_key(self, palette_name, selected=False, allow_input_state=True):
        """
        Pick the rendering colour for a widget based on the current state.

        :param palette_name: The stem name for the widget - e.g. "button".
        :param selected: Whether this item is selected or not.
        :param allow_input_state: Whether to allow input state (e.g. focus) to affect result.
        :returns: A colour palette key to be used.
        """
        key = palette_name
        if self._custom_colour:
            key = self._custom_colour
        elif self.disabled:
            key = "disabled"
        elif not self._is_valid:
            key = "invalid"
        elif allow_input_state:
            if self._has_focus:
                key = "focus_" + palette_name
            if selected:
                key = "selected_" + key
        return key

    def _pick_colours(self, palette_name, selected=False):
        """
        Pick the rendering colour for a widget based on the current state.

        :param palette_name: The stem name for the widget - e.g. "button".
        :param selected: Whether this item is selected or not.
        :returns: A colour tuple (fg, attr, background) to be used.
        """
        return self._frame.palette[self._pick_palette_key(palette_name, selected)]

    @abstractmethod
    def update(self, frame_no):
        """
        The update method is called whenever this widget needs to redraw itself.

        :param frame_no: The frame number for this screen update.
        """

    @abstractmethod
    def reset(self):
        """
        The reset method is called whenever the widget needs to go back to its
        default (initially created) state.
        """

    @abstractmethod
    def process_event(self, event):
        """
        Process any input event.

        :param event: The event that was triggered.
        :returns: None if the Effect processed the event, else the original event.
        """

    @property
    def label(self):
        """
        The label for this widget.  Can be `None`.
        """
        return self._label

    @property
    def name(self):
        """
        The name for this widget (for reference in the persistent data).  Can
        be `None`.
        """
        return self._name

    @property
    @abstractmethod
    def value(self):
        """
        The value to return for this widget based on the user's input.
        """

    @abstractmethod
    def required_height(self, offset, width):
        """
        Calculate the minimum required height for this widget.

        :param offset: The allowed width for any labels.
        :param width: The total width of the widget, including labels.
        """