diff src/browser/base_panels.py @ 449:981ed669d3b3

/!\ reorganize all the file hierarchy, move the code and launching script to src: - browser_side --> src/browser - public --> src/browser_side/public - libervia.py --> src/browser/libervia_main.py - libervia_server --> src/server - libervia_server/libervia.sh --> src/libervia.sh - twisted --> src/twisted - new module src/common - split constants.py in 3 files: - src/common/constants.py - src/browser/constants.py - src/server/constants.py - output --> html (generated by pyjsbuild during the installation) - new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css - setup.py installs libervia to the following paths: - src/common --> <LIB>/libervia/common - src/server --> <LIB>/libervia/server - src/twisted --> <LIB>/twisted - html --> <SHARE>/libervia/html - server_side --> <SHARE>libervia/server_side - LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation: - clean: remove previous installation directories - purge: remove building and previous installation directories You may need to update your sat.conf and/or launching script to update the following options/parameters: - ssl_certificate - data_dir
author souliane <souliane@mailoo.org>
date Tue, 20 May 2014 06:41:16 +0200
parents browser_side/base_panels.py@d52f529a6d42
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/base_panels.py	Tue May 20 06:41:16 2014 +0200
@@ -0,0 +1,609 @@
+#!/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/>.
+
+import pyjd  # this is dummy in pyjs
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat.core.i18n import _
+from sat_frontends.tools.strings import addURLToText, addURLToImage
+
+from pyjamas.ui.AbsolutePanel import AbsolutePanel
+from pyjamas.ui.VerticalPanel import VerticalPanel
+from pyjamas.ui.HorizontalPanel import HorizontalPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Button import Button
+from pyjamas.ui.HTML import HTML
+from pyjamas.ui.SimplePanel import SimplePanel
+from pyjamas.ui.PopupPanel import PopupPanel
+from pyjamas.ui.StackPanel import StackPanel
+from pyjamas.ui.TextArea import TextArea
+from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
+from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler
+from pyjamas.ui.FocusListener import FocusHandler
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas import DOM
+
+from datetime import datetime
+from time import time
+
+from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML
+from constants import Const as C
+
+
+class ChatText(HTMLPanel):
+
+    def __init__(self, timestamp, nick, mymess, msg, xhtml=None):
+        _date = datetime.fromtimestamp(float(timestamp or time()))
+        _msg_class = ["chat_text_msg"]
+        if mymess:
+            _msg_class.append("chat_text_mymess")
+        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
+                           {"timestamp": _date.strftime("%H:%M"),
+                            "nick": "[%s]" % html_sanitize(nick),
+                            "msg_class": ' '.join(_msg_class),
+                            "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)}  # FIXME: images and external links must be removed according to preferences
+                           )
+        self.setStyleName('chatText')
+
+
+class Occupant(HTML):
+    """Occupant of a MUC room"""
+
+    def __init__(self, nick, state=None, special=""):
+        """
+        @param nick: the user nickname
+        @param state: the user chate state (XEP-0085)
+        @param special: a string of symbols (e.g: for activities)
+        """
+        HTML.__init__(self)
+        self.nick = nick
+        self._state = state
+        self.special = special
+        self._refresh()
+
+    def __str__(self):
+        return self.nick
+
+    def setState(self, state):
+        self._state = state
+        self._refresh()
+
+    def addSpecial(self, special):
+        """@param special: unicode"""
+        if special not in self.special:
+            self.special += special
+            self._refresh()
+
+    def removeSpecials(self, special):
+        """@param special: unicode or list"""
+        if not isinstance(special, list):
+            special = [special]
+        for symbol in special:
+            self.special = self.special.replace(symbol, "")
+            self._refresh()
+
+    def _refresh(self):
+        state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else ''
+        special = "" if len(self.special) == 0 else " %s" % self.special
+        self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state))
+
+
+class OccupantsList(AbsolutePanel):
+    """Panel user to show occupants of a room"""
+
+    def __init__(self):
+        AbsolutePanel.__init__(self)
+        self.occupants_list = {}
+        self.setStyleName('occupantsList')
+
+    def addOccupant(self, nick):
+        _occupant = Occupant(nick)
+        self.occupants_list[nick] = _occupant
+        self.add(_occupant)
+
+    def removeOccupant(self, nick):
+        try:
+            self.remove(self.occupants_list[nick])
+        except KeyError:
+            log.error("trying to remove an unexisting nick")
+
+    def clear(self):
+        self.occupants_list.clear()
+        AbsolutePanel.clear(self)
+
+    def updateSpecials(self, occupants=[], html=""):
+        """Set the specified html "symbol" to the listed occupants,
+        and eventually remove it from the others (if they got it).
+        This is used for example to visualize who is playing a game.
+        @param occupants: list of the occupants that need the symbol
+        @param html: unicode symbol (actually one character or more)
+        or a list to assign different symbols of the same family.
+        """
+        index = 0
+        special = html
+        for occupant in self.occupants_list.keys():
+            if occupant in occupants:
+                if isinstance(html, list):
+                    special = html[index]
+                    index = (index + 1) % len(html)
+                self.occupants_list[occupant].addSpecial(special)
+            else:
+                self.occupants_list[occupant].removeSpecials(html)
+
+
+class PopupMenuPanel(PopupPanel):
+    """This implementation of a popup menu (context menu) allow you to assign
+    two special methods which are common to all the items, in order to hide
+    certain items and also easily define their callbacks. The menu can be
+    bound to any of the mouse button (left, middle, right).
+    """
+    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
+        """
+        @param entries: a dict of dicts, where each sub-dict is representing
+        one menu item: the sub-dict key can be used as the item text and
+        description, but optional "title" and "desc" entries would be used
+        if they exists. The sub-dicts may be extended later to do
+        more complicated stuff or overwrite the common methods.
+        @param hide: function  with 2 args: widget, key as string and
+        returns True if that item should be hidden from the context menu.
+        @param callback: function with 2 args: sender, key as string
+        @param vertical: True or False, to set the direction
+        @param item_style: alternative CSS class for the menu items
+        @param menu_style: supplementary CSS class for the sender widget
+        """
+        PopupPanel.__init__(self, autoHide=True, **kwargs)
+        self._entries = entries
+        self._hide = hide
+        self._callback = callback
+        self.vertical = vertical
+        self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._senders = {}
+
+    def _show(self, sender):
+        """Popup the menu relative to this sender's position.
+        @param sender: the widget that has been clicked
+        """
+        menu = VerticalPanel() if self.vertical is True else HorizontalPanel()
+        menu.setStyleName(self.style["menu"])
+
+        def button_cb(item):
+            """You can not put that method in the loop and rely
+            on _key, because it is overwritten by each step.
+            You can rely on item.key instead, which is copied
+            from _key after the item creation.
+            @param item: the menu item that has been clicked
+            """
+            if self._callback is not None:
+                self._callback(sender=sender, key=item.key)
+            self.hide(autoClosed=True)
+
+        for _key in self._entries.keys():
+            entry = self._entries[_key]
+            if self._hide is not None and self._hide(sender=sender, key=_key) is True:
+                continue
+            title = entry["title"] if "title" in entry.keys() else _key
+            item = Button(title, button_cb)
+            item.key = _key
+            item.setStyleName(self.style["item"])
+            item.setTitle(entry["desc"] if "desc" in entry.keys() else title)
+            menu.add(item)
+        if len(menu.getChildren()) == 0:
+            return
+        self.add(menu)
+        if self.vertical is True:
+            x = sender.getAbsoluteLeft() + sender.getOffsetWidth()
+            y = sender.getAbsoluteTop()
+        else:
+            x = sender.getAbsoluteLeft()
+            y = sender.getAbsoluteTop() + sender.getOffsetHeight()
+        self.setPopupPosition(x, y)
+        self.show()
+        if self.style["selected"]:
+            sender.addStyleDependentName(self.style["selected"])
+
+        def _onHide(popup):
+            if self.style["selected"]:
+                sender.removeStyleDependentName(self.style["selected"])
+            return PopupPanel.onHideImpl(self, popup)
+
+        self.onHideImpl = _onHide
+
+    def registerClickSender(self, sender, button=BUTTON_LEFT):
+        """Bind the menu to the specified sender.
+        @param sender: the widget to which the menu should be bound
+        @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT
+        """
+        self._senders.setdefault(sender, [])
+        self._senders[sender].append(button)
+
+        if button == BUTTON_RIGHT:
+            # WARNING: to disable the context menu is a bit tricky...
+            # The following seems to work on Firefox 24.0, but:
+            # TODO: find a cleaner way to disable the context menu
+            sender.getElement().setAttribute("oncontextmenu", "return false")
+
+        def _onBrowserEvent(event):
+            button = DOM.eventGetButton(event)
+            if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]:
+                self._show(sender)
+            return sender.__class__.onBrowserEvent(sender, event)
+
+        sender.onBrowserEvent = _onBrowserEvent
+
+    def registerMiddleClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_MIDDLE)
+
+    def registerRightClickSender(self, sender):
+        self.registerClickSender(sender, BUTTON_RIGHT)
+
+
+class ToggleStackPanel(StackPanel):
+    """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be
+    visible at the same time, clicking a sub-panel header will not display it and hide
+    the others but only toggle its own visibility. The argument 'visibleStack' is ignored.
+    Note that the argument 'visible' has been added to listener's 'onStackChanged' method.
+    """
+
+    def __init__(self, **kwargs):
+        StackPanel.__init__(self, **kwargs)
+
+    def onBrowserEvent(self, event):
+        if DOM.eventGetType(event) == "click":
+            index = self.getDividerIndex(DOM.eventGetTarget(event))
+            if index != -1:
+                self.toggleStack(index)
+
+    def add(self, widget, stackText="", asHTML=False, visible=False):
+        StackPanel.add(self, widget, stackText, asHTML)
+        self.setStackVisible(self.getWidgetCount() - 1, visible)
+
+    def toggleStack(self, index):
+        if index >= self.getWidgetCount():
+            return
+        visible = not self.getWidget(index).getVisible()
+        self.setStackVisible(index, visible)
+        for listener in self.stackListeners:
+            listener.onStackChanged(self, index, visible)
+
+
+class TitlePanel(ToggleStackPanel):
+    """A toggle panel to set the message title"""
+    def __init__(self):
+        ToggleStackPanel.__init__(self, Width="100%")
+        self.text_area = TextArea()
+        self.add(self.text_area, _("Title"))
+        self.addStackChangeListener(self)
+
+    def onStackChanged(self, sender, index, visible=None):
+        if visible is None:
+            visible = sender.getWidget(index).getVisible()
+        text = self.text_area.getText()
+        suffix = "" if (visible or not text) else (": %s" % text)
+        sender.setStackText(index, _("Title") + suffix)
+
+    def getText(self):
+        return self.text_area.getText()
+
+    def setText(self, text):
+        self.text_area.setText(text)
+
+
+class BaseTextEditor(object):
+    """Basic definition of a text editor. The method edit gets a boolean parameter which
+    should be set to True when you want to edit the text and False to only display it."""
+
+    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
+        """
+        Remark when inheriting this class: since the setContent method could be
+        overwritten by the child class, you should consider calling this __init__
+        after all the parameters affecting this setContent method have been set.
+        @param content: dict with at least a 'text' key
+        @param strproc: method to be applied on strings to clean the content
+        @param modifiedCb: method to be called when the text has been modified.
+        If this method returns:
+        - True: the modification will be saved and afterEditCb called;
+        - False: the modification won't be saved and afterEditCb called;
+        - None: the modification won't be saved and afterEditCb not called.
+        @param afterEditCb: method to be called when the edition is done
+        """
+        if content is None:
+            content = {'text': ''}
+        assert('text' in content)
+        if strproc is None:
+            def strproc(text):
+                try:
+                    return text.strip()
+                except (TypeError, AttributeError):
+                    return text
+        self.strproc = strproc
+        self.__modifiedCb = modifiedCb
+        self._afterEditCb = afterEditCb
+        self.initialized = False
+        self.edit_listeners = []
+        self.setContent(content)
+
+    def setContent(self, content=None):
+        """Set the editable content. The displayed content, which is set from the child class, could differ.
+        @param content: dict with at least a 'text' key
+        """
+        if content is None:
+            content = {'text': ''}
+        elif not isinstance(content, dict):
+            content = {'text': content}
+        assert('text' in content)
+        self._original_content = {}
+        for key in content:
+            self._original_content[key] = self.strproc(content[key])
+
+    def getContent(self):
+        """Get the current edited or editable content.
+        @return: dict with at least a 'text' key
+        """
+        raise NotImplementedError
+
+    def setOriginalContent(self, content):
+        """Use this method with care! Content initialization should normally be
+        done with self.setContent. This method exists to let you trick the editor,
+        e.g. for self.modified to return True also when nothing has been modified.
+        @param content: dict
+        """
+        self._original_content = content
+
+    def getOriginalContent(self):
+        """
+        @return the original content before modification (dict)
+        """
+        return self._original_content
+
+    def modified(self, content=None):
+        """Check if the content has been modified.
+        Remark: we don't use the direct comparison because we want to ignore empty elements
+        @content: content to be check against the original content or None to use the current content
+        @return: True if the content has been modified.
+        """
+        if content is None:
+            content = self.getContent()
+        # the following method returns True if one non empty element exists in a but not in b
+        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
+        # the following method returns True if the values for the common keys are not equals
+        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
+        # finally the combination of both to return True if a difference is found
+        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
+
+        return diff(content, self._original_content)
+
+    def edit(self, edit, abort=False, sync=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        @param sync: set to True to cancel the edition after the content has been saved somewhere else
+        """
+        if edit:
+            if not self.initialized:
+                self.syncToEditor()  # e.g.: use the selected target and unibox content
+            self.setFocus(True)
+            if abort:
+                content = self.getContent()
+                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
+                    self.edit(False, True, sync)
+                    return
+            if sync:
+                self.syncFromEditor(content)  # e.g.: save the content to unibox
+                return
+        else:
+            if not self.initialized:
+                return
+            content = self.getContent()
+            if abort:
+                self._afterEditCb(content)
+                return
+            if self.__modifiedCb and self.modified(content):
+                result = self.__modifiedCb(content)  # e.g.: send a message or update something
+                if result is not None:
+                    if self._afterEditCb:
+                        self._afterEditCb(content)  # e.g.: restore the display mode
+                    if result is True:
+                        self.setContent(content)
+            elif self._afterEditCb:
+                self._afterEditCb(content)
+
+        self.initialized = True
+
+    def setFocus(self, focus):
+        """
+        @param focus: set to True to focus the editor
+        """
+        raise NotImplementedError
+
+    def syncToEditor(self):
+        pass
+
+    def syncFromEditor(self, content):
+        pass
+
+    def abortEdition(self, content):
+        return True
+
+    def addEditListener(self, listener):
+        """Add a method to be called whenever the text is edited.
+        @param listener: method taking two arguments: sender, keycode"""
+        self.edit_listeners.append(listener)
+
+
+class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler):
+    """Base class for manage a simple text editor."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        """
+        @param content
+        @param modifiedCb
+        @param afterEditCb
+        @param options: dict with the following value:
+        - no_xhtml: set to True to clean any xhtml content.
+        - enhance_display: if True, the display text will be enhanced with addURLToText
+        - listen_keyboard: set to True to terminate the edition with <enter> or <escape>.
+        - listen_focus: set to True to terminate the edition when the focus is lost.
+        - listen_click: set to True to start the edition when you click on the widget.
+        """
+        self.options = {'no_xhtml': False,
+                        'enhance_display': True,
+                        'listen_keyboard': True,
+                        'listen_focus': False,
+                        'listen_click': False
+                        }
+        if options:
+            self.options.update(options)
+        self.__shift_down = False
+        if self.options['listen_focus']:
+            FocusHandler.__init__(self)
+        if self.options['listen_click']:
+            ClickHandler.__init__(self)
+        KeyboardHandler.__init__(self)
+        strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text)
+        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
+        self.textarea = self.display = None
+
+    def setContent(self, content=None):
+        BaseTextEditor.setContent(self, content)
+
+    def getContent(self):
+        raise NotImplementedError
+
+    def edit(self, edit, abort=False, sync=False):
+        BaseTextEditor.edit(self, edit)
+        if edit:
+            if self.options['listen_focus'] and self not in self.textarea._focusListeners:
+                self.textarea.addFocusListener(self)
+            if self.options['listen_click']:
+                self.display.clearClickListener()
+            if self not in self.textarea._keyboardListeners:
+                self.textarea.addKeyboardListener(self)
+        else:
+            self.setDisplayContent()
+            if self.options['listen_focus']:
+                try:
+                    self.textarea.removeFocusListener(self)
+                except ValueError:
+                    pass
+            if self.options['listen_click'] and self not in self.display._clickListeners:
+                self.display.addClickListener(self)
+            try:
+                self.textarea.removeKeyboardListener(self)
+            except ValueError:
+                pass
+
+    def setDisplayContent(self):
+        text = self._original_content['text']
+        if not self.options['no_xhtml']:
+            text = addURLToImage(text)
+        if self.options['enhance_display']:
+            text = addURLToText(text)
+        self.display.setHTML(convertNewLinesToXHTML(text))
+
+    def setFocus(self, focus):
+        raise NotImplementedError
+
+    def onKeyDown(self, sender, keycode, modifiers):
+        for listener in self.edit_listeners:
+            listener(self.textarea, keycode)
+        if not self.options['listen_keyboard']:
+            return
+        if keycode == KEY_SHIFT or self.__shift_down:  # allow input a new line with <shift> + <enter>
+            self.__shift_down = True
+            return
+        if keycode == KEY_ENTER:  # finish the edition
+            self.textarea.setFocus(False)
+            if not self.options['listen_focus']:
+                self.edit(False)
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        if keycode == KEY_SHIFT:
+            self.__shift_down = False
+
+    def onLostFocus(self, sender):
+        """Finish the edition when focus is lost"""
+        if self.options['listen_focus']:
+            self.edit(False)
+
+    def onClick(self, sender=None):
+        """Start the edition when the widget is clicked"""
+        if self.options['listen_click']:
+            self.edit(True)
+
+    def onBrowserEvent(self, event):
+        if self.options['listen_focus']:
+            FocusHandler.onBrowserEvent(self, event)
+        if self.options['listen_click']:
+            ClickHandler.onBrowserEvent(self, event)
+        KeyboardHandler.onBrowserEvent(self, event)
+
+
+class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with the HTML 5 "contenteditable" property."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        HTML.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = self.display = self
+
+    def getContent(self):
+        text = DOM.getInnerHTML(self.getElement())
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setHTML(self._original_content['text'])
+        self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus:
+            self.getElement().focus()
+        else:
+            self.getElement().blur()
+
+
+class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor with a TextArea for editing, HTML for display."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None):
+        SimplePanel.__init__(self)
+        SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options)
+        self.textarea = TextArea()
+        self.display = HTML()
+
+    def getContent(self):
+        text = self.textarea.getText()
+        return {'text': self.strproc(text) if text else ''}
+
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.textarea.setText(self._original_content['text'])
+        self.setWidget(self.textarea if edit else self.display)
+        SimpleTextEditor.edit(self, edit, abort, sync)
+
+    def setFocus(self, focus):
+        if focus:
+            self.textarea.setCursorPos(len(self.textarea.getText()))
+        self.textarea.setFocus(focus)