view src/browser/sat_browser/panels.py @ 613:deddd1cd8aa5 frontends_multi_profiles

browser side: added a base_location property to get location of current Libervia instance
author Goffi <goffi@goffi.org>
date Tue, 10 Feb 2015 20:42:39 +0100
parents 5983d6be4f07
children 70872a83ef15
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/>.

import pyjd  # this is dummy in pyjs
from sat.core.log import getLogger
log = getLogger(__name__)

from sat_frontends.tools.strings import addURLToText

from pyjamas.ui.AbsolutePanel import AbsolutePanel
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.HorizontalPanel import HorizontalPanel
from pyjamas.ui.TextArea import TextArea
from pyjamas.ui.Button import Button
from pyjamas.ui.HTML import HTML
from pyjamas.ui.ClickListener import ClickHandler
from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN
from pyjamas.ui.MouseListener import MouseHandler
from pyjamas.ui.Frame import Frame
from pyjamas.Timer import Timer
from pyjamas import Window
from pyjamas import DOM
from __pyjamas__ import doc


import base_panels
import base_menu
import menu
import dialog
import base_widget
import contact_list
from constants import Const as C
from sat_frontends.quick_frontend import quick_widgets


# class UniBoxPanel(HorizontalPanel):
#     """Panel containing the UniBox"""
#
#     def __init__(self, host):
#         HorizontalPanel.__init__(self)
#         self.host = host
#         self.setStyleName('uniBoxPanel')
#         self.unibox = None
#
#     def refresh(self):
#         """Enable or disable this panel. Contained widgets are created when necessary."""
#         enable = self.host.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true'
#         self.setVisible(enable)
#         if enable and not self.unibox:
#             self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
#             self.button.setTitle('Open the rich text editor')
#             self.button.addStyleName('uniBoxButton')
#             self.add(self.button)
#             self.unibox = UniBox(self.host)
#             self.add(self.unibox)
#             self.setCellWidth(self.unibox, '100%')
#             self.button.addClickListener(self.openRichMessageEditor)
#             self.unibox.addKey("@@: ")
#             self.unibox.onSelectedChange(self.host.getSelected())
#
#     def openRichMessageEditor(self):
#         """Open the rich text editor."""
#         self.button.setVisible(False)
#         self.unibox.setVisible(False)
#         self.setCellWidth(self.unibox, '0px')
#         self.host.panel._contactsMove(self)
#
#         def afterEditCb():
#             Window.removeWindowResizeListener(self)
#             self.host.panel._contactsMove(self.host.panel._hpanel)
#             self.setCellWidth(self.unibox, '100%')
#             self.button.setVisible(True)
#             self.unibox.setVisible(True)
#             self.host.resize()
#
#         richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
#         Window.addWindowResizeListener(self)
#         self.host.resize()
#
#     def onWindowResized(self, width, height):
#         right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
#         left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth()
#         ideal_width = right - left - 40
#         self.host.richtext.setWidth("%spx" % ideal_width)


class MessageBox(TextArea):
    """A basic text area for entering messages"""

    def __init__(self, host):
        TextArea.__init__(self)
        self.host = host
        self.size = (0, 0)
        self.setStyleName('messageBox')
        self.addKeyboardListener(self)
        MouseHandler.__init__(self)
        self.addMouseListener(self)

    def onBrowserEvent(self, event):
        # XXX: woraroung a pyjamas bug: self.currentEvent is not set
        #     so the TextBox's cancelKey doens't work. This is a workaround
        #     FIXME: fix the bug upstream
        self.currentEvent = event
        TextArea.onBrowserEvent(self, event)

    def onKeyPress(self, sender, keycode, modifiers):
        _txt = self.getText()

        def history_cb(text):
            self.setText(text)
            Timer(5, lambda timer: self.setCursorPos(len(text)))

        if keycode == KEY_ENTER:
            if _txt:
                self.host.selected_widget.onTextEntered(_txt)
                self.host._updateInputHistory(_txt) # FIXME: why using a global variable ?
            self.setText('')
            sender.cancelKey()
        elif keycode == KEY_UP:
            self.host._updateInputHistory(_txt, -1, history_cb)
        elif keycode == KEY_DOWN:
            self.host._updateInputHistory(_txt, +1, history_cb)
        else:
            self._onComposing()

    def _onComposing(self):
        """Callback when the user is composing a text."""
        self.host.selected_widget.state_machine._onEvent("composing")

    def onMouseUp(self, sender, x, y):
        size = (self.getOffsetWidth(), self.getOffsetHeight())
        if size != self.size:
            self.size = size
            self.host.resize()

    def onSelectedChange(self, selected):
        self._selected_cache = selected


# class UniBox(MessageBox, MouseHandler):  # AutoCompleteTextBox):
#     """This text box is used as a main typing point, for message, microblog, etc"""
#
#     def __init__(self, host):
#         MessageBox.__init__(self, host)
#         #AutoCompleteTextBox.__init__(self)
#         self.setStyleName('uniBox')
#         # FIXME
#         # host.addSelectedListener(self.onSelectedChange)
#
#     def addKey(self, key):
#         return
#         #self.getCompletionItems().completions.append(key)
#
#     def removeKey(self, key):
#         return
#         # TODO: investigate why AutoCompleteTextBox doesn't work here,
#         # maybe it can work on a TextBox but no TextArea. Remove addKey
#         # and removeKey methods if they don't serve anymore.
#         try:
#             self.getCompletionItems().completions.remove(key)
#         except KeyError:
#             log.warning("trying to remove an unknown key")
#
#     def _getTarget(self, txt):
#         """ Say who will receive the messsage
#         @return: a tuple (selected, target_type, target info) with:
#             - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None))
#             - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC
#             - msg: HTML message which will appear in the privacy warning banner """
#         target = self._selected_cache
#
#         def getSelectedOrStatus():
#             if target and target.isSelectable():
#                 _type, msg = target.getWarningData()
#                 target_hook = None  # we use the selected widget, not a hook
#             else:
#                 _type, msg = "STATUS", "This will be your new status message"
#                 target_hook = (txt, None)
#             return (target_hook, _type, msg)
#
#         if not txt.startswith('@'):
#             target_hook, _type, msg = getSelectedOrStatus()
#         elif txt.startswith('@@: '):
#             _type = "PUBLIC"
#             msg = MicroblogPanel.warning_msg_public
#             target_hook = (txt[4:], None)
#         elif txt.startswith('@'):
#             _end = txt.find(': ')
#             if _end == -1:
#                 target_hook, _type, msg = getSelectedOrStatus()
#             else:
#                 group = txt[1:_end]  # only one target group is managed for the moment
#                 if not group or not group in self.host.contact_panel.getGroups():
#                     # the group doesn't exists, we ignore the key
#                     group = None
#                     target_hook, _type, msg = getSelectedOrStatus()
#                 else:
#                     _type = "GROUP"
#                     msg = MicroblogPanel.warning_msg_group % group
#                     target_hook = (txt[_end + 2:], group)
#         else:
#             log.error("Unknown target")
#             target_hook, _type, msg = getSelectedOrStatus()
#
#         return (target_hook, _type, msg)
#
#     def onKeyPress(self, sender, keycode, modifiers):
#         _txt = self.getText()
#         target_hook, type_, msg = self._getTarget(_txt)
#
#         if keycode == KEY_ENTER:
#             if _txt:
#                 if target_hook:
#                     parsed_txt, data = target_hook
#                     self.host.send([(type_, data)], parsed_txt)
#                     self.host._updateInputHistory(_txt)
#                     self.setText('')
#             self.host.showWarning(None, None)
#         else:
#             self.host.showWarning(type_, msg)
#         MessageBox.onKeyPress(self, sender, keycode, modifiers)
#
#     def getTargetAndData(self):
#         """For external use, to get information about the (hypothetical) message
#         that would be sent if we press Enter right now in the unibox.
#         @return a tuple (target, data) with:
#           - data: what would be the content of the message (body)
#           - target: JID, group with the prefix "@" or the public entity "@@"
#         """
#         _txt = self.getText()
#         target_hook, _type, _msg = self._getTarget(_txt)
#         if target_hook:
#             data, target = target_hook
#             if target is None:
#                 return target_hook
#             return (data, "@%s" % (target if target != "" else "@"))
#         if isinstance(self._selected_cache, MicroblogPanel):
#             groups = self._selected_cache.accepted_groups
#             target = "@%s" % (groups[0] if len(groups) > 0 else "@")
#             if len(groups) > 1:
#                 Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0])
#         # elif isinstance(self._selected_cache, ChatPanel): # FIXME
#         #     target = self._selected_cache.target
#         else:
#             target = None
#         return (_txt, target)
#
#     def onWidgetClosed(self, lib_wid):
#         """Called when a libervia widget is closed"""
#         if self._selected_cache == lib_wid:
#             self.onSelectedChange(None)
#
#     """def complete(self):
#
#         #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done
#         #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this
#         return AutoCompleteTextBox.complete(self)"""


class WarningPopup():

    def __init__(self):
        self._popup = None
        self._timer = Timer(notify=self._timeCb)

    def showWarning(self, type_=None, msg=None, duration=2000):
        """Display a popup information message, e.g. to notify the recipient of a message being composed.
        If type_ is None, a popup being currently displayed will be hidden.
        @type_: a type determining the CSS style to be applied (see _showWarning)
        @msg: message to be displayed
        """
        if type_ is None:
            self.__removeWarning()
            return
        if not self._popup:
            self._showWarning(type_, msg)
        elif (type_, msg) != self._popup.target_data:
            self._timeCb(None)  # we remove the popup
            self._showWarning(type_, msg)

        self._timer.schedule(duration)

    def _showWarning(self, type_, msg):
        """Display a popup information message, e.g. to notify the recipient of a message being composed.
        @type_: a type determining the CSS style to be applied. For now the defined styles are
        "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE".
        @msg: message to be displayed
        """
        if type_ == "NONE":
            return
        if not msg:
            log.warning("no msg set uniBox warning")
            return
        if type_ == "PUBLIC":
            style = "targetPublic"
        elif type_ == "GROUP":
            style = "targetGroup"
        elif type_ == "STATUS":
            style = "targetStatus"
        elif type_ == "ONE2ONE":
            style = "targetOne2One"
        else:
            log.error("unknown message type")
            return
        contents = HTML(msg)

        self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
        self._popup.target_data = (type_, msg)
        self._popup.add(contents)
        self._popup.setStyleName("warningPopup")
        if style:
            self._popup.addStyleName(style)

        left = 0
        top = 0  # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2)
        self._popup.setPopupPosition(left, top)
        self._popup.show()

    def _timeCb(self, timer):
        if self._popup:
            self._popup.hide()
            del self._popup
            self._popup = None

    def __removeWarning(self):
        """Remove the popup"""
        self._timeCb(None)


class StatusPanel(base_panels.HTMLTextEditor):

    EMPTY_STATUS = '&lt;click to set a status&gt;'

    def __init__(self, host, status=''):
        self.host = host
        modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
        base_panels.HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True})
        self.edit(False)
        self.setStyleName('statusPanel')

    @property
    def status(self):
        return self._original_content['text']

    def __cleanContent(self, content):
        status = content['text']
        if status == self.EMPTY_STATUS or status in C.PRESENCE.values():
            content['text'] = ''
        return content

    def getContent(self):
        return self.__cleanContent(base_panels.HTMLTextEditor.getContent(self))

    def setContent(self, content):
        content = self.__cleanContent(content)
        base_panels.BaseTextEditor.setContent(self, content)

    def setDisplayContent(self):
        status = self._original_content['text']
        try:
            presence = self.host.status_panel.presence
        except AttributeError:  # during initialization
            presence = None
        if not status:
            if presence and presence in C.PRESENCE:
                status = C.PRESENCE[presence]
            else:
                status = self.EMPTY_STATUS
        self.display.setHTML(addURLToText(status))


class PresenceStatusMenuBar(base_widget.WidgetMenuBar):
    def __init__(self, parent):
        styles = {'menu_bar': 'presence-button'}
        base_widget.WidgetMenuBar.__init__(self, None, parent.host, styles=styles)
        self.button = self.addCategory(u"◉", u"◉", '')
        for presence, presence_i18n in C.PRESENCE.items():
            html = u'<span class="%s">◉</span> %s' % (contact_list.buildPresenceStyle(presence), presence_i18n)
            self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True)
        self.parent_panel = parent

    def changePresenceCb(self, presence):
        """Callback to notice the backend of a new presence set by the user.
        @param presence (str): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa')
        """
        self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status)

    @classmethod
    def getCategoryHTML(cls, menu_name_i18n, type_):
        return menu_name_i18n


class PresenceStatusPanel(HorizontalPanel, ClickHandler):

    def __init__(self, host, presence="", status=""):
        self.host = host
        HorizontalPanel.__init__(self, Width='100%')
        self.menu = PresenceStatusMenuBar(self)
        self.status_panel = StatusPanel(host, status=status)
        self.setPresence(presence)

        panel = HorizontalPanel()
        panel.add(self.menu)
        panel.add(self.status_panel)
        panel.setCellVerticalAlignment(self.menu, 'baseline')
        panel.setCellVerticalAlignment(self.status_panel, 'baseline')
        panel.setStyleName("marginAuto")
        self.add(panel)

        self.status_panel.edit(False)

        ClickHandler.__init__(self)
        self.addClickListener(self)

    @property
    def presence(self):
        return self._presence

    @property
    def status(self):
        return self.status_panel._original_content['text']

    def setPresence(self, presence):
        self._presence = presence
        contact_list.setPresenceStyle(self.menu.button, self._presence)

    def setStatus(self, status):
        self.status_panel.setContent({'text': status})
        self.status_panel.setDisplayContent()

    def onClick(self, sender):
        # As status is the default target of uniBar, we don't want to select anything if click on it
        self.host.setSelected(None)


class WebPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget):
    """ (mini)browser like widget """

    def __init__(self, host, target, profiles=None):
        """
        @param host: SatWebFrontend instance
        @param target: url to open
        """
        quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE)
        base_widget.LiberviaWidget.__init__(self, host)
        self._vpanel = VerticalPanel()
        self._vpanel.setSize('100%', '100%')
        self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick)
        self._url.setText(target or "")
        self._url.setWidth('100%')
        hpanel = HorizontalPanel()
        hpanel.add(self._url)
        btn = Button("Go", self.onUrlClick)
        hpanel.setCellWidth(self._url, "100%")
        #self.setCellWidth(btn, "10%")
        hpanel.add(self._url)
        hpanel.add(btn)
        self._vpanel.add(hpanel)
        self._vpanel.setCellHeight(hpanel, '20px')
        self._frame = Frame(target or "")
        self._frame.setSize('100%', '100%')
        DOM.setStyleAttribute(self._frame.getElement(), "position", "relative")
        self._vpanel.add(self._frame)
        self.setWidget(self._vpanel)

    def onUrlClick(self, sender):
        url = self._url.getText()
        scheme_end = url.find(':')
        scheme = "" if scheme_end == -1 else url[:scheme_end]
        if scheme not in C.WEB_PANEL_SCHEMES:
            url = "http://" + url
        self._frame.setUrl(url)


class MainPanel(AbsolutePanel):

    def __init__(self, host):
        self.host = host
        AbsolutePanel.__init__(self)

        # menu
        self.menu = menu.MainMenuPanel(host)

        # # unibox
        # self.unibox_panel = UniBoxPanel(host)
        # self.unibox_panel.setVisible(False)

        # contacts
        self._contacts = HorizontalPanel()
        self._contacts.addStyleName('globalLeftArea')
        self.contacts_switch = Button(u'«', self._contactsSwitch)
        self.contacts_switch.addStyleName('contactsSwitch')
        self._contacts.add(self.contacts_switch)

        # tabs
        self.tab_panel = base_widget.MainTabPanel(host)
        self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True)
        self.tab_panel.add(self.discuss_panel, "Discussions")
        self.tab_panel.selectTab(0)

        self.header = AbsolutePanel()
        self.header.add(self.menu)
        # self.header.add(self.unibox_panel)
        self.header.add(self.host.status_panel)
        self.header.setStyleName('header')
        self.add(self.header)

        self._hpanel = HorizontalPanel()
        self._hpanel.add(self._contacts)
        self._hpanel.add(self.tab_panel)
        self.add(self._hpanel)

        self.setWidth("100%")
        Window.addWindowResizeListener(self)

    def addContactList(self, contact_list):
        self._contacts.add(contact_list)

    def _contactsSwitch(self, btn=None):
        """ (Un)hide contacts panel """
        if btn is None:
            btn = self.contacts_switch
        clist = self.host.contact_list
        clist.setVisible(not clist.getVisible())
        btn.setText(u"«" if clist.getVisible() else u"»")
        self.host.resize()

    def _contactsMove(self, parent):
        """Move the contacts container (containing the contact list and
        the "hide/show" button) to another parent, but always as the
        first child position (insert at index 0).
        """
        if self._contacts.getParent():
            if self._contacts.getParent() == parent:
                return
            self._contacts.removeFromParent()
        parent.insert(self._contacts, 0)

    def onWindowResized(self, width, height):
        _elts = doc().getElementsByClassName('gwt-TabBar')
        if not _elts.length:
            tab_bar_h = 0
        else:
            tab_bar_h = _elts.item(0).offsetHeight
        ideal_height = Window.getClientHeight() - tab_bar_h
        self.setHeight("%s%s" % (ideal_height, "px"))

    def refresh(self):
        """Refresh the main panel"""
        self.unibox_panel.refresh()
        self.host.contact_panel.refresh()