simple_menu.py 9.64 KB
Newer Older
Rahix's avatar
Rahix committed
1
2
3
import buttons
import color
import display
Rahix's avatar
Rahix committed
4
5
import sys
import utime
Rahix's avatar
Rahix committed
6

7
8
TIMEOUT = 0x100
""":py:func:`~simple_menu.button_events` timeout marker."""
Rahix's avatar
Rahix committed
9

10
11

def button_events(timeout=None):
Rahix's avatar
Rahix committed
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    """
    Iterate over button presses (event-loop).

    This is just a helper function used internally by the menu.  But you can of
    course use it for your own scripts as well.  It works like this:

    .. code-block:: python

        import simple_menu, buttons

        for ev in simple_menu.button_events():
            if ev == buttons.BOTTOM_LEFT:
                # Left
                pass
            elif ev == buttons.BOTTOM_RIGHT:
                # Right
                pass
            elif ev == buttons.TOP_RIGHT:
                # Select
                pass
32
33

    .. versionadded:: 1.4
34
35
36
37
38
39
40

    :param float,optional timeout:
       Timeout after which the generator should yield in any case.  If a
       timeout is defined, the generator will periodically yield
       :py:data:`simple_menu.TIMEOUT`.

       .. versionadded:: 1.9
Rahix's avatar
Rahix committed
41
42
    """
    yield 0
43

Rahix's avatar
Rahix committed
44
45
    v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)
    button_pressed = True if v != 0 else False
46
47
48
49
50

    if timeout is not None:
        timeout = int(timeout * 1000)
        next_tick = utime.time_ms() + timeout

Rahix's avatar
Rahix committed
51
    while True:
52
53
54
55
56
57
        if timeout is not None:
            current_time = utime.time_ms()
            if current_time >= next_tick:
                next_tick += timeout
                yield TIMEOUT

Rahix's avatar
Rahix committed
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
        v = buttons.read(buttons.BOTTOM_LEFT | buttons.BOTTOM_RIGHT | buttons.TOP_RIGHT)

        if v == 0:
            button_pressed = False

        if not button_pressed and v & buttons.BOTTOM_LEFT != 0:
            button_pressed = True
            yield buttons.BOTTOM_LEFT

        if not button_pressed and v & buttons.BOTTOM_RIGHT != 0:
            button_pressed = True
            yield buttons.BOTTOM_RIGHT

        if not button_pressed and v & buttons.TOP_RIGHT != 0:
            button_pressed = True
            yield buttons.TOP_RIGHT


76
77
78
79
class _ExitMenuException(Exception):
    pass


Rahix's avatar
Rahix committed
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class Menu:
    """
    A simple menu for card10.

    This menu class is supposed to be inherited from to create a menu as shown
    in the example above.

    To instanciate the menu, pass a list of entries to the constructor:

    .. code-block:: python

        m = Menu(os.listdir("."))
        m.run()

    Then, call :py:meth:`~simple_menu.Menu.run` to start the event loop.
95
96

    .. versionadded:: 1.4
Rahix's avatar
Rahix committed
97
98
99
100
101
102
103
104
105
106
107
    """

    color_1 = color.CHAOSBLUE
    """Background color A."""
    color_2 = color.CHAOSBLUE_DARK
    """Background color B."""
    color_text = color.WHITE
    """Text color."""
    color_sel = color.COMMYELLOW
    """Color of the selector."""

108
109
110
111
112
113
114
    scroll_speed = 0.5
    """
    Time to wait before scrolling to the right.

    .. versionadded:: 1.9
    """

115
116
117
118
119
120
121
122
    timeout = None
    """
    Optional timeout for inactivity.  Once this timeout is reached,
    :py:meth:`~simple_menu.Menu.on_timeout` will be called.

    .. versionadded:: 1.9
    """

Rahix's avatar
Rahix committed
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
    def on_scroll(self, item, index):
        """
        Hook when the selector scrolls to a new item.

        This hook is run everytime a scroll-up or scroll-down is performed.
        Overwrite this function in your own menus if you want to do some action
        every time a new item is scrolled onto.

        :param item: The item which the selector now points to.
        :param int index: Index into the ``entries`` list of the ``item``.
        """
        pass

    def on_select(self, item, index):
        """
        Hook when an item as selected.

        The given ``index`` was selected with a SELECT button press.  Overwrite
        this function in your menu to perform an action on select.

        :param item: The item which was selected.
        :param int index: Index into the ``entries`` list of the ``item``.
        """
        pass

148
149
150
151
152
153
154
155
156
    def on_timeout(self):
        """
        The inactivity timeout has been triggered.  See
        :py:attr:`simple_menu.Menu.timeout`.

        .. versionadded:: 1.9
        """
        self.exit()

157
158
159
160
161
    def exit(self):
        """
        Exit the event-loop.  This should be called from inside an ``on_*`` hook.

        .. versionadded:: 1.9
Rahix's avatar
Rahix committed
162
163
164
        .. versionchanged:: 1.11

            Fixed this function not working properly.
165
166
167
        """
        raise _ExitMenuException()

Rahix's avatar
Rahix committed
168
169
170
171
172
173
    def __init__(self, entries):
        if len(entries) == 0:
            raise ValueError("at least one entry is required")

        self.entries = entries
        self.idx = 0
174
        self.select_time = utime.time_ms()
Rahix's avatar
Rahix committed
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
        self.disp = display.open()

    def entry2name(self, value):
        """
        Convert an entry object to a string representation.

        Overwrite this functio if your menu items are not plain strings.

        **Example**:

        .. code-block:: python

            class MyMenu(simple_menu.Menu):
                def entry2name(self, value):
                    return value[0]

            MyMenu(
                [("a", 123), ("b", 321)]
            ).run()
        """
        return str(value)

    def draw_entry(self, value, index, offset):
        """
        Draw a single entry.

        This is an internal function; you can override it for customized behavior.

        :param value: The value for this entry.  Use this to identify
            different entries.
        :param int index: A unique index per entry. Stable for a certain entry,
            but **not** an index into ``entries``.
        :param int offset: Y-offset for this entry.
        """
209
210
211
212
213
214
215
216
217
218
219
220
221
        string = self.entry2name(value)

        if offset != 20 or len(string) < 10:
            string = " " + string + " " * 9
        else:
            # Slowly scroll entry to the side
            time_offset = (utime.time_ms() - self.select_time) // int(
                self.scroll_speed * 1000
            )
            time_offset = time_offset % (len(string) - 7) - 1
            time_offset = min(len(string) - 10, max(0, time_offset))
            string = " " + string[time_offset:]

Rahix's avatar
Rahix committed
222
        self.disp.print(
223
            string,
Rahix's avatar
Rahix committed
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
            posy=offset,
            fg=self.color_text,
            bg=self.color_1 if index % 2 == 0 else self.color_2,
        )

    def draw_menu(self, offset=0):
        """
        Draw the menu.

        You'll probably never need to call this yourself; it is called
        automatially in the event loop (:py:meth:`~simple_menu.Menu.run`).
        """
        self.disp.clear()

        # Wrap around the list and draw entries from idx - 3 to idx + 4
        for y, i in enumerate(
            range(len(self.entries) + self.idx - 3, len(self.entries) + self.idx + 4)
        ):
            self.draw_entry(
                self.entries[i % len(self.entries)], i, offset + y * 20 - 40
            )

        self.disp.line(4, 22, 11, 29, col=self.color_sel, size=2)
Rahix's avatar
Rahix committed
247
        self.disp.line(4, 36, 11, 29, col=self.color_sel, size=2)
Rahix's avatar
Rahix committed
248
249
250

        self.disp.update()

Rahix's avatar
Rahix committed
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
    def error(self, line1, line2=""):
        """
        Display an error message.

        :param str line1: First line of the error message.
        :param str line2: Second line of the error message.

        .. versionadded:: 1.9
        """
        self.disp.clear(color.COMMYELLOW)

        offset = max(0, (160 - len(line1) * 14) // 2)
        self.disp.print(
            line1, posx=offset, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
        )

        offset = max(0, (160 - len(line2) * 14) // 2)
        self.disp.print(
            line2, posx=offset, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
        )

        self.disp.update()

Rahix's avatar
Rahix committed
274
275
    def run(self):
        """Start the event-loop."""
276
        try:
277
278
279
280
281
            timeout = self.scroll_speed
            if self.timeout is not None and self.timeout < self.scroll_speed:
                timeout = self.timeout

            for ev in button_events(timeout):
282
283
284
285
286
287
                if ev == buttons.BOTTOM_RIGHT:
                    self.select_time = utime.time_ms()
                    self.draw_menu(-8)
                    self.idx = (self.idx + 1) % len(self.entries)
                    try:
                        self.on_scroll(self.entries[self.idx], self.idx)
288
289
                    except _ExitMenuException:
                        raise
290
291
292
293
294
295
296
297
298
                    except Exception as e:
                        print("Exception during menu.on_scroll():")
                        sys.print_exception(e)
                elif ev == buttons.BOTTOM_LEFT:
                    self.select_time = utime.time_ms()
                    self.draw_menu(8)
                    self.idx = (self.idx + len(self.entries) - 1) % len(self.entries)
                    try:
                        self.on_scroll(self.entries[self.idx], self.idx)
299
300
                    except _ExitMenuException:
                        raise
301
302
303
304
305
306
307
                    except Exception as e:
                        print("Exception during menu.on_scroll():")
                        sys.print_exception(e)
                elif ev == buttons.TOP_RIGHT:
                    try:
                        self.on_select(self.entries[self.idx], self.idx)
                        self.select_time = utime.time_ms()
308
309
                    except _ExitMenuException:
                        raise
310
311
312
313
314
315
316
                    except Exception as e:
                        print("Menu crashed!")
                        sys.print_exception(e)
                        self.error("Menu", "crashed")
                        utime.sleep(1.0)

                self.draw_menu()
317
318
319
320
321

                if self.timeout is not None and (
                    utime.time_ms() - self.select_time
                ) > int(self.timeout * 1000):
                    self.on_timeout()
322
323
        except _ExitMenuException:
            pass