view browser_side/panels.py @ 201:aa76793da353

server + browser: message warning level/sending refactoring: - widgets now manage themselves warning level - widgets now manage themselves text entered (if they are selectable, onTextEntered must be present) - Unibox now default to selected widget/status bar, except if a hook syntax is used (e.g. "@@: public microblog") - warning message is now GROUP (in blue with default theme) when sending a message to a MUC room (instead of green before) - "@.*: " syntaxes are now fully managed by browser, no more by server
author Goffi <goffi@goffi.org>
date Sun, 07 Apr 2013 22:33:55 +0200
parents 0f5c2f799913
children 2bc6cf004e61
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.SimplePanel import SimplePanel
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.Frame import Frame
from pyjamas.ui.TextArea import TextArea
from pyjamas.ui.Label import Label
from pyjamas.ui.Button import Button
from pyjamas.ui.HTML import HTML
from pyjamas.ui.Image import Image
from pyjamas.ui.ClickListener import ClickHandler
from pyjamas.ui.KeyboardListener import KEY_ENTER
from pyjamas.ui.MouseListener import MouseHandler
from pyjamas.Timer import Timer
from pyjamas import DOM
from card_game import CardPanel
from radiocol import RadioColPanel
from menu import Menu
from jid import JID
from tools import html_sanitize
from datetime import datetime
from time import time
import dialog
import base_widget
from pyjamas import Window
from __pyjamas__ import doc

class UniBoxPanel(SimplePanel):
    """Panel containing the UniBox"""

    def __init__(self, host):
        SimplePanel.__init__(self)
        self.setStyleName('uniBoxPanel')
        self.unibox = UniBox(host)
        self.unibox.setWidth('100%')
        self.add(self.unibox)

class UniBox(TextArea, MouseHandler): #AutoCompleteTextBox):
    """This text box is used as a main typing point, for message, microblog, etc"""

    def __init__(self, host):
        TextArea.__init__(self)
        #AutoCompleteTextBox.__init__(self)
        self.__size = (0,0) 
        self._popup = None
        self._timer = Timer(notify=self._timeCb)
        self.host = host
        self.setStyleName('uniBox')
        self.addKeyboardListener(self)
        MouseHandler.__init__(self)
        self.addMouseListener(self)
        self._selected_cache = None
        host.addSelectedListener(self.onSelectedChange)

    def addKey(self, key):
        return
        #self.getCompletionItems().completions.append(key)

    def removeKey(self, key):
        try:
            self.getCompletionItems().completions.remove(key)
        except KeyError:
            print "WARNING: trying to remove an unknown key"
            

    def showWarning(self, target_data):
        target_hook, _type, msg = target_data
        if _type == "NONE":
            return
        if not msg:
            print "WARNING: no msg set uniBox warning"
            return
        if _type == "PUBLIC":
            style = "targetPublic"
        elif _type == "GROUP":
            style = "targetGroup"
        elif _type == "STATUS":
            msg = "This will be your new status message"
            style = "targetStatus"
        elif _type == "ONE2ONE":
            style = "targetOne2One"
        else:
            print "ERROR: unknown message type"
            return
        contents = HTML(msg)

        self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False)
        self._popup.target_data = target_data
        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 _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:
                _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:
            print "ERROR: Unknown target"
            target_hook, _type, msg = getSelectedOrStatus()
        
        return (target_hook, _type, msg)

    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()
        target = self._getTarget(_txt)
        if not self._popup:
            self.showWarning(target)
        elif target != self._popup.target_data:
            self._timeCb(None) #we remove the popup
            self.showWarning(target)

        self._timer.schedule(2000)

        #if keycode == KEY_ENTER and not self.visible:
        if keycode == KEY_ENTER:
            if _txt:
                target_hook, _type, msg = target
                if target_hook:
                    parsed_txt, data = target_hook
                    if _type == "PUBLIC":
                        self.host.bridge.call("sendMblog", None, "PUBLIC", None, parsed_txt)
                    elif _type == "GROUP":
                        self.host.bridge.call("sendMblog", None, "GROUP", data, parsed_txt)
                    elif _type == "STATUS":
                        self.host.bridge.call('setStatus', None, parsed_txt)
                    else:
                        print "ERROR: Unknown target hook type"
                else: #we send the message to the selected target
                    self._selected_cache.onTextEntered(_txt)
            self.setText('')
            self._timeCb(None) #we remove the popup
            sender.cancelKey()

    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

    """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 MicroblogItem():
    #XXX: should be moved in a separated module

    def __init__(self, data):
        self.id = data['id']
        self.content = data['content']
        self.author = data['author']
        self.timestamp = float(data.get('timestamp',0)) #XXX: int doesn't work here
        
class MicroblogEntry(SimplePanel, ClickHandler):

    def __init__(self, blog_panel, mblog_entry):
        SimplePanel.__init__(self)
        self._blog_panel = blog_panel

        self.author = mblog_entry.author
        self.timestamp = mblog_entry.timestamp
        _datetime = datetime.fromtimestamp(mblog_entry.timestamp)

        self.panel = HTMLPanel("""
            <div class='mb_entry_header'><span class='mb_entry_author'>%(author)s</span> on <span class='mb_entry_timestamp'>%(timestamp)s</span></div>
            <div class="mb_entry_avatar" id='id_avatar'></div>
            <div class="mb_entry_dialog">
                <p class="bubble">%(body)s</p>
            </div>
            """ % {"author": html_sanitize(self.author),
                   "timestamp": _datetime,
                   "body": html_sanitize(mblog_entry.content)
                    })
        self.avatar = Image(blog_panel.host.getAvatar(self.author))
        self.panel.add(self.avatar, "id_avatar")
        self.panel.setStyleName('mb_entry')
        self.add(self.panel)
        ClickHandler.__init__(self)
        self.addClickListener(self)

    def updateAvatar(self, new_avatar):
        """Change the avatar of the entry
        @param new_avatar: path to the new image"""
        self.avatar.setUrl(new_avatar)
    
    def onClick(self, sender):
        print "microblog entry selected (author=%s)" % self.author
        self._blog_panel.setSelectedEntry(self)


class MicroblogPanel(base_widget.LiberviaWidget):
    warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know"
    warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"

    def __init__(self, host, accepted_groups):
        """Panel used to show microblog
        @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts
        """
        base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable = True)
        #base_widget.ScrollPanelWrapper.__init__(self)
        #DropCell.__init__(self)
        self.accepted_groups = accepted_groups
        self.entries = {}
        self.selected_entry = None
        self.vpanel = VerticalPanel()
        self.vpanel.setStyleName('microblogPanel')
        self.setWidget(self.vpanel)

    @classmethod
    def registerClass(cls):
        base_widget.LiberviaWidget.addDropKey("GROUP", cls.createGroup)
        base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMeta)

    @classmethod
    def createGroup(cls, host, item):
        _new_panel = MicroblogPanel(host, [item]) #XXX: pyjamas doesn't support use of cls directly
        _new_panel.setAcceptedGroup(item)
        host.FillMicroblogPanel(_new_panel)
        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, 'GROUP', [item], 10)
        return _new_panel

    @classmethod
    def createMeta(cls, host, item):
        _new_panel = MicroblogPanel(host, []) #XXX: pyjamas doesn't support use of cls directly
        host.FillMicroblogPanel(_new_panel)
        host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, 'ALL', [], 10)
        return _new_panel

    def getWarningData(self):
        if not self.accepted_groups:
            # we have a meta MicroblogPanel, we publish publicly
            return ("PUBLIC", self.warning_msg_public)
        else:
            # we only accept one group at the moment
            # FIXME: manage several groups
            return ("GROUP", self.warning_msg_group %  self.accepted_groups[0])

    def onTextEntered(self, text):
        if not self.accepted_groups:
            self.host.bridge.call("sendMblog", None, "PUBLIC", None, text)
        else:
            # FIXME: manage several groups
            self.host.bridge.call("sendMblog", None, "GROUP", self.accepted_groups[0], text)

    def accept_all(self):
        return not self.accepted_groups #we accept every microblog only if we are not filtering by groups

    def getEntries(self):
        """Ask all the entries for the currenly accepted groups,
        and fill the panel"""
       
    def massiveInsert(self, mblogs):
        """Insert several microblogs at once
        @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs
        """
        print "Massive insertion of microblogs"
        for publisher in mblogs:
            print "adding blogs for [%s]" % publisher
            for mblog in mblogs[publisher]:
                if not mblog.has_key('content'):
                    print ("WARNING: No content found in microblog [%s]", mblog)
                    continue
                mblog_entry = MicroblogItem(mblog)
                self.addEntry(mblog_entry)

    def addEntry(self, mblog_entry):
        """Add an entry to the panel
        @param mblog_entry: MicroblogItem instance
        """
        if mblog_entry.id in self.entries:
            return
        _entry = MicroblogEntry(self, mblog_entry)
        self.entries[mblog_entry.id] = _entry
        
        # we look for the right index to insert our entry:
        # we insert the entry above the first entry
        # in the past
        idx = 0
        for child in self.vpanel.children:
            if not isinstance(child, MicroblogEntry):
                break
            if child.timestamp < mblog_entry.timestamp:
                break
            idx+=1
        self.vpanel.insert(_entry,idx)

    def setSelectedEntry(self, entry):
        if self.selected_entry == entry:
            entry = None
        if self.selected_entry:
            self.selected_entry.removeStyleName('selected_entry')
        if entry:
            entry.addStyleName('selected_entry')
        self.selected_entry = entry


    def updateValue(self, type, jid, value):
        """Update a jid value in entries
        @param type: one of 'avatar', 'nick'
        @param jid: jid concerned
        @param value: new value"""
        if type=='avatar':
            for entry in self.entries.values():
                if entry.author == jid:
                    entry.updateAvatar(value)

    def setAcceptedGroup(self, group):
        """Set the group which can be displayed in this panel
        @param group: string of the group, or list of string
        """
        if isinstance(group, list):
            self.accepted_groups.extend(group)
        else:
            self.accepted_groups.append(group)

    def isJidAccepted(self, jid):
        """Tell if a jid is actepted and shown in this panel
        @param jid: jid
        @return: True if the jid is accepted"""
        if self.accept_all():
            return True
        for group in self.accepted_groups:
            if self.host.contact_panel.isContactInGroup(group, jid):
                return True
        return False

class StatusPanel(HTMLPanel, ClickHandler):
    def __init__(self, host, status=''):
        self.host = host
        self.status = status or '&nbsp;'
        HTMLPanel.__init__(self, self.__getContent())
        self.setStyleName('statusPanel')
        ClickHandler.__init__(self)
        self.addClickListener(self)

    def __getContent(self):
        return "<span class='status'>%(status)s</span>" % {'status':html_sanitize(self.status)}

    def changeStatus(self, new_status):
        self.status = new_status or '&nbsp;'
        self.setHTML(self.__getContent())

    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 ChatText(HTMLPanel):

    def __init__(self, timestamp, nick, mymess, msg):
        _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": html_sanitize(msg)}
            )
        self.setStyleName('chatText')

class Occupant(HTML):
    """Occupant of a MUC room"""

    def __init__(self, nick):
        self.nick = nick
        HTML.__init__(self, "<div class='occupant'>%s</div>" % html_sanitize(nick))

    def __str__(self):
        return self.nick

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)

class ChatPanel(base_widget.LiberviaWidget):

    def __init__(self, host, target, type_='one2one'):
        """Panel used for conversation (one 2 one or group chat)
        @param host: SatWebFrontend instance
        @param target: entity (JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
        @param type: one2one for simple conversation, group for MUC"""
        base_widget.LiberviaWidget.__init__(self, host, target.bare, selectable = True)
        self.vpanel = VerticalPanel()
        self.vpanel.setSize('100%','100%')
        self.type = type_
        self.nick = None
        if not target:
            print "ERROR: Empty target !"
            return
        self.target = target
        self.__body = AbsolutePanel()
        self.__body.setStyleName('chatPanel_body')
        chat_area = HorizontalPanel()
        chat_area.setStyleName('chatArea')
        if type_ == 'group':
            self.occupants_list = OccupantsList()
            chat_area.add(self.occupants_list)
        self.__body.add(chat_area)
        self.content = AbsolutePanel()
        self.content.setStyleName('chatContent')
        self.content_scroll = base_widget.ScrollPanelWrapper(self.content)
        chat_area.add(self.content_scroll)
        chat_area.setCellWidth(self.content_scroll, '100%')
        self.vpanel.add(self.__body)
        self.addStyleName('chatPanel')
        self.setWidget(self.vpanel)

    """def doDetachChildren(self):
        #We need to force the use of a panel subclass method here,
        #for the same reason as doAttachChildren
        base_widget.ScrollPanelWrapper.doDetachChildren(self)

    def doAttachChildren(self):
        #We need to force the use of a panel subclass method here, else
        #the event will not propagate to children
        base_widget.ScrollPanelWrapper.doAttachChildren(self)"""

    @classmethod
    def registerClass(cls):
        base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createChat)

    @classmethod
    def createChat(cls, host, item):
        _contact = JID(item)
        host.contact_panel.setContactMessageWaiting(_contact.bare, False)
        _new_panel = ChatPanel(host, _contact) #XXX: pyjamas doesn't seems to support creating with cls directly
        _new_panel.historyPrint()
        return _new_panel

    def getWarningData(self):
        if self.type not in ["one2one", "group"]:
            raise Exception("Unmanaged type !")
        if self.type == "one2one":
            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target
        elif self.type == "group":
            msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target
        return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg)

    def onTextEntered(self, text):
        mess_type = "groupchat" if self.type=='group' else "chat"
        self.host.bridge.call('sendMessage', None, str(self.target), text, '', mess_type)

    def onQuit(self):
        base_widget.LiberviaWidget.onQuit(self)
        if self.type == 'group':
            self.host.bridge.call('mucLeave', None, self.target.bare)
        

    def setUserNick(self, nick):
        """Set the nick of the user, usefull for e.g. change the color of the user"""
        self.nick = nick
   
    def setPresents(self, nicks):
        """Set the users presents in this room
        @param occupants: list of nicks (string)"""
        self.occupants_list.clear()
        for nick in nicks:
            self.occupants_list.addOccupant(nick)

    def userJoined(self, nick, data):
        self.occupants_list.addOccupant(nick)
        self.printInfo("=> %s has joined the room" % nick)

    def userLeft(self, nick, data):
        self.occupants_list.removeOccupant(nick)
        self.printInfo("<= %s has left the room" % nick)

    def historyPrint(self, size=20):
        """Print the initial history"""
        def getHistoryCB(history):
            for line in history:
                timestamp, from_jid, to_jid, message, mess_type = line
                self.printMessage(from_jid, message, timestamp)
        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True)
   
    def printInfo(self, msg, type='normal'):
        """Print general info
        @param msg: message to print
        @type: one of:
            normal: general info like "toto has joined the room"
            me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
        """
        _wid = Label(msg)
        if type == 'normal':
            _wid.setStyleName('chatTextInfo')
        elif type == 'me':
            _wid.setStyleName('chatTextMe')
        else:
            _wid.setStyleName('chatTextInfo')
        self.content.add(_wid)
        

    def printMessage(self, from_jid, msg, timestamp=None):
        """Print message in chat window. Must be implemented by child class"""
        _jid=JID(from_jid)
        nick = _jid.node if self.type=='one2one' else _jid.resource 
        mymess = _jid.resource == self.nick if self.type == "group" else  _jid.bare == self.host.whoami.bare #mymess = True if message comes from local user
        if msg.startswith('/me '):
            self.printInfo('* %s %s' % (nick, msg[4:]),type='me')
            return
        self.content.add(ChatText(timestamp, nick, mymess, msg))
        self.content_scroll.scrollToBottom()
    
    def startGame(self, game_type, referee, players):
        """Configure the chat window to start a game"""
        if game_type=="Tarot":
            if hasattr(self, "tarot_panel"):
                return
            self.tarot_panel = CardPanel(self, referee, players, self.nick)
            self.vpanel.insert(self.tarot_panel, 0)
            self.vpanel.setCellHeight(self.tarot_panel, self.tarot_panel.getHeight())
        elif game_type=="RadioCol":
            #XXX: We can have double panel if we join quickly enough to have the group chat start signal
            #     on invitation + the one triggered on room join
            if hasattr(self, "radiocol_panel"):
                return 
            self.radiocol_panel = RadioColPanel(self, referee, self.nick)
            self.vpanel.insert(self.radiocol_panel, 0)
            self.vpanel.setCellHeight(self.radiocol_panel, self.radiocol_panel.getHeight())
    
    def getGame(self, game_type):
        """Return class managing the game type"""
        #TODO: check that the game is launched, and manage errors
        if game_type=="Tarot":
            return self.tarot_panel 
        elif game_type=="RadioCol":
            return self.radiocol_panel 

class WebPanel(base_widget.LiberviaWidget):
    """ (mini)browser like widget """
    
    def __init__(self, host, url=None):
        """
        @param host: SatWebFrontend instance
        """
        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(url 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(url 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):
        self._frame.setUrl(self._url.getText())

class ContactTabPanel(HorizontalPanel):
    """ TabPanel with a contacts list which can be hidden """
    
    def __init__(self, host, locked = False):
        self.host=host
        HorizontalPanel.__init__(self)
        self._left = VerticalPanel()
        contacts_switch = Button('<<', self._contactsSwitch)
        contacts_switch.addStyleName('contactsSwitch')
        self._left.add(contacts_switch)
        self._left.add(self.host.contact_panel)
        self._right = base_widget.WidgetsPanel(host)
        self._right.setWidth('100%')
        self._right.setHeight('100%')
        self.add(self._left)
        self.add(self._right)
        self.setCellWidth(self._right, "100%")

    def addWidget(self, wid):
        """Add a widget to the WidgetsPanel"""
        print "main addWidget", wid
        self._right.addWidget(wid)

class MainPanel(AbsolutePanel):

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

        #menu
        menu = Menu(host)

        #unibox
        unibox_panel = UniBoxPanel(host)
        self.host.setUniBox(unibox_panel.unibox)
        
        #status bar
        status = host.status_panel
        
        #contacts
        _contacts = VerticalPanel()
        contacts_switch = Button('<<', self._contactsSwitch)
        contacts_switch.addStyleName('contactsSwitch')
        _contacts.add(contacts_switch)
        _contacts.add(self.host.contact_panel)

        #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)

        header=AbsolutePanel()
        header.add(menu)
        header.add(unibox_panel)
        header.add(status)
        header.setStyleName('header')
        self.add(header)

        _hpanel = HorizontalPanel()
        _hpanel.add(_contacts)
        _hpanel.add(self.tab_panel)
        self.add(_hpanel)
        
        self.setWidth("100%")
        Window.addWindowResizeListener(self)

    def _contactsSwitch(self, btn):
        """ (Un)hide contacts panel """
        cpanel = self.host.contact_panel
        cpanel.setVisible(not cpanel.getVisible())
        btn.setText("<<" if cpanel.getVisible() else ">>")
        self.host.resize()

    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"));