diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser/sat_browser/dialog.py	Sat Aug 25 17:59:48 2018 +0200
@@ -0,0 +1,616 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011-2018 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 constants import Const as C
+from sat_frontends.tools import jid
+
+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_panel
+
+
+# List here the patterns that are not allowed in contact group names
+FORBIDDEN_PATTERNS_IN_GROUP = ()
+
+
+unicode = str # XXX: pyjama doesn't manage unicode
+
+
+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, room_jid_s=None):
+        """
+
+        @param host (SatWebFrontend)
+        @param room_jid_s (unicode): room JID
+        """
+        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(room_jid_s if room_jid_s else self.GENERATE_MUC)
+        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.refreshOptions()
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
+
+        @return: jid.JID or None to let the backend generate a new name
+        """
+        if self.exist_radio.getChecked():
+            values = self.rooms_list.getSelectedValues()
+            return jid.JID(values[0]) if values else None
+        value = self.box.getText()
+        return None if value in ('', self.GENERATE_MUC) else jid.JID(value)
+
+    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 refreshOptions(self):
+        """Refresh the already joined room list"""
+        contact_list = self.host.contact_list
+        muc_rooms = contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP)
+        for room in muc_rooms:
+            self.rooms_list.addItem(room.bare)
+        if len(muc_rooms) > 0:
+            self.exist_radio.setVisible(True)
+            self.rooms_list.setVisible(True)
+            self.exist_radio.setChecked(True)
+
+
+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.debug("Need to select as many contacts as you want")
+        else:
+            log.debug("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.refreshOptions()
+        self.onChange()
+
+    @property
+    def contacts(self):
+        """Return the selected contacts.
+
+        @return: list[jid.JID]
+        """
+        return [jid.JID(contact) for contact in self.contacts_list.getSelectedValues(True)]
+
+    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 refreshOptions(self, keep_selected=False):
+        """Fill the list with the connected contacts.
+
+        @param keep_selected (boolean): if True, keep the current selection
+        """
+        selection = self.contacts if keep_selected else []
+        self.contacts_list.clear()
+        contacts = self.host.contact_list.roster_connected
+        self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5)
+        self.contacts_list.addItem("")
+        for contact in contacts:
+            self.contacts_list.addItem(contact)
+        if selection:
+            self.contacts_list.setItemTextSelection([unicode(contact) for contact in selection])
+
+
+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, None if visible == (False, True) else host.default_muc)
+        self.contact_panel = ContactsChooser(host, nb_contact, ok_button)
+
+        self.stack_panel = base_panel.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()
+
+        # 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)
+        self.presenceListener = self.refreshContactList
+        # update the contacts list when someone logged in/out
+        self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
+
+    @property
+    def room(self):
+        """Get the room that has been selected or entered by the user
+
+        @return: jid.JID or None
+        """
+        return self.room_panel.room
+
+    @property
+    def contacts(self):
+        """Return the selected contacts.
+
+        @return: list[jid.JID]
+        """
+        return self.contact_panel.contacts
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        if index == 0:
+            suffix = "" if (visible or not self.room) else ": %s" % self.room
+            sender.setStackText(0, self.title_room + suffix)
+        elif index == 1:
+            suffix = "" if (visible or not self.contacts) else ": %s" % ", ".join([unicode(contact) for contact in self.contacts])
+            sender.setStackText(1, self.title_invite + suffix)
+
+    def refreshContactList(self, *args, **kwargs):
+        """Called when someone log in/out to update the list.
+
+        @param args: set by the event call but not used here
+        """
+        self.contact_panel.refreshOptions(keep_selected=True)
+
+    def onOK(self, sender):
+        room = self.room  # pyjamas issue: you need to use an intermediate variable to access a property's method
+        self.hide()
+        self.callback(room, self.contacts)
+
+    def onCancel(self, sender):
+        self.hide()
+
+    def hide(self):
+        self.host.removeListener('presence', self.presenceListener)
+        DialogBox.hide(self, autoClosed=True)
+
+
+class GenericConfirmDialog(DialogBox):
+
+    def __init__(self, widgets, callback, title='Confirmation', prompt_widgets=None, **kwargs):
+        """
+        Dialog to confirm an action
+        @param widgets (list[Widget]): widgets to attach
+        @param callback (callable): method to call when a button is pressed,
+            with the following arguments:
+                - result (bool): set to True if the dialog has been confirmed
+                - *args: a list of unicode (the values for the prompt_widgets)
+        @param title: title of the dialog
+        @param prompt_widgets (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_widgets is None:
+            prompt_widgets = []
+
+        content = VerticalPanel()
+        content.setWidth('100%')
+        for wid in widgets:
+            content.add(wid)
+            if wid in prompt_widgets:
+                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_widgets = prompt_widgets
+
+    def onConfirm(self, sender):
+        self.hide()
+        result = [True]
+        result.extend([box.getText() for box in self.prompt_widgets])
+        self.callback(*result)
+
+    def onCancel(self, sender):
+        self.hide()
+        self.callback(False)
+
+    def show(self):
+        DialogBox.show(self)
+        if self.prompt_widgets:
+            self.prompt_widgets[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 'NO_CLOSE' not 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 a button is pressed,
+            with the following arguments:
+                - result (bool): set to True if the dialog has been confirmed
+                - *args: a list of unicode (the values entered by the user)
+        @param textes (list[unicode]): HTML textes to display before the inputs
+        @param values (list[unicode]): default values for each input
+        @param title (unicode): dialog title
+        """
+        if textes is None:
+            textes = ['']  # display a single input without any description
+        if values is None:
+            values = []
+        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('New 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(unicode(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(unicode(value))
+        self._callListeners()
+
+    def onMinus(self, sender, step=1):
+        self.value = max(0, self.value - step)
+        self.box.setText(unicode(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(unicode(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(unicode(self.value))
+        self._callListeners()
+
+    def onMouseWheel(self, sender, velocity):
+        if velocity > 0:
+            self.onMinus(sender, 10)
+        else:
+            self.onPlus(sender, 10)