File: dropdownlist.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 (179 lines) | stat: -rw-r--r-- 6,423 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
"""This module defines a dropdown list widget"""
from asciimatics.event import KeyboardEvent, MouseEvent
from asciimatics.screen import Screen
from asciimatics.widgets.divider import Divider
from asciimatics.widgets.layout import Layout
from asciimatics.widgets.listbox import ListBox
from asciimatics.widgets.temppopup import _TempPopup
from asciimatics.widgets.text import Text
from asciimatics.widgets.utilities import _enforce_width
from asciimatics.widgets.widget import Widget


class _DropdownPopup(_TempPopup):
    """
    An internal Frame for selecting an item from a drop-down list..
    """

    def __init__(self, parent):
        """
        :param parent: The widget that spawned this pop-up.
        """
        # Decide which way to present the list - up or down from the parent widget.
        location = parent.get_location()
        if parent.frame.screen.height - location[1] < 3:
            height = min(len(parent.options) + 4, location[1] + 2)
            start_line = location[1] - height + 2
            reverse = True
        else:
            start_line = location[1] - 1
            height = min(len(parent.options) + 4, parent.frame.screen.height - location[1] + 1)
            reverse = False

        if parent.fit:
            width = min(max(map(lambda x: len(x[0]), parent.options)) + 4, parent.width)
        else:
            width = parent.width
        # Construct the Frame
        super().__init__(parent.frame.screen,
                         parent,
                         location[0], start_line,
                         width, height)

        # Build the widget to display the time selection.
        layout = Layout([1], fill_frame=True)
        self.add_layout(layout)
        self._field = Text()
        self._field.disabled = True
        divider = Divider()
        divider.disabled = True
        self._list = ListBox(Widget.FILL_FRAME,
                             [(f" {i[0]}", i[1]) for i in parent.options],
                             add_scroll_bar=len(parent.options) > height - 4,
                             on_select=self.close, on_change=self._link)
        layout.add_widget(self._list if reverse else self._field, 0)
        layout.add_widget(divider, 0)
        layout.add_widget(self._field if reverse else self._list, 0)
        self.fix()

        # Set up the correct time.
        self._list.value = parent.value

    def _link(self):
        # pylint: disable=protected-access
        self._field.value = self._list.options[self._list._line][0]

    def _on_close(self, cancelled):
        if not cancelled:
            self._parent.value = self._list.value


class DropdownList(Widget):
    """
    This widget allows you to pick an item from a temporary pop-up list.
    """

    __slots__ = ["_on_change", "_child", "_options", "_line", "_fit"]

    def __init__(self, options, label=None, name=None, on_change=None, fit=None, **kwargs):
        """
        :param options: The options for each row in the widget.
        :param label: An optional label for the widget.
        :param name: The name for the widget.
        :param on_change: Optional function to call when the selected time changes.
        :param fit: Shrink width of dropdown to fit the width of options. Default False.

        The `options` are a list of tuples, where the first value is the string to be displayed
        to the user and the second is an interval value to identify the entry to the program.
        For example:

            options=[("First option", 1), ("Second option", 2)]

        Also see the common keyword arguments in :py:obj:`.Widget`.
        """
        super().__init__(name, **kwargs)
        self._label = label
        self._on_change = on_change
        self._child = None
        self._options = options
        self._line = 0 if len(options) > 0 else None
        self._value = options[self._line][1] if self._line is not None else None
        self._fit = fit

    @property
    def options(self):
        """
        The set of allowed options for the drop-down list.
        """
        return self._options

    @options.setter
    def options(self, new_value):
        self._options = new_value
        self.value = self._value

    @property
    def fit(self):
        """
        Whether to shrink to largest element width or not.
        """
        return self._fit

    def update(self, frame_no):
        self._draw_label()

        # This widget only ever needs display the current selection - the separate Frame does all
        # the clever stuff when it has the focus.
        text = "" if self._line is None else self._options[self._line][0]
        (colour, attr, background) = self._pick_colours("field", selected=self._has_focus)
        if self._fit:
            width = min(max(map(lambda x: len(x[0]), self._options)) + 1, self.width - 3)
        else:
            width = self.width - 3
        self._frame.canvas.print_at(
            f"[ {_enforce_width(text, width, self._frame.canvas.unicode_aware):{width}}]",
            self._x + self._offset,
            self._y,
            colour, attr, background)

    def reset(self):
        pass

    def process_event(self, event):
        if event is not None:
            if isinstance(event, KeyboardEvent):
                if event.key_code in [Screen.ctrl("M"), Screen.ctrl("J"), ord(" ")]:
                    event = None
            elif isinstance(event, MouseEvent):
                if event.buttons != 0:
                    if self.is_mouse_over(event, include_label=False):
                        event = None
            if event is None:
                self._child = _DropdownPopup(self)
                self.frame.scene.add_effect(self._child)

        return event

    def required_height(self, offset, width):
        return 1

    @property
    def value(self):
        """
        The current value for this DropdownList.
        """
        return self._value

    @value.setter
    def value(self, new_value):
        # Only trigger change notification after we've changed selection
        old_value = self._value
        self._value = new_value
        for i, [_, value] in enumerate(self._options):
            if value == new_value:
                self._line = i
                break
        else:
            self._value = self._line = None
        if old_value != self._value and self._on_change:
            self._on_change()