view browser_side/base_panels.py @ 328:835a8ae799e7

Add notifications support, fixes bug 7.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sat, 23 Feb 2013 16:27:32 +0100
parents 0b7934e75e76
children 2067d6241927
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Libervia: a Salut à Toi frontend
Copyright (C) 2011, 2012, 2013 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 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.PopupPanel import PopupPanel
from pyjamas.ui.StackPanel import StackPanel
from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT
from pyjamas import DOM

from datetime import datetime
from time import time

from tools import html_sanitize, inlineRoot

from sat_frontends.tools.strings import addURLToText


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, special=""):
        HTML.__init__(self)
        self.nick = nick
        self.special = special
        self._refresh()

    def __str__(self):
        return self.nick

    def addSpecial(self, special=""):
        if special not in self.special:
            self.special += special
            self._refresh()

    def _refresh(self):
        special = "" if len(self.special) == 0 else " %s" % self.special
        self.setHTML("<div class='occupant'>%s%s</div>" % (html_sanitize(self.nick), special))


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:
            print "ERROR: trying to remove an unexisting nick"

    def clear(self):
        self.occupants_list.clear()
        AbsolutePanel.clear(self)

    def addSpecials(self, occupants=[], html=""):
        index = 0
        special = html
        for occupant in occupants:
            if occupant in self.occupants_list.keys():
                if isinstance(html, list):
                    special = html[index]
                    index = (index + 1) % len(html)
                self.occupants_list[occupant].addSpecial(special)


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={}, **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"}
        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)