Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/dialog.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,547 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools.misc import DEFAULT_MUC + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.Grid import Grid +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.Button import Button +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.RadioButton import RadioButton +from pyjamas.ui import HasAlignment +from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER +from pyjamas.ui.MouseListener import MouseWheelHandler +from pyjamas import Window + +import base_panels + + +# List here the patterns that are not allowed in contact group names +FORBIDDEN_PATTERNS_IN_GROUP = () + + +class RoomChooser(Grid): + """Select a room from the rooms you already joined, or create a new one""" + + GENERATE_MUC = "<use random name>" + + def __init__(self, host, default_room=DEFAULT_MUC): + Grid.__init__(self, 2, 2, Width='100%') + self.host = host + + self.new_radio = RadioButton("room", "Discussion room:") + self.new_radio.setChecked(True) + self.box = TextBox(Width='95%') + self.box.setText(self.GENERATE_MUC if default_room == "" else default_room) + self.exist_radio = RadioButton("room", "Already joined:") + self.rooms_list = ListBox(Width='95%') + + self.add(self.new_radio, 0, 0) + self.add(self.box, 0, 1) + self.add(self.exist_radio, 1, 0) + self.add(self.rooms_list, 1, 1) + + self.box.addFocusListener(self) + self.rooms_list.addFocusListener(self) + + self.exist_radio.setVisible(False) + self.rooms_list.setVisible(False) + self.setRooms() + + def onFocus(self, sender): + if sender == self.rooms_list: + self.exist_radio.setChecked(True) + elif sender == self.box: + if self.box.getText() == self.GENERATE_MUC: + self.box.setText("") + self.new_radio.setChecked(True) + + def onLostFocus(self, sender): + if sender == self.box: + if self.box.getText() == "": + self.box.setText(self.GENERATE_MUC) + + def setRooms(self): + for room in self.host.room_list: + self.rooms_list.addItem(room.bare) + if len(self.host.room_list) > 0: + self.exist_radio.setVisible(True) + self.rooms_list.setVisible(True) + self.exist_radio.setChecked(True) + + def getRoom(self): + if self.exist_radio.getChecked(): + values = self.rooms_list.getSelectedValues() + return "" if values == [] else values[0] + value = self.box.getText() + return "" if value == self.GENERATE_MUC else value + + +class ContactsChooser(VerticalPanel): + """Select one or several connected contacts""" + + def __init__(self, host, nb_contact=None, ok_button=None): + """ + @param host: SatWebFrontend instance + @param nb_contact: number of contacts that have to be selected, None for no limit + If a tuple is given instead of an integer, nb_contact[0] is the minimal and + nb_contact[1] is the maximal number of contacts to be chosen. + """ + self.host = host + if isinstance(nb_contact, tuple): + if len(nb_contact) == 0: + nb_contact = None + elif len(nb_contact) == 1: + nb_contact = (nb_contact[0], nb_contact[0]) + elif nb_contact is not None: + nb_contact = (nb_contact, nb_contact) + if nb_contact is None: + log.warning("Need to select as many contacts as you want") + else: + log.warning("Need to select between %d and %d contacts" % nb_contact) + self.nb_contact = nb_contact + self.ok_button = ok_button + VerticalPanel.__init__(self, Width='100%') + self.contacts_list = ListBox() + self.contacts_list.setMultipleSelect(True) + self.contacts_list.setWidth("95%") + self.contacts_list.addStyleName('contactsChooser') + self.contacts_list.addChangeListener(self.onChange) + self.add(self.contacts_list) + self.setContacts() + self.onChange() + + def onChange(self, sender=None): + if self.ok_button is None: + return + if self.nb_contact: + selected = len(self.contacts_list.getSelectedValues(True)) + if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: + self.ok_button.setEnabled(True) + else: + self.ok_button.setEnabled(False) + + def setContacts(self, selected=[]): + """Fill the list with the connected contacts + @param select: list of the contacts to select by default + """ + self.contacts_list.clear() + contacts = self.host.contact_panel.getConnected(filter_muc=True) + self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) + self.contacts_list.addItem("") + for contact in contacts: + if contact not in [room.bare for room in self.host.room_list]: + self.contacts_list.addItem(contact) + self.contacts_list.setItemTextSelection(selected) + + def getContacts(self): + return self.contacts_list.getSelectedValues(True) + + +class RoomAndContactsChooser(DialogBox): + """Select a room and some users to invite in""" + + def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", + title_room="Join room", title_invite="Invite contacts", visible=(True, True)): + DialogBox.__init__(self, centered=True) + self.host = host + self.callback = callback + self.title_room = title_room + self.title_invite = title_invite + + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + ok_button = Button("OK", self.onOK) + button_panel.add(ok_button) + button_panel.add(Button("Cancel", self.onCancel)) + + self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) + self.contact_panel = ContactsChooser(host, nb_contact, ok_button) + + self.stack_panel = base_panels.ToggleStackPanel(Width="100%") + self.stack_panel.add(self.room_panel, visible=visible[0]) + self.stack_panel.add(self.contact_panel, visible=visible[1]) + self.stack_panel.addStackChangeListener(self) + self.onStackChanged(self.stack_panel, 0, visible[0]) + self.onStackChanged(self.stack_panel, 1, visible[1]) + + main_panel = VerticalPanel() + main_panel.setStyleName("room-contact-chooser") + main_panel.add(self.stack_panel) + main_panel.add(button_panel) + + self.setWidget(main_panel) + self.setHTML(title) + self.show() + + # needed to update the contacts list when someone logged in/out + self.host.room_contacts_chooser = self + + def getRoom(self, asSuffix=False): + room = self.room_panel.getRoom() + if asSuffix: + return room if room == "" else ": %s" % room + else: + return room + + def getContacts(self, asSuffix=False): + contacts = self.contact_panel.getContacts() + if asSuffix: + return "" if contacts == [] else ": %s" % ", ".join(contacts) + else: + return contacts + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + if index == 0: + sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True))) + elif index == 1: + sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True))) + + def resetContacts(self): + """Called when someone log in/out to update the list""" + self.contact_panel.setContacts(self.getContacts()) + + def onOK(self, sender): + room_jid = self.getRoom() + if room_jid != "" and "@" not in room_jid: + Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain) + return + self.hide() + self.callback(room_jid, self.getContacts()) + + def onCancel(self, sender): + self.hide() + + def hide(self): + self.host.room_contacts_chooser = None + DialogBox.hide(self, autoClosed=True) + + +class GenericConfirmDialog(DialogBox): + + def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs): + """ + Dialog to confirm an action + @param widgets: widgets to attach + @param callback: method to call when a button is clicked + @param title: title of the dialog + @param prompt: textbox from which to retrieve the string value to be passed to the callback when + OK button is pressed. If None, OK button will return "True". Cancel button always returns "False". + """ + self.callback = callback + DialogBox.__init__(self, centered=True, **kwargs) + + content = VerticalPanel() + content.setWidth('100%') + for wid in widgets: + content.add(wid) + if wid == prompt: + wid.setWidth('100%') + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + self.confirm_button = Button("OK", self.onConfirm) + button_panel.add(self.confirm_button) + self.cancel_button = Button("Cancel", self.onCancel) + button_panel.add(self.cancel_button) + content.add(button_panel) + self.setHTML(title) + self.setWidget(content) + self.prompt = prompt + + def onConfirm(self, sender): + self.hide() + self.callback(self.prompt.getText() if self.prompt else True) + + def onCancel(self, sender): + self.hide() + self.callback(False) + + def show(self): + DialogBox.show(self) + if self.prompt: + self.prompt.setFocus(True) + + +class ConfirmDialog(GenericConfirmDialog): + + def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): + GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) + + +class GenericDialog(DialogBox): + """Dialog which just show a widget and a close button""" + + def __init__(self, title, main_widget, callback=None, options=None, **kwargs): + """Simple notice dialog box + @param title: HTML put in the header + @param main_widget: widget put in the body + @param callback: method to call on closing + @param options: one or more of the following options: + - NO_CLOSE: don't add a close button""" + DialogBox.__init__(self, centered=True, **kwargs) + self.callback = callback + if not options: + options = [] + _body = VerticalPanel() + _body.setSize('100%', '100%') + _body.setSpacing(4) + _body.add(main_widget) + _body.setCellWidth(main_widget, '100%') + _body.setCellHeight(main_widget, '100%') + if not 'NO_CLOSE' in options: + _close_button = Button("Close", self.onClose) + _body.add(_close_button) + _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) + self.setHTML(title) + self.setWidget(_body) + self.panel.setSize('100%', '100%') # Need this hack to have correct size in Gecko & Webkit + + def close(self): + """Same effect as clicking the close button""" + self.onClose(None) + + def onClose(self, sender): + self.hide() + if self.callback: + self.callback() + + +class InfoDialog(GenericDialog): + + def __init__(self, title, body, callback=None, options=None, **kwargs): + GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) + + +class PromptDialog(GenericConfirmDialog): + + def __init__(self, callback, text='', title='User input', **kwargs): + prompt = TextBox() + prompt.setText(text) + GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs) + + +class PopupPanelWrapper(PopupPanel): + """This wrapper catch Escape event to avoid request cancellation by Firefox""" + + def onEventPreview(self, event): + if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: + #needed to prevent request cancellation in Firefox + event.preventDefault() + return PopupPanel.onEventPreview(self, event) + + +class ExtTextBox(TextBox): + """Extended TextBox""" + + def __init__(self, *args, **kwargs): + if 'enter_cb' in kwargs: + self.enter_cb = kwargs['enter_cb'] + del kwargs['enter_cb'] + TextBox.__init__(self, *args, **kwargs) + self.addKeyboardListener(self) + + def onKeyUp(self, sender, keycode, modifiers): + pass + + def onKeyDown(self, sender, keycode, modifiers): + pass + + def onKeyPress(self, sender, keycode, modifiers): + if self.enter_cb and keycode == KEY_ENTER: + self.enter_cb(self) + + +class GroupSelector(DialogBox): + + def __init__(self, top_widgets, initial_groups, selected_groups, + ok_title="OK", ok_cb=None, cancel_cb=None): + DialogBox.__init__(self, centered=True) + main_panel = VerticalPanel() + self.ok_cb = ok_cb + self.cancel_cb = cancel_cb + + for wid in top_widgets: + main_panel.add(wid) + + main_panel.add(Label('Select in which groups your contact is:')) + self.list_box = ListBox() + self.list_box.setMultipleSelect(True) + self.list_box.setVisibleItemCount(5) + self.setAvailableGroups(initial_groups) + self.setGroupsSelected(selected_groups) + main_panel.add(self.list_box) + + def cb(text): + self.list_box.addItem(text) + self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") + + main_panel.add(AddGroupPanel(initial_groups, cb)) + + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + button_panel.add(Button(ok_title, self.onOK)) + button_panel.add(Button("Cancel", self.onCancel)) + main_panel.add(button_panel) + + self.setWidget(main_panel) + + def getSelectedGroups(self): + """Return a list of selected groups""" + return self.list_box.getSelectedValues() + + def setAvailableGroups(self, groups): + _groups = list(set(groups)) + _groups.sort() + self.list_box.clear() + for group in _groups: + self.list_box.addItem(group) + + def setGroupsSelected(self, selected_groups): + self.list_box.setItemTextSelection(selected_groups) + + def onOK(self, sender): + self.hide() + if self.ok_cb: + self.ok_cb(self) + + def onCancel(self, sender): + self.hide() + if self.cancel_cb: + self.cancel_cb(self) + + +class AddGroupPanel(HorizontalPanel): + def __init__(self, groups, cb=None): + """ + @param groups: list of the already existing groups + """ + HorizontalPanel.__init__(self) + self.groups = groups + self.add(Label('Add group:')) + self.textbox = ExtTextBox(enter_cb=self.onGroupInput) + self.add(self.textbox) + self.add(Button("add", lambda sender: self.onGroupInput(self.textbox))) + self.cb = cb + + def onGroupInput(self, sender): + text = sender.getText() + if text == "": + return + for group in self.groups: + if text == group: + Window.alert("The group '%s' already exists." % text) + return + for pattern in FORBIDDEN_PATTERNS_IN_GROUP: + if pattern in text: + Window.alert("The pattern '%s' is not allowed in group names." % pattern) + return + sender.setText('') + self.groups.append(text) + if self.cb is not None: + self.cb(text) + + +class WheelTextBox(TextBox, MouseWheelHandler): + + def __init__(self, *args, **kwargs): + TextBox.__init__(self, *args, **kwargs) + MouseWheelHandler.__init__(self) + + +class IntSetter(HorizontalPanel): + """This class show a bar with button to set an int value""" + + def __init__(self, label, value=0, value_max=None, visible_len=3): + """initialize the intSetter + @param label: text shown in front of the setter + @param value: initial value + @param value_max: limit value, None or 0 for unlimited""" + HorizontalPanel.__init__(self) + self.value = value + self.value_max = value_max + _label = Label(label) + self.add(_label) + self.setCellWidth(_label, "100%") + minus_button = Button("-", self.onMinus) + self.box = WheelTextBox() + self.box.setVisibleLength(visible_len) + self.box.setText(str(value)) + self.box.addInputListener(self) + self.box.addMouseWheelListener(self) + plus_button = Button("+", self.onPlus) + self.add(minus_button) + self.add(self.box) + self.add(plus_button) + self.valueChangedListener = [] + + def addValueChangeListener(self, listener): + self.valueChangedListener.append(listener) + + def removeValueChangeListener(self, listener): + if listener in self.valueChangedListener: + self.valueChangedListener.remove(listener) + + def _callListeners(self): + for listener in self.valueChangedListener: + listener(self.value) + + def setValue(self, value): + """Change the value and fire valueChange listeners""" + self.value = value + self.box.setText(str(value)) + self._callListeners() + + def onMinus(self, sender, step=1): + self.value = max(0, self.value - step) + self.box.setText(str(self.value)) + self._callListeners() + + def onPlus(self, sender, step=1): + self.value += step + if self.value_max: + self.value = min(self.value, self.value_max) + self.box.setText(str(self.value)) + self._callListeners() + + def onInput(self, sender): + """Accept only valid integer && normalize print (no leading 0)""" + try: + self.value = int(self.box.getText()) if self.box.getText() else 0 + except ValueError: + pass + if self.value_max: + self.value = min(self.value, self.value_max) + self.box.setText(str(self.value)) + self._callListeners() + + def onMouseWheel(self, sender, velocity): + if velocity > 0: + self.onMinus(sender, 10) + else: + self.onPlus(sender, 10)