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)