view browser_side/base_widget.py @ 197:0fc32122a877

browser side: dropped widget's row is now checked, to avoid the drop on a removed BorderWidget. This finish the previous commit. Widgets are now (hopefuly) safely movable
author Goffi <goffi@goffi.org>
date Wed, 06 Mar 2013 22:36:21 +0100
parents c2639c9f86ea
children ab239b3b67b3
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.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.DropWidget import DropWidget
from pyjamas.ui.ClickListener import ClickHandler
from pyjamas.ui import HasAlignment
from pyjamas import DOM
import dialog
from tools import LiberviaDragWidget
from pyjamas import Window
from __pyjamas__ import doc

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):
        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 == "WIDGET":
            if not LiberviaDragWidget.current:
                print "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
                self.removeStyleName('dragover')
                return
            widgets_panel.removeWidget(_new_panel)
        elif item_type in self.drop_keys:
            _new_panel = self.drop_keys[item_type](self.host,item)
        else:
            print "WARNING: unmanaged item type"
            return
        if isinstance(self, LiberviaWidget):
            self.host.unregisterWidget(self)
            self.onQuit()
            if not isinstance(_new_panel, LiberviaWidget):
                print ('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')
        header = WidgetHeader(self, self.__title)
        self.add(header)
        self.setSize('100%', '100%')
        self.addStyleName('widget')
        if self.__selectable:
            self.addClickListener(self)
        self.host.registerWidget(self)

    def getWidgetsPanel(self):
        current = self
        while current is not None and current.__class__ != WidgetsPanel:
            current = current.getParent()
        if current is None:
            print "Error: can't find WidgetsPanel"
        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)
        self.onQuit()

    def onQuit(self):
        """ Called when the widget is actually ending """
        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 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)

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

            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:
            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)
        print "putting widget %s at %d, %d" % (wid, 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()
            print "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 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:
            print ("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, tabText=None, asHTML=False):
        TabPanel.add(self, widget, tabText, asHTML)
        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)