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)