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
|
"""Button for proxy actions.
Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2022
"""
# standard imports
import logging
import os
# kivy imports
from kivy import metrics
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.image import Image
from kivy.uix.label import Label
import kivy.properties as kp
# tagit imports
from tagit.external.tooltip import Tooltip
# exports
__all__ = ('Action', )
## code ##
logger = logging.getLogger(__name__)
# load kv
Builder.load_file(os.path.join(os.path.dirname(__file__), 'action.kv'))
class Action(ButtonBehavior, BoxLayout, Tooltip):
"""
An Action can be triggered in three ways:
* Touch event: Clicking or touching on the button
* Key event: Entering a keyboard shortcut
* Single-shot: Programmatically triggered once, without UI attachment
For the last, use the *single_shot* classmethod.
For the first two, declare the Action in a kv file or in code.
Note that the remarks below about kv do not apply if the object is
created in code.
When an Action is declared in kv, two restrictions apply (also see
examples below):
* To disable touch_trigger, it must be declared last
* show must be declared after all other properties
Enable key triggers, but hide the Action in the UI:
Action:
show: []
Action:
# alias for the one above
Show text, image, or both, with default height and the width
stretched as necessary:
Action:
show: 'text',
Action:
show: 'image',
Action:
show: 'text', 'image'
Action:
width: 200 # has no effect unless autowidth is False
show: 'image', 'text'
Make the Action larger:
Action:
# increased height. The image width scales accordingly
height: 80
show: 'image', 'text'
Action:
# scales to parent widget's width
autowidth: False
size_hint_x: 1
show: 'image', 'text'
Action:
# fixed width and height
width: 150
height: 80
autowidth: False
show: 'image', 'text' # must be declared **last**
Show the button but disable touch events:
Action:
height: 80
show: 'image', 'text'
touch_trigger: False # must be declared **after** show
Do the same in code:
>>> Action(
... size=(130, 80),
... autowidth=False,
... show=('image', 'text'),
... text='foobar',
... touch_trigger=False)
"""
# content
tooltip = kp.StringProperty()
source = kp.StringProperty()
text = kp.StringProperty()
# visibility flags
show = kp.ListProperty([])
# sizing
default_height = kp.NumericProperty(30)
autowidth = kp.BooleanProperty(True)
# responsiveness
key_trigger = kp.BooleanProperty(True)
touch_trigger = kp.BooleanProperty(True)
# required such that properties can be set via constructor
font_size = kp.StringProperty("15sp")
# FIXME: Check why I have to pass size instead of height/width
height = kp.NumericProperty(0)
width = kp.NumericProperty(0)
# internal properties
root = kp.ObjectProperty(None)
selected_alpha = kp.NumericProperty(0)
_image = kp.ObjectProperty(None)
_label = kp.ObjectProperty(None)
def __init__(self, **kwargs):
show = kwargs.pop('show', [])
touch_trigger = kwargs.pop('touch_trigger', None)
super(Action, self).__init__(**kwargs)
# delay such that on_show is executed once the other
# properties are set
self.show = show
if touch_trigger is not None:
self.touch_trigger = touch_trigger
## display
def on_show(self, wx, show):
if self._image is not None:
self.remove_widget(self._image)
if self._label is not None:
self.remove_widget(self._label)
if 'image' in show:
self.height = self.default_height if self.height == 0 else self.height
self.touch_trigger = True
self._image = Image(
source=self.source,
# size
height=self.height,
width=self.height, # square image
size_hint=(None, None),
)
self.add_widget(self._image)
if self.autowidth:
self.width = self.height
if 'text' in show:
self.height = self.default_height if self.height == 0 else self.height
self.touch_trigger = True
self._label = Label(
# text
text=self.text,
halign='left',
valign='middle',
padding=(metrics.dp(10), 0),
# size
font_size=self.font_size,
height=self.height,
size_hint=(None, None),
)
self._label.bind(texture_size=self.on_texture_size)
self.add_widget(self._label)
def on_size(self, wx, size):
for child in self.children:
child.height = self.height
if isinstance(child, Image):
child.width = self.height
elif not self.autowidth: # must be the label
self.on_texture_size(child, None)
def on_texture_size(self, label, texture_size):
if self.autowidth:
# adapt the width to the label's width
self.width = max(0, sum([child.width for child in self.children]))
else:
# adapt the label's width
others = sum([child.width for child in self.children if child != label])
label.width = self.width - others
label.height = self.height
label.text_size = (self.width - others, self.height)
def on_tooltip(self, callee, text):
self.set_tooltip(text)
## properties
@property
def cfg(self):
return self.root.session.cfg
## action triggering
@classmethod
def single_shot(cls, root, *args, **kwargs):
#logger.info(f'action: {cls.__name__}, {args}, {kwargs}')
root.action_log.append(str(cls.__name__))
return cls(root=root).apply(*args, **kwargs)
def on_root(self, wx, root):
root.keys.bind(on_press=self.on_keyboard)
def on_press(self):
self.selected_alpha = 1
def on_release(self):
if self.touch_trigger:
Animation(selected_alpha=0, d=.25, t='out_quad').start(self)
#logger.info(f'action: {type(self).__name__}')
self.root.action_log.append(str(type(self).__name__))
self.apply()
def on_keyboard(self, wx, evt):
if self.key_trigger and self.ktrigger(evt):
#logger.info(f'action: {type(self).__name__}')
self.root.action_log.append(str(type(self).__name__))
self.apply()
# stop the event from further processing
return True
## interfaces for subtypes
def ktrigger(self, evt):
"""Return True if the action should be triggered by keyboard event *evt*."""
return False
def apply(self, *args, **kwargs):
"""Execute the action."""
pass
## EOF ##
|