Mercurial > libervia-web
comparison src/browser/sat_browser/dialog.py @ 467:97c72fe4a5f2
browser_side: import fixes:
- moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly
- refactored bad import practices: classes are most of time not imported directly, module is imported instead.
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 09 Jun 2014 22:15:26 +0200 |
parents | src/browser/dialog.py@1a0cec9b0f1e |
children | 1af112b97e45 |
comparison
equal
deleted
inserted
replaced
466:01880aa8ea2d | 467:97c72fe4a5f2 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011, 2012, 2013, 2014 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 from sat_frontends.tools.misc import DEFAULT_MUC | |
23 | |
24 from pyjamas.ui.VerticalPanel import VerticalPanel | |
25 from pyjamas.ui.Grid import Grid | |
26 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
27 from pyjamas.ui.PopupPanel import PopupPanel | |
28 from pyjamas.ui.DialogBox import DialogBox | |
29 from pyjamas.ui.ListBox import ListBox | |
30 from pyjamas.ui.Button import Button | |
31 from pyjamas.ui.TextBox import TextBox | |
32 from pyjamas.ui.Label import Label | |
33 from pyjamas.ui.HTML import HTML | |
34 from pyjamas.ui.RadioButton import RadioButton | |
35 from pyjamas.ui import HasAlignment | |
36 from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER | |
37 from pyjamas.ui.MouseListener import MouseWheelHandler | |
38 from pyjamas import Window | |
39 | |
40 import base_panels | |
41 | |
42 | |
43 # List here the patterns that are not allowed in contact group names | |
44 FORBIDDEN_PATTERNS_IN_GROUP = () | |
45 | |
46 | |
47 class RoomChooser(Grid): | |
48 """Select a room from the rooms you already joined, or create a new one""" | |
49 | |
50 GENERATE_MUC = "<use random name>" | |
51 | |
52 def __init__(self, host, default_room=DEFAULT_MUC): | |
53 Grid.__init__(self, 2, 2, Width='100%') | |
54 self.host = host | |
55 | |
56 self.new_radio = RadioButton("room", "Discussion room:") | |
57 self.new_radio.setChecked(True) | |
58 self.box = TextBox(Width='95%') | |
59 self.box.setText(self.GENERATE_MUC if default_room == "" else default_room) | |
60 self.exist_radio = RadioButton("room", "Already joined:") | |
61 self.rooms_list = ListBox(Width='95%') | |
62 | |
63 self.add(self.new_radio, 0, 0) | |
64 self.add(self.box, 0, 1) | |
65 self.add(self.exist_radio, 1, 0) | |
66 self.add(self.rooms_list, 1, 1) | |
67 | |
68 self.box.addFocusListener(self) | |
69 self.rooms_list.addFocusListener(self) | |
70 | |
71 self.exist_radio.setVisible(False) | |
72 self.rooms_list.setVisible(False) | |
73 self.setRooms() | |
74 | |
75 def onFocus(self, sender): | |
76 if sender == self.rooms_list: | |
77 self.exist_radio.setChecked(True) | |
78 elif sender == self.box: | |
79 if self.box.getText() == self.GENERATE_MUC: | |
80 self.box.setText("") | |
81 self.new_radio.setChecked(True) | |
82 | |
83 def onLostFocus(self, sender): | |
84 if sender == self.box: | |
85 if self.box.getText() == "": | |
86 self.box.setText(self.GENERATE_MUC) | |
87 | |
88 def setRooms(self): | |
89 for room in self.host.room_list: | |
90 self.rooms_list.addItem(room.bare) | |
91 if len(self.host.room_list) > 0: | |
92 self.exist_radio.setVisible(True) | |
93 self.rooms_list.setVisible(True) | |
94 self.exist_radio.setChecked(True) | |
95 | |
96 def getRoom(self): | |
97 if self.exist_radio.getChecked(): | |
98 values = self.rooms_list.getSelectedValues() | |
99 return "" if values == [] else values[0] | |
100 value = self.box.getText() | |
101 return "" if value == self.GENERATE_MUC else value | |
102 | |
103 | |
104 class ContactsChooser(VerticalPanel): | |
105 """Select one or several connected contacts""" | |
106 | |
107 def __init__(self, host, nb_contact=None, ok_button=None): | |
108 """ | |
109 @param host: SatWebFrontend instance | |
110 @param nb_contact: number of contacts that have to be selected, None for no limit | |
111 If a tuple is given instead of an integer, nb_contact[0] is the minimal and | |
112 nb_contact[1] is the maximal number of contacts to be chosen. | |
113 """ | |
114 self.host = host | |
115 if isinstance(nb_contact, tuple): | |
116 if len(nb_contact) == 0: | |
117 nb_contact = None | |
118 elif len(nb_contact) == 1: | |
119 nb_contact = (nb_contact[0], nb_contact[0]) | |
120 elif nb_contact is not None: | |
121 nb_contact = (nb_contact, nb_contact) | |
122 if nb_contact is None: | |
123 log.warning("Need to select as many contacts as you want") | |
124 else: | |
125 log.warning("Need to select between %d and %d contacts" % nb_contact) | |
126 self.nb_contact = nb_contact | |
127 self.ok_button = ok_button | |
128 VerticalPanel.__init__(self, Width='100%') | |
129 self.contacts_list = ListBox() | |
130 self.contacts_list.setMultipleSelect(True) | |
131 self.contacts_list.setWidth("95%") | |
132 self.contacts_list.addStyleName('contactsChooser') | |
133 self.contacts_list.addChangeListener(self.onChange) | |
134 self.add(self.contacts_list) | |
135 self.setContacts() | |
136 self.onChange() | |
137 | |
138 def onChange(self, sender=None): | |
139 if self.ok_button is None: | |
140 return | |
141 if self.nb_contact: | |
142 selected = len(self.contacts_list.getSelectedValues(True)) | |
143 if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: | |
144 self.ok_button.setEnabled(True) | |
145 else: | |
146 self.ok_button.setEnabled(False) | |
147 | |
148 def setContacts(self, selected=[]): | |
149 """Fill the list with the connected contacts | |
150 @param select: list of the contacts to select by default | |
151 """ | |
152 self.contacts_list.clear() | |
153 contacts = self.host.contact_panel.getConnected(filter_muc=True) | |
154 self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) | |
155 self.contacts_list.addItem("") | |
156 for contact in contacts: | |
157 if contact not in [room.bare for room in self.host.room_list]: | |
158 self.contacts_list.addItem(contact) | |
159 self.contacts_list.setItemTextSelection(selected) | |
160 | |
161 def getContacts(self): | |
162 return self.contacts_list.getSelectedValues(True) | |
163 | |
164 | |
165 class RoomAndContactsChooser(DialogBox): | |
166 """Select a room and some users to invite in""" | |
167 | |
168 def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", | |
169 title_room="Join room", title_invite="Invite contacts", visible=(True, True)): | |
170 DialogBox.__init__(self, centered=True) | |
171 self.host = host | |
172 self.callback = callback | |
173 self.title_room = title_room | |
174 self.title_invite = title_invite | |
175 | |
176 button_panel = HorizontalPanel() | |
177 button_panel.addStyleName("marginAuto") | |
178 ok_button = Button("OK", self.onOK) | |
179 button_panel.add(ok_button) | |
180 button_panel.add(Button("Cancel", self.onCancel)) | |
181 | |
182 self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) | |
183 self.contact_panel = ContactsChooser(host, nb_contact, ok_button) | |
184 | |
185 self.stack_panel = base_panels.ToggleStackPanel(Width="100%") | |
186 self.stack_panel.add(self.room_panel, visible=visible[0]) | |
187 self.stack_panel.add(self.contact_panel, visible=visible[1]) | |
188 self.stack_panel.addStackChangeListener(self) | |
189 self.onStackChanged(self.stack_panel, 0, visible[0]) | |
190 self.onStackChanged(self.stack_panel, 1, visible[1]) | |
191 | |
192 main_panel = VerticalPanel() | |
193 main_panel.setStyleName("room-contact-chooser") | |
194 main_panel.add(self.stack_panel) | |
195 main_panel.add(button_panel) | |
196 | |
197 self.setWidget(main_panel) | |
198 self.setHTML(title) | |
199 self.show() | |
200 | |
201 # needed to update the contacts list when someone logged in/out | |
202 self.host.room_contacts_chooser = self | |
203 | |
204 def getRoom(self, asSuffix=False): | |
205 room = self.room_panel.getRoom() | |
206 if asSuffix: | |
207 return room if room == "" else ": %s" % room | |
208 else: | |
209 return room | |
210 | |
211 def getContacts(self, asSuffix=False): | |
212 contacts = self.contact_panel.getContacts() | |
213 if asSuffix: | |
214 return "" if contacts == [] else ": %s" % ", ".join(contacts) | |
215 else: | |
216 return contacts | |
217 | |
218 def onStackChanged(self, sender, index, visible=None): | |
219 if visible is None: | |
220 visible = sender.getWidget(index).getVisible() | |
221 if index == 0: | |
222 sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True))) | |
223 elif index == 1: | |
224 sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True))) | |
225 | |
226 def resetContacts(self): | |
227 """Called when someone log in/out to update the list""" | |
228 self.contact_panel.setContacts(self.getContacts()) | |
229 | |
230 def onOK(self, sender): | |
231 room_jid = self.getRoom() | |
232 if room_jid != "" and "@" not in room_jid: | |
233 Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain) | |
234 return | |
235 self.hide() | |
236 self.callback(room_jid, self.getContacts()) | |
237 | |
238 def onCancel(self, sender): | |
239 self.hide() | |
240 | |
241 def hide(self): | |
242 self.host.room_contacts_chooser = None | |
243 DialogBox.hide(self, autoClosed=True) | |
244 | |
245 | |
246 class GenericConfirmDialog(DialogBox): | |
247 | |
248 def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs): | |
249 """ | |
250 Dialog to confirm an action | |
251 @param widgets: widgets to attach | |
252 @param callback: method to call when a button is clicked | |
253 @param title: title of the dialog | |
254 @param prompt: textbox from which to retrieve the string value to be passed to the callback when | |
255 OK button is pressed. If None, OK button will return "True". Cancel button always returns "False". | |
256 """ | |
257 self.callback = callback | |
258 DialogBox.__init__(self, centered=True, **kwargs) | |
259 | |
260 content = VerticalPanel() | |
261 content.setWidth('100%') | |
262 for wid in widgets: | |
263 content.add(wid) | |
264 if wid == prompt: | |
265 wid.setWidth('100%') | |
266 button_panel = HorizontalPanel() | |
267 button_panel.addStyleName("marginAuto") | |
268 self.confirm_button = Button("OK", self.onConfirm) | |
269 button_panel.add(self.confirm_button) | |
270 self.cancel_button = Button("Cancel", self.onCancel) | |
271 button_panel.add(self.cancel_button) | |
272 content.add(button_panel) | |
273 self.setHTML(title) | |
274 self.setWidget(content) | |
275 self.prompt = prompt | |
276 | |
277 def onConfirm(self, sender): | |
278 self.hide() | |
279 self.callback(self.prompt.getText() if self.prompt else True) | |
280 | |
281 def onCancel(self, sender): | |
282 self.hide() | |
283 self.callback(False) | |
284 | |
285 def show(self): | |
286 DialogBox.show(self) | |
287 if self.prompt: | |
288 self.prompt.setFocus(True) | |
289 | |
290 | |
291 class ConfirmDialog(GenericConfirmDialog): | |
292 | |
293 def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): | |
294 GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) | |
295 | |
296 | |
297 class GenericDialog(DialogBox): | |
298 """Dialog which just show a widget and a close button""" | |
299 | |
300 def __init__(self, title, main_widget, callback=None, options=None, **kwargs): | |
301 """Simple notice dialog box | |
302 @param title: HTML put in the header | |
303 @param main_widget: widget put in the body | |
304 @param callback: method to call on closing | |
305 @param options: one or more of the following options: | |
306 - NO_CLOSE: don't add a close button""" | |
307 DialogBox.__init__(self, centered=True, **kwargs) | |
308 self.callback = callback | |
309 if not options: | |
310 options = [] | |
311 _body = VerticalPanel() | |
312 _body.setSize('100%', '100%') | |
313 _body.setSpacing(4) | |
314 _body.add(main_widget) | |
315 _body.setCellWidth(main_widget, '100%') | |
316 _body.setCellHeight(main_widget, '100%') | |
317 if not 'NO_CLOSE' in options: | |
318 _close_button = Button("Close", self.onClose) | |
319 _body.add(_close_button) | |
320 _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) | |
321 self.setHTML(title) | |
322 self.setWidget(_body) | |
323 self.panel.setSize('100%', '100%') # Need this hack to have correct size in Gecko & Webkit | |
324 | |
325 def close(self): | |
326 """Same effect as clicking the close button""" | |
327 self.onClose(None) | |
328 | |
329 def onClose(self, sender): | |
330 self.hide() | |
331 if self.callback: | |
332 self.callback() | |
333 | |
334 | |
335 class InfoDialog(GenericDialog): | |
336 | |
337 def __init__(self, title, body, callback=None, options=None, **kwargs): | |
338 GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) | |
339 | |
340 | |
341 class PromptDialog(GenericConfirmDialog): | |
342 | |
343 def __init__(self, callback, text='', title='User input', **kwargs): | |
344 prompt = TextBox() | |
345 prompt.setText(text) | |
346 GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs) | |
347 | |
348 | |
349 class PopupPanelWrapper(PopupPanel): | |
350 """This wrapper catch Escape event to avoid request cancellation by Firefox""" | |
351 | |
352 def onEventPreview(self, event): | |
353 if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: | |
354 #needed to prevent request cancellation in Firefox | |
355 event.preventDefault() | |
356 return PopupPanel.onEventPreview(self, event) | |
357 | |
358 | |
359 class ExtTextBox(TextBox): | |
360 """Extended TextBox""" | |
361 | |
362 def __init__(self, *args, **kwargs): | |
363 if 'enter_cb' in kwargs: | |
364 self.enter_cb = kwargs['enter_cb'] | |
365 del kwargs['enter_cb'] | |
366 TextBox.__init__(self, *args, **kwargs) | |
367 self.addKeyboardListener(self) | |
368 | |
369 def onKeyUp(self, sender, keycode, modifiers): | |
370 pass | |
371 | |
372 def onKeyDown(self, sender, keycode, modifiers): | |
373 pass | |
374 | |
375 def onKeyPress(self, sender, keycode, modifiers): | |
376 if self.enter_cb and keycode == KEY_ENTER: | |
377 self.enter_cb(self) | |
378 | |
379 | |
380 class GroupSelector(DialogBox): | |
381 | |
382 def __init__(self, top_widgets, initial_groups, selected_groups, | |
383 ok_title="OK", ok_cb=None, cancel_cb=None): | |
384 DialogBox.__init__(self, centered=True) | |
385 main_panel = VerticalPanel() | |
386 self.ok_cb = ok_cb | |
387 self.cancel_cb = cancel_cb | |
388 | |
389 for wid in top_widgets: | |
390 main_panel.add(wid) | |
391 | |
392 main_panel.add(Label('Select in which groups your contact is:')) | |
393 self.list_box = ListBox() | |
394 self.list_box.setMultipleSelect(True) | |
395 self.list_box.setVisibleItemCount(5) | |
396 self.setAvailableGroups(initial_groups) | |
397 self.setGroupsSelected(selected_groups) | |
398 main_panel.add(self.list_box) | |
399 | |
400 def cb(text): | |
401 self.list_box.addItem(text) | |
402 self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") | |
403 | |
404 main_panel.add(AddGroupPanel(initial_groups, cb)) | |
405 | |
406 button_panel = HorizontalPanel() | |
407 button_panel.addStyleName("marginAuto") | |
408 button_panel.add(Button(ok_title, self.onOK)) | |
409 button_panel.add(Button("Cancel", self.onCancel)) | |
410 main_panel.add(button_panel) | |
411 | |
412 self.setWidget(main_panel) | |
413 | |
414 def getSelectedGroups(self): | |
415 """Return a list of selected groups""" | |
416 return self.list_box.getSelectedValues() | |
417 | |
418 def setAvailableGroups(self, groups): | |
419 _groups = list(set(groups)) | |
420 _groups.sort() | |
421 self.list_box.clear() | |
422 for group in _groups: | |
423 self.list_box.addItem(group) | |
424 | |
425 def setGroupsSelected(self, selected_groups): | |
426 self.list_box.setItemTextSelection(selected_groups) | |
427 | |
428 def onOK(self, sender): | |
429 self.hide() | |
430 if self.ok_cb: | |
431 self.ok_cb(self) | |
432 | |
433 def onCancel(self, sender): | |
434 self.hide() | |
435 if self.cancel_cb: | |
436 self.cancel_cb(self) | |
437 | |
438 | |
439 class AddGroupPanel(HorizontalPanel): | |
440 def __init__(self, groups, cb=None): | |
441 """ | |
442 @param groups: list of the already existing groups | |
443 """ | |
444 HorizontalPanel.__init__(self) | |
445 self.groups = groups | |
446 self.add(Label('Add group:')) | |
447 self.textbox = ExtTextBox(enter_cb=self.onGroupInput) | |
448 self.add(self.textbox) | |
449 self.add(Button("add", lambda sender: self.onGroupInput(self.textbox))) | |
450 self.cb = cb | |
451 | |
452 def onGroupInput(self, sender): | |
453 text = sender.getText() | |
454 if text == "": | |
455 return | |
456 for group in self.groups: | |
457 if text == group: | |
458 Window.alert("The group '%s' already exists." % text) | |
459 return | |
460 for pattern in FORBIDDEN_PATTERNS_IN_GROUP: | |
461 if pattern in text: | |
462 Window.alert("The pattern '%s' is not allowed in group names." % pattern) | |
463 return | |
464 sender.setText('') | |
465 self.groups.append(text) | |
466 if self.cb is not None: | |
467 self.cb(text) | |
468 | |
469 | |
470 class WheelTextBox(TextBox, MouseWheelHandler): | |
471 | |
472 def __init__(self, *args, **kwargs): | |
473 TextBox.__init__(self, *args, **kwargs) | |
474 MouseWheelHandler.__init__(self) | |
475 | |
476 | |
477 class IntSetter(HorizontalPanel): | |
478 """This class show a bar with button to set an int value""" | |
479 | |
480 def __init__(self, label, value=0, value_max=None, visible_len=3): | |
481 """initialize the intSetter | |
482 @param label: text shown in front of the setter | |
483 @param value: initial value | |
484 @param value_max: limit value, None or 0 for unlimited""" | |
485 HorizontalPanel.__init__(self) | |
486 self.value = value | |
487 self.value_max = value_max | |
488 _label = Label(label) | |
489 self.add(_label) | |
490 self.setCellWidth(_label, "100%") | |
491 minus_button = Button("-", self.onMinus) | |
492 self.box = WheelTextBox() | |
493 self.box.setVisibleLength(visible_len) | |
494 self.box.setText(str(value)) | |
495 self.box.addInputListener(self) | |
496 self.box.addMouseWheelListener(self) | |
497 plus_button = Button("+", self.onPlus) | |
498 self.add(minus_button) | |
499 self.add(self.box) | |
500 self.add(plus_button) | |
501 self.valueChangedListener = [] | |
502 | |
503 def addValueChangeListener(self, listener): | |
504 self.valueChangedListener.append(listener) | |
505 | |
506 def removeValueChangeListener(self, listener): | |
507 if listener in self.valueChangedListener: | |
508 self.valueChangedListener.remove(listener) | |
509 | |
510 def _callListeners(self): | |
511 for listener in self.valueChangedListener: | |
512 listener(self.value) | |
513 | |
514 def setValue(self, value): | |
515 """Change the value and fire valueChange listeners""" | |
516 self.value = value | |
517 self.box.setText(str(value)) | |
518 self._callListeners() | |
519 | |
520 def onMinus(self, sender, step=1): | |
521 self.value = max(0, self.value - step) | |
522 self.box.setText(str(self.value)) | |
523 self._callListeners() | |
524 | |
525 def onPlus(self, sender, step=1): | |
526 self.value += step | |
527 if self.value_max: | |
528 self.value = min(self.value, self.value_max) | |
529 self.box.setText(str(self.value)) | |
530 self._callListeners() | |
531 | |
532 def onInput(self, sender): | |
533 """Accept only valid integer && normalize print (no leading 0)""" | |
534 try: | |
535 self.value = int(self.box.getText()) if self.box.getText() else 0 | |
536 except ValueError: | |
537 pass | |
538 if self.value_max: | |
539 self.value = min(self.value, self.value_max) | |
540 self.box.setText(str(self.value)) | |
541 self._callListeners() | |
542 | |
543 def onMouseWheel(self, sender, velocity): | |
544 if velocity > 0: | |
545 self.onMinus(sender, 10) | |
546 else: | |
547 self.onPlus(sender, 10) |