diff src/browser/sat_browser/base_widget.py @ 467:97c72fe4a5f2

browser_side: import fixes: - moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly - refactored bad import practices: classes are most of time not imported directly, module is imported instead.
author Goffi <goffi@goffi.org>
date Mon, 09 Jun 2014 22:15:26 +0200
parents src/browser/base_widget.py@1eeed8028199
children 5d8632a7bfde
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/base_widget.py	Mon Jun 09 22:15:26 2014 +0200
@@ -0,0 +1,732 @@
+#!/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 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.FlexTable import FlexTable
+from pyjamas.ui.TabPanel import TabPanel
+from pyjamas.ui.HTMLPanel import HTMLPanel
+from pyjamas.ui.Label import Label
+from pyjamas.ui.Button import Button
+from pyjamas.ui.Image import Image
+from pyjamas.ui.Widget import Widget
+from pyjamas.ui.DragWidget import DragWidget
+from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.ui.ClickListener import ClickHandler
+from pyjamas.ui import HasAlignment
+from pyjamas import DOM
+from pyjamas import Window
+from __pyjamas__ import doc
+
+import dialog
+
+
+class DragLabel(DragWidget):
+
+    def __init__(self, text, _type):
+        DragWidget.__init__(self)
+        self._text = text
+        self._type = _type
+
+    def onDragStart(self, event):
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self._text, self._type))
+        dt.setDragImage(self.getElement(), 15, 15)
+
+
+class LiberviaDragWidget(DragLabel):
+    """ A DragLabel which keep the widget being dragged as class value """
+    current = None  # widget currently dragged
+
+    def __init__(self, text, _type, widget):
+        DragLabel.__init__(self, text, _type)
+        self.widget = widget
+
+    def onDragStart(self, event):
+        LiberviaDragWidget.current = self.widget
+        DragLabel.onDragStart(self, event)
+
+    def onDragEnd(self, event):
+        LiberviaDragWidget.current = None
+
+
+class DropCell(DropWidget):
+    """Cell in the middle grid which replace itself with the dropped widget on DnD"""
+    drop_keys = {}
+
+    def __init__(self, host):
+        DropWidget.__init__(self)
+        self.host = host
+        self.setStyleName('dropCell')
+
+    @classmethod
+    def addDropKey(cls, key, callback):
+        DropCell.drop_keys[key] = callback
+
+    def onDragEnter(self, event):
+        if self == LiberviaDragWidget.current:
+            return
+        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):
+        self.removeStyleName('dragover')
+        DOM.eventPreventDefault(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")
+            log.debug("message: %s" % item)
+            log.debug("type: %s" % item_type)
+        except:
+            log.debug("no message found")
+            item = '&nbsp;'
+            item_type = None
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            if self == _new_panel:  # We can't drop on ourself
+                return
+            # we need to remove the widget from the panel as it will be inserted elsewhere
+            widgets_panel = _new_panel.getWidgetsPanel()
+            wid_row = widgets_panel.getWidgetCoords(_new_panel)[0]
+            row_wids = widgets_panel.getLiberviaRowWidgets(wid_row)
+            if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]:
+                # the dropped widget is the only one in the same row
+                # as the target widget (self), we don't do anything
+                return
+            widgets_panel.removeWidget(_new_panel)
+        elif item_type in self.drop_keys:
+            _new_panel = self.drop_keys[item_type](self.host, item)
+        else:
+            log.warning("unmanaged item type")
+            return
+        if isinstance(self, LiberviaWidget):
+            self.host.unregisterWidget(self)
+            self.onQuit()
+            if not isinstance(_new_panel, LiberviaWidget):
+                log.warning("droping an object which is not a class of LiberviaWidget")
+        _flextable = self.getParent()
+        _widgetspanel = _flextable.getParent().getParent()
+        row_idx, cell_idx = self._getCellAndRow(_flextable, event)
+        if self.host.getSelected == self:
+            self.host.setSelected(None)
+        _widgetspanel.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 WidgetHeader(AbsolutePanel, LiberviaDragWidget):
+
+    def __init__(self, parent, title):
+        AbsolutePanel.__init__(self)
+        self.add(title)
+        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')
+        setting_button.addClickListener(parent.onSetting)
+        close_button = Image("media/icons/misc/close.png")
+        close_button.setStyleName('widgetHeader_closeButton')
+        close_button.addClickListener(parent.onClose)
+        button_group.add(setting_button)
+        button_group.add(close_button)
+        button_group_wrapper.setWidget(button_group)
+        self.add(button_group_wrapper)
+        self.addStyleName('widgetHeader')
+        LiberviaDragWidget.__init__(self, "", "WIDGET", parent)
+
+
+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()
+        self.__title = Label(title)
+        self.__title.setStyleName('widgetHeader_title')
+        self._close_listeners = []
+        header = WidgetHeader(self, self.__title)
+        self.add(header)
+        self.setSize('100%', '100%')
+        self.addStyleName('widget')
+        if self.__selectable:
+            self.addClickListener(self)
+
+            def onClose(sender):
+                """Check dynamically if the unibox is enable or not"""
+                if self.host.uni_box:
+                    self.host.uni_box.onWidgetClosed(sender)
+
+            self.addCloseListener(onClose)
+        self.host.registerWidget(self)
+
+    def getDebugName(self):
+        return "%s (%s)" % (self, self.__title.getText())
+
+    def getWidgetsPanel(self, expect=True):
+        return self.getParent(WidgetsPanel, expect)
+
+    def getParent(self, class_=None, expect=True):
+        """Return the closest ancestor of the specified class.
+
+        Note: this method overrides pyjamas.ui.Widget.getParent
+
+        @param class_: class of the ancestor to look for or None to return the first parent
+        @param expect: set to True if the parent is expected (print a message if not found)
+        @return: the parent/ancestor or None if it has not been found
+        """
+        current = Widget.getParent(self)
+        if class_ is None:
+            return current  # this is the default behavior
+        while current is not None and not isinstance(current, class_):
+            current = Widget.getParent(current)
+        if current is None and expect:
+            log.debug("Can't find parent %s for %s" % (class_, self))
+        return current
+
+    def onClick(self, sender):
+        self.host.setSelected(self)
+
+    def onClose(self, sender):
+        """ Called when the close button is pushed """
+        _widgetspanel = self.getWidgetsPanel()
+        _widgetspanel.removeWidget(self)
+        for callback in self._close_listeners:
+            callback(self)
+        self.onQuit()
+
+    def onQuit(self):
+        """ Called when the widget is actually ending """
+        pass
+
+    def addCloseListener(self, callback):
+        """Add a close listener to this widget
+        @param callback: function to be called from self.onClose"""
+        self._close_listeners.append(callback)
+
+    def refresh(self):
+        """This can be overwritten by a child class to refresh the display when,
+        instead of creating a new one, an existing widget is found and reused.
+        """
+        pass
+
+    def onSetting(self, sender):
+        widpanel = self.getWidgetsPanel()
+        row, col = widpanel.getIndex(self)
+        body = VerticalPanel()
+
+        # colspan & rowspan
+        colspan = widpanel.getColSpan(row, col)
+        rowspan = widpanel.getRowSpan(row, col)
+
+        def onColSpanChange(value):
+            widpanel.setColSpan(row, col, value)
+
+        def onRowSpanChange(value):
+            widpanel.setRowSpan(row, col, value)
+        colspan_setter = dialog.IntSetter("Columns span", colspan)
+        colspan_setter.addValueChangeListener(onColSpanChange)
+        colspan_setter.setWidth('100%')
+        rowspan_setter = dialog.IntSetter("Rows span", rowspan)
+        rowspan_setter.addValueChangeListener(onRowSpanChange)
+        rowspan_setter.setWidth('100%')
+        body.add(colspan_setter)
+        body.add(rowspan_setter)
+
+        # size
+        width_str = self.getWidth()
+        if width_str.endswith('px'):
+            width = int(width_str[:-2])
+        else:
+            width = 0
+        height_str = self.getHeight()
+        if height_str.endswith('px'):
+            height = int(height_str[:-2])
+        else:
+            height = 0
+
+        def onWidthChange(value):
+            if not value:
+                self.setWidth('100%')
+            else:
+                self.setWidth('%dpx' % value)
+
+        def onHeightChange(value):
+            if not value:
+                self.setHeight('100%')
+            else:
+                self.setHeight('%dpx' % value)
+        width_setter = dialog.IntSetter("width (0=auto)", width)
+        width_setter.addValueChangeListener(onWidthChange)
+        width_setter.setWidth('100%')
+        height_setter = dialog.IntSetter("height (0=auto)", height)
+        height_setter.addValueChangeListener(onHeightChange)
+        height_setter.setHeight('100%')
+        body.add(width_setter)
+        body.add(height_setter)
+
+        # reset
+        def onReset(sender):
+            colspan_setter.setValue(1)
+            rowspan_setter.setValue(1)
+            width_setter.setValue(0)
+            height_setter.setValue(0)
+
+        reset_bt = Button("Reset", onReset)
+        body.add(reset_bt)
+        body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER)
+
+        _dialog = dialog.GenericDialog("Widget setting", body)
+        _dialog.show()
+
+    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 getWarningData(self):
+        """ Return exposition warning level when this widget is selected and something is sent to it
+        This method should be overriden by children
+        @return: tuple (warning level type/HTML msg). Type can be one of:
+            - PUBLIC
+            - GROUP
+            - ONE2ONE
+            - MISC
+            - NONE
+        """
+        if not self.__selectable:
+            log.error("getWarningLevel must not be called for an unselectable widget")
+            raise Exception
+        # TODO: cleaner warning types (more general constants)
+        return ("NONE", None)
+
+    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%')
+
+    def doDetachChildren(self):
+        # We need to force the use of a panel subclass method here,
+        # for the same reason as doAttachChildren
+        VerticalPanel.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
+        VerticalPanel.doAttachChildren(self)
+
+    def matchEntity(self, entity):
+        """This method should be overwritten by child classes."""
+        raise NotImplementedError
+
+
+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(''))
+        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 WidgetsPanel(ScrollPanelWrapper):
+
+    def __init__(self, host, locked=False):
+        ScrollPanelWrapper.__init__(self)
+        self.setSize('100%', '100%')
+        self.host = host
+        self.locked = locked  # if True: tab will not be removed when there are no more widgets inside
+        self.selected = None
+        self.flextable = FlexTable()
+        self.flextable.setSize('100%', '100%')
+        self.setWidget(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
+        td_elt = _bottom.getElement().parentNode
+        DOM.setStyleAttribute(td_elt, "height", "1px")  # needed so the cell adapt to the size of the border (specially in webkit)
+        self._max_cols = 1  # give the maximum number of columns i a raw
+
+    def isLocked(self):
+        return self.locked
+
+    def changeWidget(self, row, col, wid):
+        """Change the widget in the given location, add row or columns when necessary"""
+        log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col))
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        try:
+            prev_wid = self.flextable.getWidget(row, col)
+        except:
+            log.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
+            log.debug("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
+
+            elif isinstance(prev_wid, LeftBorderWidget):
+                if col != 0:
+                    log.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:
+                    log.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:
+            self._max_cols = _max_cols
+            self._sizesAdjust()
+
+    def _sizesAdjust(self):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        width = 100.0 / max(1, self._max_cols - 2)  # we don't count the borders
+
+        for row_idx in xrange(self.flextable.getRowCount()):
+            for col_idx in xrange(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)
+
+        last_row = max(0, self.flextable.getRowCount() - 1)
+        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)
+        log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0))
+        self.changeWidget(last_row, 0, wid)
+
+    def removeWidget(self, wid):
+        """Remove a widget and the cell where it is"""
+        _row, _col = self.flextable.getIndex(wid)
+        self.flextable.remove(wid)
+        self.flextable.removeCell(_row, _col)
+        if not self.getLiberviaRowWidgets(_row):  # we have no more widgets, we remove the row
+            self.flextable.removeRow(_row)
+        _max_cols = 1
+        for row_idx in xrange(self.flextable.getRowCount()):
+            _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx))
+        if _max_cols != self._max_cols:
+            self._max_cols = _max_cols
+            self._sizesAdjust()
+        current = self
+
+        blank_page = self.getLiberviaWidgetsCount() == 0  # do we still have widgets on the page ?
+
+        if blank_page and not self.isLocked():
+            # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed
+            while current is not None:
+                if isinstance(current, MainTabPanel):
+                    current.onWidgetPanelRemove(self)
+                    return
+                current = current.getParent()
+            log.error("no MainTabPanel found !")
+
+    def getWidgetCoords(self, wid):
+        return self.flextable.getIndex(wid)
+
+    def getLiberviaRowWidgets(self, row):
+        """ Return all the LiberviaWidget in the row """
+        return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)]
+
+    def getRowWidgets(self, row):
+        """ Return all the widgets in the row """
+        widgets = []
+        cols = self.flextable.getCellCount(row)
+        for col in xrange(cols):
+            widgets.append(self.flextable.getWidget(row, col))
+        return widgets
+
+    def getLiberviaWidgetsCount(self):
+        """ Get count of contained widgets """
+        return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)])
+
+    def getIndex(self, wid):
+        return self.flextable.getIndex(wid)
+
+    def getColSpan(self, row, col):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.getColSpan(row, col)
+
+    def setColSpan(self, row, col, value):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.setColSpan(row, col, value)
+
+    def getRowSpan(self, row, col):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.getRowSpan(row, col)
+
+    def setRowSpan(self, row, col, value):
+        cellFormatter = self.flextable.getFlexCellFormatter()
+        return cellFormatter.setRowSpan(row, col, value)
+
+
+class DropTab(Label, DropWidget):
+
+    def __init__(self, tab_panel, text):
+        Label.__init__(self, text)
+        DropWidget.__init__(self, tab_panel)
+        self.tab_panel = tab_panel
+        self.setStyleName('dropCell')
+        self.setWordWrap(False)
+        DOM.setStyleAttribute(self.getElement(), "min-width", "30px")
+
+    def _getIndex(self):
+        """ get current index of the DropTab """
+        # XXX: awful hack, but seems the only way to get index
+        return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1
+
+    def onDragEnter(self, event):
+        #if self == LiberviaDragWidget.current:
+        #    return
+        self.addStyleName('dragover')
+        DOM.eventPreventDefault(event)
+
+    def onDragLeave(self, event):
+        self.removeStyleName('dragover')
+
+    def onDragOver(self, event):
+        DOM.eventPreventDefault(event)
+
+    def onDrop(self, event):
+        DOM.eventPreventDefault(event)
+        self.removeStyleName('dragover')
+        if self._getIndex() == self.tab_panel.tabBar.getSelectedTab():
+            # the widget come from the DragTab, so nothing to do, we let it there
+            return
+
+        # FIXME: quite the same stuff as in DropCell, need some factorisation
+        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")
+            log.debug("message: %s" % item)
+            log.debug("type: %s" % item_type)
+        except:
+            log.debug("no message found")
+            item = '&nbsp;'
+            item_type = None
+        if item_type == "WIDGET":
+            if not LiberviaDragWidget.current:
+                log.error("No widget registered in LiberviaDragWidget !")
+                return
+            _new_panel = LiberviaDragWidget.current
+            _new_panel.getWidgetsPanel().removeWidget(_new_panel)
+        elif item_type in DropCell.drop_keys:
+            _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item)
+        else:
+            log.warning("unmanaged item type")
+            return
+
+        widgets_panel = self.tab_panel.getWidget(self._getIndex())
+        widgets_panel.addWidget(_new_panel)
+
+
+class MainTabPanel(TabPanel):
+
+    def __init__(self, host):
+        TabPanel.__init__(self)
+        self.host = host
+        self.tabBar.setVisible(False)
+        self.setStyleName('liberviaTabPanel')
+        self.addStyleName('mainTabPanel')
+        Window.addWindowResizeListener(self)
+
+    def getCurrentPanel(self):
+        """ Get the panel of the currently selected tab """
+        return self.deck.visibleWidget
+
+    def onWindowResized(self, width, height):
+        tab_panel_elt = self.getElement()
+        _elts = doc().getElementsByClassName('gwt-TabBar')
+        if not _elts.length:
+            log.error("no TabBar found, it should exist !")
+            tab_bar_h = 0
+        else:
+            tab_bar_h = _elts.item(0).offsetHeight
+        ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5
+        ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5
+        self.setWidth("%s%s" % (ideal_width, "px"))
+        self.setHeight("%s%s" % (ideal_height, "px"))
+
+    def add(self, widget, text=''):
+        tab = DropTab(self, text)
+        TabPanel.add(self, widget, tab, False)
+        if self.getWidgetCount() > 1:
+            self.tabBar.setVisible(True)
+            self.host.resize()
+
+    def onWidgetPanelRemove(self, panel):
+        """ Called when a child WidgetsPanel is empty and need to be removed """
+        self.remove(panel)
+        widgets_count = self.getWidgetCount()
+        if widgets_count == 1:
+            self.tabBar.setVisible(False)
+            self.host.resize()
+            self.selectTab(0)
+        else:
+            self.selectTab(widgets_count - 1)