flexablelib-components 3.3.1__tar.gz → 3.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flexablelib_components-3.3.1/flexablelib_components.egg-info → flexablelib_components-3.5.0}/PKG-INFO +3 -3
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/__init__.py +5 -3
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/base.py +239 -10
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/data.py +226 -17
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/dates.py +143 -3
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/display.py +220 -3
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/inputs.py +196 -10
- flexablelib_components-3.5.0/flexablelib_components/media.py +531 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/navigation.py +237 -24
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/overlays.py +29 -1
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0/flexablelib_components.egg-info}/PKG-INFO +3 -3
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components.egg-info/SOURCES.txt +1 -0
- flexablelib_components-3.5.0/flexablelib_components.egg-info/requires.txt +2 -0
- flexablelib_components-3.3.1/flexablelib_components.egg-info/requires.txt +0 -2
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/LICENSE +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/MANIFEST.in +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/README.md +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/py.typed +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components.egg-info/dependency_links.txt +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components.egg-info/top_level.txt +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/setup.cfg +0 -0
- {flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flexablelib-components
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Interactive terminal components for flexablelib: inputs, dates, tables, forms, menus, tabs, dialogs and more.
|
|
5
5
|
Home-page: https://github.com/Opus-Perpetuus/flexablelib
|
|
6
6
|
Author: Benjamin Ramirez
|
|
@@ -17,8 +17,8 @@ Classifier: Topic :: Software Development :: User Interfaces
|
|
|
17
17
|
Requires-Python: >=3.8
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
-
Requires-Dist: flexablelib>=3.
|
|
21
|
-
Requires-Dist: flexablelib-tui>=3.
|
|
20
|
+
Requires-Dist: flexablelib>=3.5.0
|
|
21
|
+
Requires-Dist: flexablelib-tui>=3.5.0
|
|
22
22
|
Dynamic: author
|
|
23
23
|
Dynamic: author-email
|
|
24
24
|
Dynamic: classifier
|
{flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/__init__.py
RENAMED
|
@@ -9,14 +9,15 @@ pieces (badges, alerts, links, breadcrumbs, code, Markdown/HTML, timelines,
|
|
|
9
9
|
steppers, comments, stats, charts), navigation (tabs, menus, pagination,
|
|
10
10
|
sidebar, wizard, accordion, tree, command palette, split panes, app shell),
|
|
11
11
|
data (interactive and editable tables, filter bar, catalog, forms, login)
|
|
12
|
-
|
|
12
|
+
dialogs (modal, confirm, prompt, loading, popover) and pictures (image,
|
|
13
|
+
image viewer: kitty, iTerm2, sixel, Unicode blocks, ASCII, link).
|
|
13
14
|
|
|
14
15
|
Every component works inside :class:`flexablelib_tui.App` (focus with Tab,
|
|
15
16
|
keys to the focused component) and also renders on its own with
|
|
16
17
|
``render(width)``. See docs/COMPONENTS.md.
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
__version__ = '3.
|
|
20
|
+
__version__ = '3.5.0'
|
|
20
21
|
|
|
21
22
|
from .base import Theme, DEFAULT_THEME, Input, Box, Button, frame
|
|
22
23
|
from .inputs import (TextInput, PasswordInput, NumberInput, ExpressionInput,
|
|
@@ -38,6 +39,7 @@ from .navigation import (Tabs, Menu, ContextMenu, MenuButton, Pagination,
|
|
|
38
39
|
AppShell, SortControl)
|
|
39
40
|
from .data import (DataTable, FilterBar, EditableTable, CatalogGrid, Form,
|
|
40
41
|
ValidationSummary, LoginForm, FilePreview, parse_query)
|
|
42
|
+
from .media import Image, ImageViewer, IMAGE_EXTENSIONS
|
|
41
43
|
from .overlays import (Modal, ConfirmDialog, PromptDialog, LoadingOverlay,
|
|
42
44
|
Popover, HelpLine)
|
|
43
45
|
|
|
@@ -60,5 +62,5 @@ __all__ = [
|
|
|
60
62
|
'DataTable', 'FilterBar', 'EditableTable', 'CatalogGrid', 'Form',
|
|
61
63
|
'ValidationSummary', 'LoginForm', 'FilePreview', 'parse_query',
|
|
62
64
|
'Modal', 'ConfirmDialog', 'PromptDialog', 'LoadingOverlay', 'Popover',
|
|
63
|
-
'HelpLine', '__version__',
|
|
65
|
+
'HelpLine', 'Image', 'ImageViewer', 'IMAGE_EXTENSIONS', '__version__',
|
|
64
66
|
]
|
{flexablelib_components-3.3.1 → flexablelib_components-3.5.0}/flexablelib_components/base.py
RENAMED
|
@@ -7,8 +7,8 @@ from flexablelib import _frames as _fx
|
|
|
7
7
|
from flexablelib.colors import RESET, parse_spec
|
|
8
8
|
from flexablelib_tui import Component
|
|
9
9
|
|
|
10
|
-
__all__ = ['Theme', 'DEFAULT_THEME', '
|
|
11
|
-
'Box', 'Button']
|
|
10
|
+
__all__ = ['Theme', 'DEFAULT_THEME', 'THEMES', 'Input', 'frame',
|
|
11
|
+
'join_columns', 'Box', 'Button']
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Theme(object):
|
|
@@ -19,12 +19,25 @@ class Theme(object):
|
|
|
19
19
|
``selection`` the highlighted row of a list or table, and ``info`` /
|
|
20
20
|
``success`` / ``warning`` / ``error`` the message kinds, ``field`` the
|
|
21
21
|
background of text fields.
|
|
22
|
+
|
|
23
|
+
``background`` / ``foreground`` are the terminal colours the theme is
|
|
24
|
+
meant for (used by contrast checks), ``marker=True`` shows focus and
|
|
25
|
+
selection with text markers in colour too. Ready-made themes:
|
|
26
|
+
``Theme.preset(name)`` with ``'default'``, ``'high-contrast'``,
|
|
27
|
+
``'high-contrast-light'``, ``'deuteranopia'``, ``'protanopia'``,
|
|
28
|
+
``'tritanopia'`` and ``'monochrome'`` (see :data:`THEMES`).
|
|
22
29
|
"""
|
|
23
30
|
|
|
24
31
|
def __init__(self, accent='cyan', muted='bright_black', text=None,
|
|
25
|
-
selection='black on cyan', border='bright_black',
|
|
32
|
+
selection='bold black on cyan', border='bright_black',
|
|
26
33
|
info='blue', success='green', warning='yellow',
|
|
27
|
-
error='red', title='bold', field='on #262626'
|
|
34
|
+
error='red', title='bold', field='on #262626',
|
|
35
|
+
background='#000000', foreground='#e5e5e5', marker=False,
|
|
36
|
+
name='custom'):
|
|
37
|
+
self.name = name
|
|
38
|
+
self.background = background
|
|
39
|
+
self.foreground = foreground
|
|
40
|
+
self.marker = bool(marker)
|
|
28
41
|
self.field = field
|
|
29
42
|
self.accent = accent
|
|
30
43
|
self.muted = muted
|
|
@@ -41,8 +54,85 @@ class Theme(object):
|
|
|
41
54
|
"""The colour for a message kind ('info', 'success', ...)."""
|
|
42
55
|
return getattr(self, name, None) or self.info
|
|
43
56
|
|
|
57
|
+
ROLES = ('accent', 'muted', 'text', 'selection', 'border', 'info',
|
|
58
|
+
'success', 'warning', 'error', 'title', 'field')
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def preset(cls, name):
|
|
62
|
+
"""A copy of one of :data:`THEMES` (``KeyError`` lists the names)."""
|
|
63
|
+
try:
|
|
64
|
+
values = THEMES[name]
|
|
65
|
+
except KeyError:
|
|
66
|
+
raise KeyError('unknown theme {0!r}; choose from {1}'.format(
|
|
67
|
+
name, ', '.join(sorted(THEMES))))
|
|
68
|
+
return cls(name=name, **values)
|
|
69
|
+
|
|
70
|
+
def contrast(self):
|
|
71
|
+
"""``{role: contrast ratio}`` of each role's text against its
|
|
72
|
+
background (the theme's ``background`` when the role sets none)."""
|
|
73
|
+
from flexablelib_tui.accessibility import spec_colors, contrast_ratio
|
|
74
|
+
out = {}
|
|
75
|
+
for role in self.ROLES:
|
|
76
|
+
spec = getattr(self, role)
|
|
77
|
+
fg, bg, attrs = spec_colors(spec)
|
|
78
|
+
if fg is None and bg is None:
|
|
79
|
+
continue
|
|
80
|
+
if 'reverse' in attrs:
|
|
81
|
+
fg, bg = bg, fg
|
|
82
|
+
fg = fg or self.foreground
|
|
83
|
+
bg = bg or self.background
|
|
84
|
+
out[role] = round(contrast_ratio(fg, bg), 2)
|
|
85
|
+
return out
|
|
44
86
|
|
|
45
|
-
|
|
87
|
+
def __repr__(self):
|
|
88
|
+
return 'Theme({0!r})'.format(self.name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
#: The ready-made themes (``Theme.preset(name)``). The high-contrast ones
|
|
92
|
+
#: reach WCAG AAA (7:1) for text; the colour-blind ones keep every message
|
|
93
|
+
#: kind distinct under that deficiency (checked with the Machado 2009
|
|
94
|
+
#: simulation) at 7:1 or more on a dark background, and all of them show
|
|
95
|
+
#: focus with text markers as well as colour.
|
|
96
|
+
THEMES = {
|
|
97
|
+
'default': {},
|
|
98
|
+
'high-contrast': dict(
|
|
99
|
+
accent='#ffff00', muted='#d0d0d0', text='#ffffff',
|
|
100
|
+
selection='bold black on #ffff00', border='#ffffff', info='#00ffff',
|
|
101
|
+
success='#00ff00', warning='#ffff00', error='#ff6060',
|
|
102
|
+
title='bold #ffffff', field='#ffffff on #1c1c1c',
|
|
103
|
+
background='#000000', foreground='#ffffff', marker=True),
|
|
104
|
+
'high-contrast-light': dict(
|
|
105
|
+
accent='#0000c0', muted='#404040', text='#000000',
|
|
106
|
+
selection='bold #ffffff on #0000c0', border='#000000', info='#00008b',
|
|
107
|
+
success='#005a00', warning='#6b4000', error='#b00000',
|
|
108
|
+
title='bold #000000', field='#000000 on #ececec',
|
|
109
|
+
background='#ffffff', foreground='#000000', marker=True),
|
|
110
|
+
'deuteranopia': dict(
|
|
111
|
+
accent='#56b4e9', muted='#c0c0c0', text='#ffffff',
|
|
112
|
+
selection='bold black on #56b4e9', border='#a0a0a0', info='#ffffff',
|
|
113
|
+
success='#56b4e9', warning='#ffb000', error='#ff6e6e',
|
|
114
|
+
title='bold #ffffff', field='#ffffff on #1c1c1c',
|
|
115
|
+
background='#000000', foreground='#ffffff', marker=True),
|
|
116
|
+
'protanopia': dict(
|
|
117
|
+
accent='#4da6ff', muted='#c0c0c0', text='#ffffff',
|
|
118
|
+
selection='bold black on #4da6ff', border='#a0a0a0', info='#ffffff',
|
|
119
|
+
success='#4da6ff', warning='#f0e442', error='#ff6e6e',
|
|
120
|
+
title='bold #ffffff', field='#ffffff on #1c1c1c',
|
|
121
|
+
background='#000000', foreground='#ffffff', marker=True),
|
|
122
|
+
'tritanopia': dict(
|
|
123
|
+
accent='#00e0ff', muted='#c0c0c0', text='#ffffff',
|
|
124
|
+
selection='bold black on #00e0ff', border='#a0a0a0', info='#a0a0ff',
|
|
125
|
+
success='#00e0ff', warning='#f0e442', error='#ff6e6e',
|
|
126
|
+
title='bold #ffffff', field='#ffffff on #1c1c1c',
|
|
127
|
+
background='#000000', foreground='#ffffff', marker=True),
|
|
128
|
+
'monochrome': dict(
|
|
129
|
+
accent='bold', muted='dim', text=None, selection='bold reverse',
|
|
130
|
+
border=None, info='bold', success='bold', warning='bold underline',
|
|
131
|
+
error='bold underline', title='bold', field='underline',
|
|
132
|
+
background='#000000', foreground='#e5e5e5', marker=True),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
DEFAULT_THEME = Theme(name='default')
|
|
46
136
|
|
|
47
137
|
_SPEC_CACHE = {}
|
|
48
138
|
|
|
@@ -55,12 +145,91 @@ _BOXES = {
|
|
|
55
145
|
}
|
|
56
146
|
|
|
57
147
|
|
|
148
|
+
_PRESETS = {}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def active_theme():
|
|
152
|
+
"""The theme for components that did not choose one: the accessibility
|
|
153
|
+
setting ``theme`` (``App(theme=...)`` or ``FLEXABLELIB_THEME``) or
|
|
154
|
+
:data:`DEFAULT_THEME`."""
|
|
155
|
+
from flexablelib_tui import accessibility as _a11y
|
|
156
|
+
choice = _a11y.settings.theme
|
|
157
|
+
if not choice:
|
|
158
|
+
return DEFAULT_THEME
|
|
159
|
+
if isinstance(choice, Theme):
|
|
160
|
+
return choice
|
|
161
|
+
theme = _PRESETS.get(choice)
|
|
162
|
+
if theme is None:
|
|
163
|
+
try:
|
|
164
|
+
theme = Theme.preset(choice)
|
|
165
|
+
except KeyError:
|
|
166
|
+
theme = DEFAULT_THEME
|
|
167
|
+
_PRESETS[choice] = theme
|
|
168
|
+
return theme
|
|
169
|
+
|
|
170
|
+
|
|
58
171
|
class _Themed(Component):
|
|
59
172
|
"""A component with a theme and cached colour specs."""
|
|
60
173
|
|
|
61
174
|
def __init__(self, theme=None, **kwargs):
|
|
62
175
|
super(_Themed, self).__init__(**kwargs)
|
|
63
|
-
self.theme = theme
|
|
176
|
+
self.theme = theme
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def theme(self):
|
|
180
|
+
"""The component's own theme, else the one the app (``App(theme=)``)
|
|
181
|
+
or ``FLEXABLELIB_THEME`` sets, else :data:`DEFAULT_THEME`."""
|
|
182
|
+
if self._theme is not None:
|
|
183
|
+
return self._theme
|
|
184
|
+
return active_theme()
|
|
185
|
+
|
|
186
|
+
@theme.setter
|
|
187
|
+
def theme(self, value):
|
|
188
|
+
if isinstance(value, str):
|
|
189
|
+
value = Theme.preset(value)
|
|
190
|
+
self._theme = value
|
|
191
|
+
|
|
192
|
+
def _marked(self):
|
|
193
|
+
return Component._marked(self) or getattr(self.theme, 'marker', False)
|
|
194
|
+
|
|
195
|
+
# -- mouse: clickable spots registered while drawing --------------------
|
|
196
|
+
|
|
197
|
+
_hot = ()
|
|
198
|
+
_hot_base = 0
|
|
199
|
+
|
|
200
|
+
def render(self, width=None, height=None):
|
|
201
|
+
self._hot = []
|
|
202
|
+
self._hot_base = 0
|
|
203
|
+
return Component.render(self, width, height)
|
|
204
|
+
|
|
205
|
+
def hot(self, row, action, x0=0, x1=None):
|
|
206
|
+
"""Make columns ``[x0, x1)`` of output row ``row`` clickable: a left
|
|
207
|
+
press there calls ``action()`` (``False`` from it means unused)."""
|
|
208
|
+
if isinstance(self._hot, tuple):
|
|
209
|
+
self._hot = []
|
|
210
|
+
self._hot.append((self._hot_base + row, x0, x1, action))
|
|
211
|
+
|
|
212
|
+
def hot_parts(self, row, parts, gap=1, start=0, actions=()):
|
|
213
|
+
"""Register ``actions[i]`` for the columns ``parts[i]`` take on
|
|
214
|
+
``row`` when the parts are joined with ``gap`` columns between them
|
|
215
|
+
(``None`` actions are skipped)."""
|
|
216
|
+
x = start
|
|
217
|
+
for part, action in zip(parts, actions):
|
|
218
|
+
w = _fx.visible_width(_fx.strip_ansi(part))
|
|
219
|
+
if action is not None:
|
|
220
|
+
self.hot(row, action, x, x + w)
|
|
221
|
+
x += w + gap
|
|
222
|
+
|
|
223
|
+
def on_mouse(self, event):
|
|
224
|
+
if event.action != 'press' or event.button != 0:
|
|
225
|
+
return False
|
|
226
|
+
for row, x0, x1, action in reversed(self._hot):
|
|
227
|
+
if row == event.y and event.x >= x0 and \
|
|
228
|
+
(x1 is None or event.x < x1):
|
|
229
|
+
result = action()
|
|
230
|
+
self.refresh()
|
|
231
|
+
return result is not False
|
|
232
|
+
return False
|
|
64
233
|
|
|
65
234
|
def paint(self, text, spec):
|
|
66
235
|
if not text or not spec or not self._active():
|
|
@@ -72,6 +241,17 @@ class _Themed(Component):
|
|
|
72
241
|
return text
|
|
73
242
|
return prefix + text.replace(RESET, RESET + prefix) + RESET
|
|
74
243
|
|
|
244
|
+
def mark_current(self, text, spec=None):
|
|
245
|
+
"""A highlighted row (the current item of a list): the selection
|
|
246
|
+
colour, and a ``▸`` marker (``>`` without colour) when focus markers
|
|
247
|
+
are on, so it never depends on colour alone."""
|
|
248
|
+
if self._marked() and text:
|
|
249
|
+
text = ('▸' if self._active() else '>') + \
|
|
250
|
+
_fx.slice_columns(text, 1, max(0, _fx.visible_width(text) - 1))
|
|
251
|
+
if self._active():
|
|
252
|
+
text = self.paint(text, spec or self.theme.selection)
|
|
253
|
+
return text
|
|
254
|
+
|
|
75
255
|
def cursor(self, char):
|
|
76
256
|
"""``char`` drawn as the text cursor."""
|
|
77
257
|
if not self._active():
|
|
@@ -119,6 +299,7 @@ class Box(_Themed):
|
|
|
119
299
|
"""
|
|
120
300
|
|
|
121
301
|
expand = True
|
|
302
|
+
role = 'region'
|
|
122
303
|
|
|
123
304
|
def __init__(self, child, title=None, style='rounded', padding=1,
|
|
124
305
|
height=None, footer=None, **kwargs):
|
|
@@ -163,7 +344,12 @@ class Box(_Themed):
|
|
|
163
344
|
lines = [pad + _fx.fit_line(line, inner_w) + pad for line in lines]
|
|
164
345
|
focus = self._has_focus()
|
|
165
346
|
color = self.theme.accent if focus else self.theme.border
|
|
166
|
-
|
|
347
|
+
style, title = self.style, self.title
|
|
348
|
+
if focus and self._marked():
|
|
349
|
+
# Focus without colour: a heavy (or double) frame and a marker.
|
|
350
|
+
style = 'double' if style == 'heavy' else 'heavy'
|
|
351
|
+
title = '▸ ' + (title or '')
|
|
352
|
+
return frame(lines, width, title, color, self.paint, style,
|
|
167
353
|
title_color=('bold ' + self.theme.accent) if focus
|
|
168
354
|
else self.theme.title, footer=self.footer)
|
|
169
355
|
|
|
@@ -259,13 +445,44 @@ class Input(_Themed):
|
|
|
259
445
|
mark = '*' if self.required else ''
|
|
260
446
|
spec = self.theme.accent if self.focused else self.theme.title
|
|
261
447
|
out.append(self.paint(self.label + mark, spec))
|
|
448
|
+
self._hot_base = len(out)
|
|
262
449
|
out.extend(self.control(width, height))
|
|
450
|
+
self._hot_base = 0
|
|
263
451
|
if self.help and not self.error:
|
|
264
452
|
out.append(self.paint(self.help, self.theme.muted))
|
|
265
453
|
if self.error:
|
|
266
454
|
out.append(self.paint('✖ ' + self.error, self.theme.error))
|
|
267
455
|
return out
|
|
268
456
|
|
|
457
|
+
role = 'textbox'
|
|
458
|
+
|
|
459
|
+
def a11y_value(self):
|
|
460
|
+
value = self.value
|
|
461
|
+
if value is None:
|
|
462
|
+
return ''
|
|
463
|
+
if isinstance(value, bool):
|
|
464
|
+
return ''
|
|
465
|
+
return str(value)
|
|
466
|
+
|
|
467
|
+
def a11y_state(self):
|
|
468
|
+
state = []
|
|
469
|
+
if self.required:
|
|
470
|
+
state.append('required')
|
|
471
|
+
if self.error:
|
|
472
|
+
state.append('invalid')
|
|
473
|
+
if self.is_empty() and not isinstance(self.value, bool):
|
|
474
|
+
state.append('empty')
|
|
475
|
+
return state
|
|
476
|
+
|
|
477
|
+
def a11y_name(self):
|
|
478
|
+
if self.a11y_label:
|
|
479
|
+
return str(self.a11y_label)
|
|
480
|
+
for text in (self.label, getattr(self, 'placeholder', None),
|
|
481
|
+
(self.name or '').replace('_', ' ')):
|
|
482
|
+
if isinstance(text, str) and text.strip():
|
|
483
|
+
return text.strip()
|
|
484
|
+
return ''
|
|
485
|
+
|
|
269
486
|
def marker(self):
|
|
270
487
|
"""The gutter that shows focus: a bar when focused."""
|
|
271
488
|
if self.focused:
|
|
@@ -282,6 +499,7 @@ class Button(_Themed):
|
|
|
282
499
|
"""
|
|
283
500
|
|
|
284
501
|
focusable = True
|
|
502
|
+
role = 'button'
|
|
285
503
|
|
|
286
504
|
def __init__(self, label, on_press=None, kind='default', shortcut=None,
|
|
287
505
|
**kwargs):
|
|
@@ -319,11 +537,22 @@ class Button(_Themed):
|
|
|
319
537
|
|
|
320
538
|
def draw(self, width, height):
|
|
321
539
|
label = self.text()
|
|
322
|
-
if
|
|
323
|
-
label = '[>' + label[2:-2] + '<]'
|
|
324
|
-
|
|
540
|
+
if self._marked() and self.focused:
|
|
541
|
+
label = ('[>' + label[2:-2] + '<]') if not self._active() else \
|
|
542
|
+
('▸' + label[1:-1] + '◂')
|
|
543
|
+
style = self.style()
|
|
544
|
+
if self.hovered and self._active() and not self.disabled:
|
|
545
|
+
style += ' underline'
|
|
546
|
+
return [self.paint(label, style)]
|
|
325
547
|
|
|
326
548
|
def on_key(self, key):
|
|
327
549
|
if str(key) in ('enter', 'space'):
|
|
328
550
|
return self.press()
|
|
329
551
|
return False
|
|
552
|
+
|
|
553
|
+
def on_mouse(self, event):
|
|
554
|
+
if event.action == 'release' and event.button == 0 and \
|
|
555
|
+
not self.disabled and 0 <= event.y < 1 and \
|
|
556
|
+
0 <= event.x < self.natural_width():
|
|
557
|
+
return self.press()
|
|
558
|
+
return event.action == 'press' and event.button == 0
|