view browser_side/panels.py @ 85:a8f027738c16

browser side: widgets cells can now be added by putting a widget on a border
author Goffi <goffi@goffi.org>
date Mon, 27 Jun 2011 03:14:37 +0200
parents 8f35e9970e7f
children 6c3b3254605f
line wrap: on
line source

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

"""
Libervia: a Salut à Toi frontend
Copyright (C) 2011  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.ScrollPanel import ScrollPanel
from pyjamas.ui.TabPanel import TabPanel
from pyjamas.ui.HTMLPanel import HTMLPanel
from pyjamas.ui.FlexTable import FlexTable
from pyjamas.ui.Grid import Grid
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.DropWidget import DropWidget
from pyjamas.ui.ClickListener import ClickHandler
from pyjamas.ui.KeyboardListener import KEY_ENTER
from pyjamas.ui import HasAlignment
from pyjamas.Timer import Timer
from pyjamas import Window
from pyjamas import DOM
from card_game import CardPanel
from menu import Menu
from jid import JID
from tools import html_sanitize
from datetime import datetime
from time import time
import dialog
from __pyjamas__ import doc

class DropCell(DropWidget):
    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
    
    def __init__(self, host):
        DropWidget.__init__(self)
        self.host = host
        self.setStyleName('dropCell')
    
    def onDragEnter(self, event):
        self.addStyleName('dragover')
        DOM.eventPreventDefault(event)

    def onDragLeave(self, event):
        if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\
           event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth()-1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight()-1:
           #We check that we are inside widget's box, and we don't remove the style in this case because
           #if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we
           #don't want that
            self.removeStyleName('dragover')

    def onDragOver(self, event):
        DOM.eventPreventDefault(event)

    def _getCellAndRow(self, grid, event):
        """Return cell and row index where the event is occuring"""
        cell = grid.getEventTargetCell(event)
        row = DOM.getParent(cell)
        return (row.rowIndex, cell.cellIndex)


    def onDrop(self, event):
        dt = event.dataTransfer
        #'text', 'text/plain', and 'Text' are equivalent.
        try:
            item, item_type = dt.getData("text/plain").split('\n') #Workaround for webkit, only text/plain seems to be managed
            if item_type and item_type[-1] == '\0': #Workaround for what looks like a pyjamas bug: the \0 should not be there, and 
                item_type = item_type[:-1]          # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
            #item_type = dt.getData("type")
            print "message: %s" % item
            print "type: %s" % item_type
        except:
            print "no message found"
            item='&nbsp;'
            item_type = None
        DOM.eventPreventDefault(event)
        if item_type=="GROUP":
            _new_panel = MicroblogPanel(self.host, item)
            _new_panel.setAcceptedGroup(item)
            self.host.FillMicroblogPanel(_new_panel)
        elif item_type=="CONTACT":
            _contact = JID(item)
            self.host.contact_panel.setContactMessageWaiting(_contact.bare, False)
            _new_panel = ChatPanel(self.host, _contact)
            _new_panel.historyPrint()
        elif item_type=="CONTACT_TITLE":
            _new_panel = MicroblogPanel(self.host, accept_all=True)
            self.host.FillMicroblogPanel(_new_panel)
        else:
            return False
        if isinstance(self, LiberviaWidget):
            self.host.unregisterWidget(self)
            if not isinstance(_new_panel, LiberviaWidget):
                print ('WARNING: droping an object which is not a class of LiberviaWidget')
        _flextable = self.getParent()
        _widgetpanel = _flextable.getParent()
        row_idx, cell_idx = self._getCellAndRow(_flextable, event)
        if self.host.selected == self:
            self.host.select(None)
        _widgetpanel.changeWidget(row_idx, cell_idx, _new_panel)
        """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable))
        _width = 90/float(len(_unempty_panels) or 1)
        #now we resize all the cell of the column
        for panel in _unempty_panels:
            td_elt = panel.getElement().parentNode
            DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)"""
        #FIXME: delete object ? Check the right way with pyjamas

class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
    """Libervia's widget which can replace itself with a dropped widget on DnD"""
    
    def __init__(self, host, title='', selectable=False):
        """Init the widget
        @param host: SatWebFrontend object
        @param title: title show in the header of the widget
        @param selectable: True is widget can be selected by user"""
        VerticalPanel.__init__(self)
        DropCell.__init__(self, host)
        ClickHandler.__init__(self)
        self.__selectable = selectable
        self.__title_id = HTMLPanel.createUniqueId()
        self.__setting_button_id = HTMLPanel.createUniqueId()
        self.__close_button_id = HTMLPanel.createUniqueId()
        header = AbsolutePanel()
        self.__title = Label(title)
        self.__title.setStyleName('widgetHeader_title')
        header.add(self.__title)
        #header.setCellVerticalAlignment(self.title, HasAlignment.ALIGN_MIDDLE)
        #header.setCellWidth(self.title, '100%')
        button_group_wrapper = SimplePanel()
        button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
        button_group = HorizontalPanel()
        button_group.setStyleName('widgetHeader_buttonGroup')
        setting_button = Image("media/icons/misc/settings.png")
        setting_button.setStyleName('widgetHeader_settingButton')
        close_button = Image("media/icons/misc/close.png")
        close_button.setStyleName('widgetHeader_closeButton')
        button_group.add(setting_button)
        button_group.add(close_button)
        button_group_wrapper.setWidget(button_group)
        header.add(button_group_wrapper)
        self.add(header)
        header.addStyleName('widgetHeader')
        self.setSize('100%', '100%')
        self.addStyleName('widget')
        if self.__selectable:
            self.addClickListener(self)
        self.host.registerWidget(self)
    
    def onClick(self, sender):
        self.host.select(self)

    def setTitle(self, text):
        """change the title in the header of the widget
        @param text: text of the new title"""
        self.__title.setText(text)

    def isSelectable(self):
        return self.__selectable

    def setSelectable(self, selectable):
        if not self.__selectable:
            try:
                self.removeClickListener(self)
            except ValueError:
                pass
        if self.selectable and not self in self._clickListeners:
            self.addClickListener(self)
        self.__selectable = selectable

    def setWidget(self, widget, scrollable=True):
        """Set the widget that will be in the body of the LiberviaWidget
        @param widget: widget to put in the body
        @param scrollable: if true, the widget will be in a ScrollPanelWrapper"""
        if scrollable:
            _scrollpanelwrapper = ScrollPanelWrapper()
            _scrollpanelwrapper.setStyleName('widgetBody')
            _scrollpanelwrapper.setWidget(widget)
            body_wid = _scrollpanelwrapper
        else:
            body_wid = widget
        self.add(body_wid)
        self.setCellHeight(body_wid, '100%')


class ScrollPanelWrapper(SimplePanel):
    """Scroll Panel like component, wich use the full available space
    to work around percent size issue, it use some of the ideas found
    here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316
    specially in code given at comment #46, thanks to Stefan Bachert"""

    def __init__(self, *args, **kwargs):
        SimplePanel.__init__(self)
        self.spanel = ScrollPanel(*args, **kwargs)
        SimplePanel.setWidget(self, self.spanel)
        DOM.setStyleAttribute(self.getElement(), "position", "relative")
        DOM.setStyleAttribute(self.getElement(), "top", "0px")
        DOM.setStyleAttribute(self.getElement(), "left", "0px")
        DOM.setStyleAttribute(self.getElement(), "width", "100%")
        DOM.setStyleAttribute(self.getElement(), "height", "100%")
        DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute")
        DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%")
        DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%")

    def setWidget(self, widget):
        self.spanel.setWidget(widget)

    def setScrollPosition(self, position):
        self.spanel.setScrollPosition(position)

    def scrollToBottom(self):
        self.setScrollPosition(self.spanel.getElement().scrollHeight)

class EmptyWidget(DropCell, SimplePanel):
    """Empty dropable panel"""

    def __init__(self, host):
        SimplePanel.__init__(self)
        DropCell.__init__(self, host)
        self.setWidget(HTML('&nbsp;'))
        self.setSize('100%','100%')

class BorderWidget(EmptyWidget):
    def __init__(self, host):
        EmptyWidget.__init__(self, host)
        self.addStyleName('borderPanel')

class LeftBorderWidget(BorderWidget):
    def __init__(self, host):
        BorderWidget.__init__(self, host)
        self.addStyleName('leftBorderWidget')

class RightBorderWidget(BorderWidget):
    def __init__(self, host):
        BorderWidget.__init__(self, host)
        self.addStyleName('rightBorderWidget')

class BottomBorderWidget(BorderWidget):
    def __init__(self, host):
        BorderWidget.__init__(self, host)
        self.addStyleName('bottomBorderWidget')

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): #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._popup = None
        self._timer = Timer(notify=self._timeCb)
        self.host = host
        self.setStyleName('uniBox')
        self.addKeyboardListener(self)

    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):
        type, target = target_data
        if type == "PUBLIC":
            msg = "This message will be PUBLIC and everybody will be able to see it, even people you don't know"
            style = "targetPublic"
        elif type == "GROUP":
            msg = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" % (target or '')
            style = "targetGroup"
        elif type == "STATUS":
            msg = "This will be your new status message"
            style = "targetStatus"
        elif type == "ONE2ONE":
            msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % target
            style = "targetOne2One"
        else:
            print "WARNING: undetermined target for this message"
            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.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 (target_type, target info)"""
        type = None
        target = None
        if txt.startswith('@@: '):
            type = "PUBLIC"
        elif txt.startswith('@'):
            type = "GROUP"
            _end = txt.find(': ')
            if _end == -1:
                type = "STATUS"
            else:
                target = txt[1:_end] #only one target group is managed for the moment
                if not target in self.host.contact_panel.getGroups():
                    target = None
        elif self.host.selected == None:
            type = "STATUS"
        elif isinstance(self.host.selected, ChatPanel):
            type = "ONE2ONE"
            target = str(self.host.selected.target)
        else:
            print "Unknown selected host:",self.host.selected
            type = "UNKNOWN"
        return (type, target)

    def onKeyPress(self, sender, keycode, modifiers):
        _txt = self.getText()
        if not self._popup:
            self.showWarning(self._getTarget(_txt))
        else:
            _target = self._getTarget(_txt)
            if _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:
                if _txt.startswith('@'):
                    self.host.bridge.call('sendMblog', None, self.getText())
                elif self.host.selected == None:
                    self.host.bridge.call('setStatus', None, _txt)
                elif isinstance(self.host.selected, ChatPanel):
                    _chat = self.host.selected
                    mess_type = "groupchat" if _chat.type=='group' else "chat"
                    self.host.bridge.call('sendMessage', None, str(_chat.target), _txt, '', mess_type)
            self.setText('')
            self._timeCb(None) #we remove the popup

    """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 MicroblogEntry(SimplePanel):

    def __init__(self, mblog_entry):
        SimplePanel.__init__(self)

        _datetime = datetime.fromtimestamp(mblog_entry.timestamp)

        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_body'>%(body)s</div>" %
            {"author": html_sanitize(mblog_entry.author),
            "timestamp": _datetime,
            "body": html_sanitize(mblog_entry.content)}
            )
        panel.setStyleName('microblogEntry')
        self.add(panel)

class MicroblogPanel(LiberviaWidget):

    def __init__(self, host, title='', accept_all=False):
        """Panel used to show microblog
        @param title: title of the panel
        @param accept_all: if true, show every message, without filtering jids"""
        LiberviaWidget.__init__(self, host, title)
        #ScrollPanelWrapper.__init__(self)
        #DropCell.__init__(self)
        self.accept_all = accept_all
        self.accepted_groups = []
        self.entries = {}
        self.vpanel = VerticalPanel()
        self.vpanel.setStyleName('microblogPanel')
        self.setWidget(self.vpanel)

    def addEntry(self, mblog_entry):
        """Add an entry to the panel
        @param text: main text of the entry
        @param id: unique id of the entry
        @param author: who wrote the entry
        @param date: when the entry was written"""
        if mblog_entry.id in self.entries:
            return
        _entry = MicroblogEntry(mblog_entry)
        self.entries[mblog_entry.id] = _entry
        self.vpanel.insert(_entry,0)

    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 show 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.select(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 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(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"""
        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 = 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
        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
        ScrollPanelWrapper.doAttachChildren(self)"""

    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):
            stamps=history.keys()
            stamps.sort()
            for stamp in stamps:
                self.printMessage(history[stamp][0], history[stamp][1], stamp)
        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, 20)
   
    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"""
        print "print message:",msg
        _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":
            self.tarot_panel = CardPanel(self, referee, players, self.nick)
            self.vpanel.insert(self.tarot_panel, 1)
            self.vpanel.setCellHeight(self.tarot_panel, self.tarot_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 

class WidgetsPanel(SimplePanel):
    
    def __init__(self, host):
        SimplePanel.__init__(self)
        self.host = host
        self.flextable = FlexTable()
        self.flextable.setSize('100%','100%')
        self.add(self.flextable)
        self.setStyleName('widgetsPanel')
        _bottom = BottomBorderWidget(self.host)
        self.flextable.setWidget(0, 0, _bottom) #There will be always an Empty widget on the last row,
                                                                      #dropping a widget there will add a new row
        self._max_cols = 1 #give the maximum number of columns i a raw

    def changeWidget(self, row, col, wid):
        """Change the widget in the given location, add row or columns when necessary"""
        print "changing widget:", wid, row, col
        last_row = max(0, self.flextable.getRowCount()-1)
        try:
            prev_wid = self.flextable.getWidget(row, col)
        except:
            print "ERROR: Trying to change an unexisting widget !"
            return

        cellFormatter = self.flextable.getFlexCellFormatter()

        if isinstance(prev_wid, BorderWidget):
            #We are on a border, we must create a row and/or columns
            print "BORDER WIDGET"
            prev_wid.removeStyleName('dragover')
            
            if isinstance(prev_wid, BottomBorderWidget):
                #We are on the bottom border, we create a new row
                self.flextable.insertRow(last_row)
                self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host))
                self.flextable.setWidget(last_row, 1, wid)
                self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host))
                cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT)
                row = last_row
                last_row+=1

            elif isinstance(prev_wid, LeftBorderWidget):
                if col!=0:
                    print "ERROR: LeftBorderWidget must be on the first column !"
                    return
                self.flextable.insertCell(row, col+1)
                self.flextable.setWidget(row, 1, wid)

            elif isinstance(prev_wid, RightBorderWidget):
                if col!=self.flextable.getCellCount(row)-1:
                    print "ERROR: RightBorderWidget must be on the last column !"
                    return
                self.flextable.insertCell(row, col)
                self.flextable.setWidget(row, col, wid)

        else:
            prev_wid.removeFromParent()
            self.flextable.setWidget(row, col, wid)
        
        _max_cols = max(self._max_cols, self.flextable.getCellCount(row))
        if _max_cols != self._max_cols:
            #we hage to adjust sizes
            self._max_cols = _max_cols
            width = 100.0/max(1, self._max_cols-2) #we don't count the borders
            for row_idx in range(self.flextable.getRowCount()):
                for col_idx in range(self.flextable.getCellCount(row_idx)):
                    _widget = self.flextable.getWidget(row_idx, col_idx)
                    if not isinstance(_widget, BorderWidget):
                        td_elt = _widget.getElement().parentNode
                        DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width)

            cellFormatter.setColSpan(last_row, 0, self._max_cols)

    
    def addWidget(self, wid):
        """Add a widget to a new cell on the next to last row"""
        last_row = max(0, self.flextable.getRowCount()-1)
        print "putting widget %s at %d, %d" % (wid, last_row, 0)
        self.changeWidget(last_row, 0, wid)

class MainDiscussionPanel(HorizontalPanel):
    
    def __init__(self, host):
        self.host=host
        HorizontalPanel.__init__(self)
        self._left = self.host.contact_panel
        self._right = 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 MainTabPanel(TabPanel):

    def __init__(self, host):
        TabPanel.__init__(self)
        self.host=host
        self.tabBar.setVisible(False)
        self.setStyleName('mainTabPanel')
        Window.addWindowResizeListener(self)

    def onWindowResized(self, width, height):
        tab_panel_elt = self.getElement()
        _elts = doc().getElementsByClassName('gwt-TabBar')
        if not _elts.length:
            print ("ERROR: not TabBar found, it should exist !")
            tab_bar_h = 0
        else:
            tab_bar_h = _elts.item(0).offsetHeight
        ideal_height = Window.getClientHeight() - tab_panel_elt.offsetTop - tab_bar_h - 5
        self.setWidth("%s%s" % (width-5, "px"));
        self.setHeight("%s%s" % (ideal_height, "px"));

    def add(self, widget, tabText=None, asHTML=False):
        TabPanel.add(self, widget, tabText, asHTML)
        if self.getWidgetCount()>1:
            self.tabBar.setVisible(True)
            self.host.resize()
        
class MainPanel(AbsolutePanel):

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

        menu = Menu(host)
        unibox_panel = UniBoxPanel(host)
        self.host.setUniBox(unibox_panel.unibox)
        status = host.status_panel
        self.tab_panel = MainTabPanel(host)
        self.discuss_panel = MainDiscussionPanel(self.host)
        self.tab_panel.add(self.discuss_panel, "Discussions")
        self.tab_panel.selectTab(0)

        self.add(menu)
        self.add(unibox_panel)
        self.add(status)
        self.add(self.tab_panel)
        
        self.setWidth("100%")
        Window.addWindowResizeListener(self)

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