Mercurial > libervia-web
comparison browser/sat_browser/dialog.py @ 1124:28e3eb3bb217
files reorganisation and installation rework:
- files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory)
- VERSION file is now used, as for other SàT projects
- replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly
- removed check for data_dir if it's empty
- installation tested working in virtual env
- libervia launching script is now in bin/libervia
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 25 Aug 2018 17:59:48 +0200 |
parents | src/browser/sat_browser/dialog.py@f2170536ba23 |
children | 2af117bfe6cc |
comparison
equal
deleted
inserted
replaced
1123:63a4b8fe9782 | 1124:28e3eb3bb217 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.log import getLogger | |
21 log = getLogger(__name__) | |
22 | |
23 from constants import Const as C | |
24 from sat_frontends.tools import jid | |
25 | |
26 from pyjamas.ui.VerticalPanel import VerticalPanel | |
27 from pyjamas.ui.Grid import Grid | |
28 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
29 from pyjamas.ui.PopupPanel import PopupPanel | |
30 from pyjamas.ui.DialogBox import DialogBox | |
31 from pyjamas.ui.ListBox import ListBox | |
32 from pyjamas.ui.Button import Button | |
33 from pyjamas.ui.TextBox import TextBox | |
34 from pyjamas.ui.Label import Label | |
35 from pyjamas.ui.HTML import HTML | |
36 from pyjamas.ui.RadioButton import RadioButton | |
37 from pyjamas.ui import HasAlignment | |
38 from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER | |
39 from pyjamas.ui.MouseListener import MouseWheelHandler | |
40 from pyjamas import Window | |
41 | |
42 import base_panel | |
43 | |
44 | |
45 # List here the patterns that are not allowed in contact group names | |
46 FORBIDDEN_PATTERNS_IN_GROUP = () | |
47 | |
48 | |
49 unicode = str # XXX: pyjama doesn't manage unicode | |
50 | |
51 | |
52 class RoomChooser(Grid): | |
53 """Select a room from the rooms you already joined, or create a new one""" | |
54 | |
55 GENERATE_MUC = "<use random name>" | |
56 | |
57 def __init__(self, host, room_jid_s=None): | |
58 """ | |
59 | |
60 @param host (SatWebFrontend) | |
61 @param room_jid_s (unicode): room JID | |
62 """ | |
63 Grid.__init__(self, 2, 2, Width='100%') | |
64 self.host = host | |
65 | |
66 self.new_radio = RadioButton("room", "Discussion room:") | |
67 self.new_radio.setChecked(True) | |
68 self.box = TextBox(Width='95%') | |
69 self.box.setText(room_jid_s if room_jid_s else self.GENERATE_MUC) | |
70 self.exist_radio = RadioButton("room", "Already joined:") | |
71 self.rooms_list = ListBox(Width='95%') | |
72 | |
73 self.add(self.new_radio, 0, 0) | |
74 self.add(self.box, 0, 1) | |
75 self.add(self.exist_radio, 1, 0) | |
76 self.add(self.rooms_list, 1, 1) | |
77 | |
78 self.box.addFocusListener(self) | |
79 self.rooms_list.addFocusListener(self) | |
80 | |
81 self.exist_radio.setVisible(False) | |
82 self.rooms_list.setVisible(False) | |
83 self.refreshOptions() | |
84 | |
85 @property | |
86 def room(self): | |
87 """Get the room that has been selected or entered by the user | |
88 | |
89 @return: jid.JID or None to let the backend generate a new name | |
90 """ | |
91 if self.exist_radio.getChecked(): | |
92 values = self.rooms_list.getSelectedValues() | |
93 return jid.JID(values[0]) if values else None | |
94 value = self.box.getText() | |
95 return None if value in ('', self.GENERATE_MUC) else jid.JID(value) | |
96 | |
97 def onFocus(self, sender): | |
98 if sender == self.rooms_list: | |
99 self.exist_radio.setChecked(True) | |
100 elif sender == self.box: | |
101 if self.box.getText() == self.GENERATE_MUC: | |
102 self.box.setText("") | |
103 self.new_radio.setChecked(True) | |
104 | |
105 def onLostFocus(self, sender): | |
106 if sender == self.box: | |
107 if self.box.getText() == "": | |
108 self.box.setText(self.GENERATE_MUC) | |
109 | |
110 def refreshOptions(self): | |
111 """Refresh the already joined room list""" | |
112 contact_list = self.host.contact_list | |
113 muc_rooms = contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP) | |
114 for room in muc_rooms: | |
115 self.rooms_list.addItem(room.bare) | |
116 if len(muc_rooms) > 0: | |
117 self.exist_radio.setVisible(True) | |
118 self.rooms_list.setVisible(True) | |
119 self.exist_radio.setChecked(True) | |
120 | |
121 | |
122 class ContactsChooser(VerticalPanel): | |
123 """Select one or several connected contacts""" | |
124 | |
125 def __init__(self, host, nb_contact=None, ok_button=None): | |
126 """ | |
127 @param host: SatWebFrontend instance | |
128 @param nb_contact: number of contacts that have to be selected, None for no limit | |
129 If a tuple is given instead of an integer, nb_contact[0] is the minimal and | |
130 nb_contact[1] is the maximal number of contacts to be chosen. | |
131 """ | |
132 self.host = host | |
133 if isinstance(nb_contact, tuple): | |
134 if len(nb_contact) == 0: | |
135 nb_contact = None | |
136 elif len(nb_contact) == 1: | |
137 nb_contact = (nb_contact[0], nb_contact[0]) | |
138 elif nb_contact is not None: | |
139 nb_contact = (nb_contact, nb_contact) | |
140 if nb_contact is None: | |
141 log.debug("Need to select as many contacts as you want") | |
142 else: | |
143 log.debug("Need to select between %d and %d contacts" % nb_contact) | |
144 self.nb_contact = nb_contact | |
145 self.ok_button = ok_button | |
146 VerticalPanel.__init__(self, Width='100%') | |
147 self.contacts_list = ListBox() | |
148 self.contacts_list.setMultipleSelect(True) | |
149 self.contacts_list.setWidth("95%") | |
150 self.contacts_list.addStyleName('contactsChooser') | |
151 self.contacts_list.addChangeListener(self.onChange) | |
152 self.add(self.contacts_list) | |
153 self.refreshOptions() | |
154 self.onChange() | |
155 | |
156 @property | |
157 def contacts(self): | |
158 """Return the selected contacts. | |
159 | |
160 @return: list[jid.JID] | |
161 """ | |
162 return [jid.JID(contact) for contact in self.contacts_list.getSelectedValues(True)] | |
163 | |
164 def onChange(self, sender=None): | |
165 if self.ok_button is None: | |
166 return | |
167 if self.nb_contact: | |
168 selected = len(self.contacts_list.getSelectedValues(True)) | |
169 if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: | |
170 self.ok_button.setEnabled(True) | |
171 else: | |
172 self.ok_button.setEnabled(False) | |
173 | |
174 def refreshOptions(self, keep_selected=False): | |
175 """Fill the list with the connected contacts. | |
176 | |
177 @param keep_selected (boolean): if True, keep the current selection | |
178 """ | |
179 selection = self.contacts if keep_selected else [] | |
180 self.contacts_list.clear() | |
181 contacts = self.host.contact_list.roster_connected | |
182 self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) | |
183 self.contacts_list.addItem("") | |
184 for contact in contacts: | |
185 self.contacts_list.addItem(contact) | |
186 if selection: | |
187 self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection]) | |
188 | |
189 | |
190 class RoomAndContactsChooser(DialogBox): | |
191 """Select a room and some users to invite in""" | |
192 | |
193 def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", | |
194 title_room="Join room", title_invite="Invite contacts", visible=(True, True)): | |
195 DialogBox.__init__(self, centered=True) | |
196 self.host = host | |
197 self.callback = callback | |
198 self.title_room = title_room | |
199 self.title_invite = title_invite | |
200 | |
201 button_panel = HorizontalPanel() | |
202 button_panel.addStyleName("marginAuto") | |
203 ok_button = Button("OK", self.onOK) | |
204 button_panel.add(ok_button) | |
205 button_panel.add(Button("Cancel", self.onCancel)) | |
206 | |
207 self.room_panel = RoomChooser(host, None if visible == (False, True) else host.default_muc) | |
208 self.contact_panel = ContactsChooser(host, nb_contact, ok_button) | |
209 | |
210 self.stack_panel = base_panel.ToggleStackPanel(Width="100%") | |
211 self.stack_panel.add(self.room_panel, visible=visible[0]) | |
212 self.stack_panel.add(self.contact_panel, visible=visible[1]) | |
213 self.stack_panel.addStackChangeListener(self) | |
214 self.onStackChanged(self.stack_panel, 0, visible[0]) | |
215 self.onStackChanged(self.stack_panel, 1, visible[1]) | |
216 | |
217 main_panel = VerticalPanel() | |
218 main_panel.setStyleName("room-contact-chooser") | |
219 main_panel.add(self.stack_panel) | |
220 main_panel.add(button_panel) | |
221 | |
222 self.setWidget(main_panel) | |
223 self.setHTML(title) | |
224 self.show() | |
225 | |
226 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword) | |
227 self.presenceListener = self.refreshContactList | |
228 # update the contacts list when someone logged in/out | |
229 self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) | |
230 | |
231 @property | |
232 def room(self): | |
233 """Get the room that has been selected or entered by the user | |
234 | |
235 @return: jid.JID or None | |
236 """ | |
237 return self.room_panel.room | |
238 | |
239 @property | |
240 def contacts(self): | |
241 """Return the selected contacts. | |
242 | |
243 @return: list[jid.JID] | |
244 """ | |
245 return self.contact_panel.contacts | |
246 | |
247 def onStackChanged(self, sender, index, visible=None): | |
248 if visible is None: | |
249 visible = sender.getWidget(index).getVisible() | |
250 if index == 0: | |
251 suffix = "" if (visible or not self.room) else ": %s" % self.room | |
252 sender.setStackText(0, self.title_room + suffix) | |
253 elif index == 1: | |
254 suffix = "" if (visible or not self.contacts) else ": %s" % ", ".join([unicode(contact) for contact in self.contacts]) | |
255 sender.setStackText(1, self.title_invite + suffix) | |
256 | |
257 def refreshContactList(self, *args, **kwargs): | |
258 """Called when someone log in/out to update the list. | |
259 | |
260 @param args: set by the event call but not used here | |
261 """ | |
262 self.contact_panel.refreshOptions(keep_selected=True) | |
263 | |
264 def onOK(self, sender): | |
265 room = self.room # pyjamas issue: you need to use an intermediate variable to access a property's method | |
266 self.hide() | |
267 self.callback(room, self.contacts) | |
268 | |
269 def onCancel(self, sender): | |
270 self.hide() | |
271 | |
272 def hide(self): | |
273 self.host.removeListener('presence', self.presenceListener) | |
274 DialogBox.hide(self, autoClosed=True) | |
275 | |
276 | |
277 class GenericConfirmDialog(DialogBox): | |
278 | |
279 def __init__(self, widgets, callback, title='Confirmation', prompt_widgets=None, **kwargs): | |
280 """ | |
281 Dialog to confirm an action | |
282 @param widgets (list[Widget]): widgets to attach | |
283 @param callback (callable): method to call when a button is pressed, | |
284 with the following arguments: | |
285 - result (bool): set to True if the dialog has been confirmed | |
286 - *args: a list of unicode (the values for the prompt_widgets) | |
287 @param title: title of the dialog | |
288 @param prompt_widgets (list[TextBox]): input widgets from which to retrieve | |
289 the string value(s) to be passed to the callback when OK button is pressed. | |
290 If None, OK button will return "True". Cancel button always returns "False". | |
291 """ | |
292 self.callback = callback | |
293 added_style = kwargs.pop('AddStyleName', None) | |
294 DialogBox.__init__(self, centered=True, **kwargs) | |
295 if added_style: | |
296 self.addStyleName(added_style) | |
297 | |
298 if prompt_widgets is None: | |
299 prompt_widgets = [] | |
300 | |
301 content = VerticalPanel() | |
302 content.setWidth('100%') | |
303 for wid in widgets: | |
304 content.add(wid) | |
305 if wid in prompt_widgets: | |
306 wid.setWidth('100%') | |
307 button_panel = HorizontalPanel() | |
308 button_panel.addStyleName("marginAuto") | |
309 self.confirm_button = Button("OK", self.onConfirm) | |
310 button_panel.add(self.confirm_button) | |
311 self.cancel_button = Button("Cancel", self.onCancel) | |
312 button_panel.add(self.cancel_button) | |
313 content.add(button_panel) | |
314 self.setHTML(title) | |
315 self.setWidget(content) | |
316 self.prompt_widgets = prompt_widgets | |
317 | |
318 def onConfirm(self, sender): | |
319 self.hide() | |
320 result = [True] | |
321 result.extend([box.getText() for box in self.prompt_widgets]) | |
322 self.callback(*result) | |
323 | |
324 def onCancel(self, sender): | |
325 self.hide() | |
326 self.callback(False) | |
327 | |
328 def show(self): | |
329 DialogBox.show(self) | |
330 if self.prompt_widgets: | |
331 self.prompt_widgets[0].setFocus(True) | |
332 | |
333 | |
334 class ConfirmDialog(GenericConfirmDialog): | |
335 | |
336 def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): | |
337 GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) | |
338 | |
339 | |
340 class GenericDialog(DialogBox): | |
341 """Dialog which just show a widget and a close button""" | |
342 | |
343 def __init__(self, title, main_widget, callback=None, options=None, **kwargs): | |
344 """Simple notice dialog box | |
345 @param title: HTML put in the header | |
346 @param main_widget: widget put in the body | |
347 @param callback: method to call on closing | |
348 @param options: one or more of the following options: | |
349 - NO_CLOSE: don't add a close button""" | |
350 added_style = kwargs.pop('AddStyleName', None) | |
351 DialogBox.__init__(self, centered=True, **kwargs) | |
352 if added_style: | |
353 self.addStyleName(added_style) | |
354 | |
355 self.callback = callback | |
356 if not options: | |
357 options = [] | |
358 _body = VerticalPanel() | |
359 _body.setSize('100%', '100%') | |
360 _body.setSpacing(4) | |
361 _body.add(main_widget) | |
362 _body.setCellWidth(main_widget, '100%') | |
363 _body.setCellHeight(main_widget, '100%') | |
364 if 'NO_CLOSE' not in options: | |
365 _close_button = Button("Close", self.onClose) | |
366 _body.add(_close_button) | |
367 _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) | |
368 self.setHTML(title) | |
369 self.setWidget(_body) | |
370 self.panel.setSize('100%', '100%') # Need this hack to have correct size in Gecko & Webkit | |
371 | |
372 def close(self): | |
373 """Same effect as clicking the close button""" | |
374 self.onClose(None) | |
375 | |
376 def onClose(self, sender): | |
377 self.hide() | |
378 if self.callback: | |
379 self.callback() | |
380 | |
381 | |
382 class InfoDialog(GenericDialog): | |
383 | |
384 def __init__(self, title, body, callback=None, options=None, **kwargs): | |
385 GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) | |
386 | |
387 | |
388 class PromptDialog(GenericConfirmDialog): | |
389 | |
390 def __init__(self, callback, textes=None, values=None, title='User input', **kwargs): | |
391 """Prompt the user for one or more input(s). | |
392 | |
393 @param callback (callable): method to call when a button is pressed, | |
394 with the following arguments: | |
395 - result (bool): set to True if the dialog has been confirmed | |
396 - *args: a list of unicode (the values entered by the user) | |
397 @param textes (list[unicode]): HTML textes to display before the inputs | |
398 @param values (list[unicode]): default values for each input | |
399 @param title (unicode): dialog title | |
400 """ | |
401 if textes is None: | |
402 textes = [''] # display a single input without any description | |
403 if values is None: | |
404 values = [] | |
405 all_widgets = [] | |
406 prompt_widgets = [] | |
407 for count in xrange(len(textes)): | |
408 all_widgets.append(HTML(textes[count])) | |
409 prompt = TextBox() | |
410 if len(values) > count: | |
411 prompt.setText(values[count]) | |
412 all_widgets.append(prompt) | |
413 prompt_widgets.append(prompt) | |
414 | |
415 GenericConfirmDialog.__init__(self, all_widgets, callback, title, prompt_widgets, **kwargs) | |
416 | |
417 | |
418 class PopupPanelWrapper(PopupPanel): | |
419 """This wrapper catch Escape event to avoid request cancellation by Firefox""" | |
420 | |
421 def onEventPreview(self, event): | |
422 if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: | |
423 # needed to prevent request cancellation in Firefox | |
424 event.preventDefault() | |
425 return PopupPanel.onEventPreview(self, event) | |
426 | |
427 | |
428 class ExtTextBox(TextBox): | |
429 """Extended TextBox""" | |
430 | |
431 def __init__(self, *args, **kwargs): | |
432 if 'enter_cb' in kwargs: | |
433 self.enter_cb = kwargs['enter_cb'] | |
434 del kwargs['enter_cb'] | |
435 TextBox.__init__(self, *args, **kwargs) | |
436 self.addKeyboardListener(self) | |
437 | |
438 def onKeyUp(self, sender, keycode, modifiers): | |
439 pass | |
440 | |
441 def onKeyDown(self, sender, keycode, modifiers): | |
442 pass | |
443 | |
444 def onKeyPress(self, sender, keycode, modifiers): | |
445 if self.enter_cb and keycode == KEY_ENTER: | |
446 self.enter_cb(self) | |
447 | |
448 | |
449 class GroupSelector(DialogBox): | |
450 | |
451 def __init__(self, top_widgets, initial_groups, selected_groups, | |
452 ok_title="OK", ok_cb=None, cancel_cb=None): | |
453 DialogBox.__init__(self, centered=True) | |
454 main_panel = VerticalPanel() | |
455 self.ok_cb = ok_cb | |
456 self.cancel_cb = cancel_cb | |
457 | |
458 for wid in top_widgets: | |
459 main_panel.add(wid) | |
460 | |
461 main_panel.add(Label('Select in which groups your contact is:')) | |
462 self.list_box = ListBox() | |
463 self.list_box.setMultipleSelect(True) | |
464 self.list_box.setVisibleItemCount(5) | |
465 self.setAvailableGroups(initial_groups) | |
466 self.setGroupsSelected(selected_groups) | |
467 main_panel.add(self.list_box) | |
468 | |
469 def cb(text): | |
470 self.list_box.addItem(text) | |
471 self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") | |
472 | |
473 main_panel.add(AddGroupPanel(initial_groups, cb)) | |
474 | |
475 button_panel = HorizontalPanel() | |
476 button_panel.addStyleName("marginAuto") | |
477 button_panel.add(Button(ok_title, self.onOK)) | |
478 button_panel.add(Button("Cancel", self.onCancel)) | |
479 main_panel.add(button_panel) | |
480 | |
481 self.setWidget(main_panel) | |
482 | |
483 def getSelectedGroups(self): | |
484 """Return a list of selected groups""" | |
485 return self.list_box.getSelectedValues() | |
486 | |
487 def setAvailableGroups(self, groups): | |
488 _groups = list(set(groups)) | |
489 _groups.sort() | |
490 self.list_box.clear() | |
491 for group in _groups: | |
492 self.list_box.addItem(group) | |
493 | |
494 def setGroupsSelected(self, selected_groups): | |
495 self.list_box.setItemTextSelection(selected_groups) | |
496 | |
497 def onOK(self, sender): | |
498 self.hide() | |
499 if self.ok_cb: | |
500 self.ok_cb(self) | |
501 | |
502 def onCancel(self, sender): | |
503 self.hide() | |
504 if self.cancel_cb: | |
505 self.cancel_cb(self) | |
506 | |
507 | |
508 class AddGroupPanel(HorizontalPanel): | |
509 def __init__(self, groups, cb=None): | |
510 """ | |
511 @param groups: list of the already existing groups | |
512 """ | |
513 HorizontalPanel.__init__(self) | |
514 self.groups = groups | |
515 self.add(Label('New group:')) | |
516 self.textbox = ExtTextBox(enter_cb=self.onGroupInput) | |
517 self.add(self.textbox) | |
518 self.add(Button("Add", lambda sender: self.onGroupInput(self.textbox))) | |
519 self.cb = cb | |
520 | |
521 def onGroupInput(self, sender): | |
522 text = sender.getText() | |
523 if text == "": | |
524 return | |
525 for group in self.groups: | |
526 if text == group: | |
527 Window.alert("The group '%s' already exists." % text) | |
528 return | |
529 for pattern in FORBIDDEN_PATTERNS_IN_GROUP: | |
530 if pattern in text: | |
531 Window.alert("The pattern '%s' is not allowed in group names." % pattern) | |
532 return | |
533 sender.setText('') | |
534 self.groups.append(text) | |
535 if self.cb is not None: | |
536 self.cb(text) | |
537 | |
538 | |
539 class WheelTextBox(TextBox, MouseWheelHandler): | |
540 | |
541 def __init__(self, *args, **kwargs): | |
542 TextBox.__init__(self, *args, **kwargs) | |
543 MouseWheelHandler.__init__(self) | |
544 | |
545 | |
546 class IntSetter(HorizontalPanel): | |
547 """This class show a bar with button to set an int value""" | |
548 | |
549 def __init__(self, label, value=0, value_max=None, visible_len=3): | |
550 """initialize the intSetter | |
551 @param label: text shown in front of the setter | |
552 @param value: initial value | |
553 @param value_max: limit value, None or 0 for unlimited""" | |
554 HorizontalPanel.__init__(self) | |
555 self.value = value | |
556 self.value_max = value_max | |
557 _label = Label(label) | |
558 self.add(_label) | |
559 self.setCellWidth(_label, "100%") | |
560 minus_button = Button("-", self.onMinus) | |
561 self.box = WheelTextBox() | |
562 self.box.setVisibleLength(visible_len) | |
563 self.box.setText(unicode(value)) | |
564 self.box.addInputListener(self) | |
565 self.box.addMouseWheelListener(self) | |
566 plus_button = Button("+", self.onPlus) | |
567 self.add(minus_button) | |
568 self.add(self.box) | |
569 self.add(plus_button) | |
570 self.valueChangedListener = [] | |
571 | |
572 def addValueChangeListener(self, listener): | |
573 self.valueChangedListener.append(listener) | |
574 | |
575 def removeValueChangeListener(self, listener): | |
576 if listener in self.valueChangedListener: | |
577 self.valueChangedListener.remove(listener) | |
578 | |
579 def _callListeners(self): | |
580 for listener in self.valueChangedListener: | |
581 listener(self.value) | |
582 | |
583 def setValue(self, value): | |
584 """Change the value and fire valueChange listeners""" | |
585 self.value = value | |
586 self.box.setText(unicode(value)) | |
587 self._callListeners() | |
588 | |
589 def onMinus(self, sender, step=1): | |
590 self.value = max(0, self.value - step) | |
591 self.box.setText(unicode(self.value)) | |
592 self._callListeners() | |
593 | |
594 def onPlus(self, sender, step=1): | |
595 self.value += step | |
596 if self.value_max: | |
597 self.value = min(self.value, self.value_max) | |
598 self.box.setText(unicode(self.value)) | |
599 self._callListeners() | |
600 | |
601 def onInput(self, sender): | |
602 """Accept only valid integer && normalize print (no leading 0)""" | |
603 try: | |
604 self.value = int(self.box.getText()) if self.box.getText() else 0 | |
605 except ValueError: | |
606 pass | |
607 if self.value_max: | |
608 self.value = min(self.value, self.value_max) | |
609 self.box.setText(unicode(self.value)) | |
610 self._callListeners() | |
611 | |
612 def onMouseWheel(self, sender, velocity): | |
613 if velocity > 0: | |
614 self.onMinus(sender, 10) | |
615 else: | |
616 self.onPlus(sender, 10) |