view src/browser/sat_browser/dialog.py @ 595:d78126d82ca0 frontends_multi_profiles

browser side (blog module): fixed isJidAccepted + added __str__ method to facilitate debugging + use of AttributeError and TypeError in some exception (because pyjamas can raise both depending on compilation options)
author Goffi <goffi@goffi.org>
date Fri, 06 Feb 2015 19:15:52 +0100
parents d41e850b31b9
children ed9cd20260ff
line wrap: on
line source

#!/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 (list[Widget]): widgets to attach
        @param callback: method to call when a button is clicked
        @param title: title of the dialog
        @param prompt (TextBox, list[TextBox]): input widgets from which to retrieve
        the string value(s) 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
        added_style = kwargs.pop('AddStyleName', None)
        DialogBox.__init__(self, centered=True, **kwargs)
        if added_style:
            self.addStyleName(added_style)

        if prompt is None:
            prompt = []
        elif isinstance(prompt, TextBox):
            prompt = [prompt]

        content = VerticalPanel()
        content.setWidth('100%')
        for wid in widgets:
            content.add(wid)
            if wid in 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()
        result = [box.getText() for box in self.prompt] if self.prompt else [True]
        self.callback(*result)

    def onCancel(self, sender):
        self.hide()
        self.callback(False)

    def show(self):
        DialogBox.show(self)
        if self.prompt:
            self.prompt[0].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"""
        added_style = kwargs.pop('AddStyleName', None)
        DialogBox.__init__(self, centered=True, **kwargs)
        if added_style:
            self.addStyleName(added_style)

        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, textes=None, values=None, title='User input', **kwargs):
        """Prompt the user for one or more input(s).

        @param callback (callable): method to call when clicking OK
        @param textes (str, list[str]): HTML textes to display before the inputs
        @param values (str, list[str]): default values for each input
        @param title (str): dialog title
        """
        if textes is None:
            textes = ['']  # display a single input without any description
        elif not isinstance(textes, list):
            textes = [textes]  # allow to pass a single string instead of a list
        if values is None:
            values = []
        elif not isinstance(values, list):
            values = [values]  # allow to pass a single string instead of a list
        all_widgets = []
        prompt_widgets = []
        for count in xrange(len(textes)):
            all_widgets.append(HTML(textes[count]))
            prompt = TextBox()
            if len(values) > count:
                prompt.setText(values[count])
            all_widgets.append(prompt)
            prompt_widgets.append(prompt)

        GenericConfirmDialog.__init__(self, all_widgets, callback, title, prompt_widgets, **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)