-
-
Notifications
You must be signed in to change notification settings - Fork 695
/
Copy pathnumberinput.py
252 lines (211 loc) · 8.5 KB
/
numberinput.py
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
import sys
from decimal import Decimal, InvalidOperation
from rubicon.objc import SEL, objc_method, objc_property
from travertino.size import at_least
from toga.colors import TRANSPARENT
from toga.widgets.numberinput import _clean_decimal, _clean_decimal_str
from toga_cocoa.colors import native_color
from toga_cocoa.libs import (
NSLayoutAttributeBottom,
NSLayoutAttributeCenterY,
NSLayoutAttributeLeft,
NSLayoutAttributeRight,
NSLayoutAttributeTop,
NSLayoutConstraint,
NSLayoutRelationEqual,
NSStepper,
NSTextAlignment,
NSTextField,
NSTextFieldSquareBezel,
NSTextView,
NSView,
)
from .base import Widget
class TogaStepper(NSStepper):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)
@objc_method
def onChange_(self, stepper) -> None:
# Stepper has increased/decreased
self.interface.value = _clean_decimal(stepper.floatValue, self.interface.step)
@objc_method
def controlTextDidChange_(self, notification) -> None:
value = str(self.impl.native_input.stringValue)
try:
# Try to convert to a decimal. If the value isn't a number,
# this will raise InvalidOperation
Decimal(value)
except InvalidOperation:
# If the string value isn't valid, remove any characters that
# would make it invalid.
self.impl.native_input.stringValue = _clean_decimal_str(value)
self.interface.on_change()
class TogaNumberInput(NSTextField):
interface = objc_property(object, weak=True)
impl = objc_property(object, weak=True)
@objc_method
def textDidEndEditing_(self, notification) -> None:
# Loss of focus; ensure that the displayed value
# matches the clipped, normalized decimal value
self.impl.set_value(self.interface.value)
class NumberInput(Widget):
def create(self):
self.native = NSView.alloc().init()
self.native_input = TogaNumberInput.new()
self.native_input.interface = self.interface
self.native_input.impl = self
self.native_input.bezeled = True
self.native_input.bezelStyle = NSTextFieldSquareBezel
self.native_input.translatesAutoresizingMaskIntoConstraints = False
self.native_input.selectable = True
self.native_stepper = TogaStepper.alloc().init()
self.native_stepper.interface = self.interface
self.native_stepper.impl = self
self.native_stepper.translatesAutoresizingMaskIntoConstraints = False
self.native_stepper.target = self.native_stepper
self.native_stepper.action = SEL("onChange:")
self.native_stepper.valueWraps = False
self.native_stepper.controller = self.native_input
self.native_input.delegate = self.native_stepper
# Add the input and stepper to the constraining box.
self.native.addSubview(self.native_input)
self.native.addSubview(self.native_stepper)
# Add constraints to lay out the input and stepper.
# Stepper is always top right corner.
self.native.addConstraint(
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.native,
NSLayoutAttributeTop,
NSLayoutRelationEqual,
self.native_stepper,
NSLayoutAttributeTop,
1.0,
0,
)
)
self.native.addConstraint(
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.native,
NSLayoutAttributeRight,
NSLayoutRelationEqual,
self.native_stepper,
NSLayoutAttributeRight,
1.0,
0,
)
)
# Stepper height matches container box height
self.native.addConstraint(
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.native,
NSLayoutAttributeBottom,
NSLayoutRelationEqual,
self.native_stepper,
NSLayoutAttributeBottom,
1.0,
0,
)
)
# Input is always left, centred vertically on the stepper
self.native.addConstraint(
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.native_stepper,
NSLayoutAttributeCenterY,
NSLayoutRelationEqual,
self.native_input,
NSLayoutAttributeCenterY,
1.0,
0,
)
)
self.native.addConstraint(
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.native,
NSLayoutAttributeLeft,
NSLayoutRelationEqual,
self.native_input,
NSLayoutAttributeLeft,
1.0,
0,
)
)
# Stepper and input meet in the middle with a small gap
self.native.addConstraint(
NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_(
self.native_stepper,
NSLayoutAttributeLeft,
NSLayoutRelationEqual,
self.native_input,
NSLayoutAttributeRight,
1.0,
2,
)
)
# Add the layout constraints for the main box
self.add_constraints()
def set_color(self, color):
self.native_input.textColor = native_color(color)
def set_background_color(self, color):
if color is TRANSPARENT:
# The text view needs to be made transparent *and* non-bezeled
self.native_input.drawsBackground = False
self.native_input.bezeled = False
else:
self.native_input.drawsBackground = True
self.native_input.bezeled = True
self.native_input.backgroundColor = native_color(color)
def has_focus(self):
# When the NSTextField gets focus, a field editor is created, and that editor
# has the original widget as the delegate. The first responder is the Field Editor.
return isinstance(self.native.window.firstResponder, NSTextView) and (
self.native.window.firstResponder.delegate == self.native_input
)
def focus(self):
if not self.has_focus():
self.interface.window._impl.native.makeFirstResponder(self.native_input)
def get_readonly(self):
return not self.native_input.isEditable()
def set_readonly(self, value):
self.native_input.editable = not value
def set_step(self, step):
self.native_stepper.increment = step
def set_min_value(self, value):
if value is None:
self.native_stepper.minValue = -sys.float_info.max
else:
self.native_stepper.minValue = float(value)
def set_max_value(self, value):
if value is None:
self.native_stepper.maxValue = sys.float_info.max
else:
self.native_stepper.maxValue = float(value)
def set_alignment(self, value):
self.native_input.alignment = NSTextAlignment(value)
def set_font(self, font):
self.native_input.font = font._impl.native
def get_value(self):
try:
return _clean_decimal(
str(self.native_input.stringValue), self.interface.step
)
except InvalidOperation:
return None
def set_value(self, value):
if value is None:
self.native_stepper.floatValue = 0.0
self.native_input.stringValue = ""
else:
self.native_stepper.floatValue = float(value)
self.native_input.stringValue = str(value)
self.interface.on_change()
def get_enabled(self):
return self.native_input.isEnabled
def set_enabled(self, value):
self.native_input.enabled = value
self.native_stepper.enabled = value
def rehint(self):
# Height of a text input is known and fixed.
input_size = self.native_input.intrinsicContentSize()
stepper_size = self.native_stepper.intrinsicContentSize()
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
self.interface.intrinsic.height = max(input_size.height, stepper_size.height)