diff src/browser/sat_browser/list_manager.py @ 679:a90cc8fc9605

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 16:15:18 +0100
parents 6d3142b782c3
children 9877607c719a
line wrap: on
line diff
--- a/src/browser/sat_browser/list_manager.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/list_manager.py	Wed Mar 18 16:15:18 2015 +0100
@@ -19,253 +19,326 @@
 
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from pyjamas.ui.Grid import Grid
 from pyjamas.ui.Button import Button
-from pyjamas.ui.ListBox import ListBox
 from pyjamas.ui.FlowPanel import FlowPanel
 from pyjamas.ui.AutoComplete import AutoCompleteTextBox
-from pyjamas.ui.Label import Label
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
-from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.DialogBox import DialogBox
 from pyjamas.ui.KeyboardListener import KEY_ENTER
-from pyjamas.ui.MouseListener import MouseHandler
-from pyjamas.ui.FocusListener import FocusHandler
-from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.ui.DragWidget import DragWidget
 from pyjamas.Timer import Timer
-from pyjamas import DOM
+
+import base_panel
+import base_widget
+import libervia_widget
 
-import base_panels
-import base_widget
+from sat_frontends.tools import jid
+
+
+unicode = str  # FIXME: pyjamas workaround
 
 # HTML content for the removal button (image or text)
-REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>'
+REMOVE_BUTTON = '<span class="itemRemoveIcon">x</span>'
+
 
-# Item to be considered for an empty list box selection.
-# Could be whatever which doesn't look like a JID or a group name.
-EMPTY_SELECTION_ITEM = ""
+# FIXME: dirty method and magic string to fix ASAP
+def tryJID(obj):
+    return jid.JID(obj) if (isinstance(obj, unicode) and not obj.startswith('@')) else obj
 
 
-class ListManager():
-    """A manager for sub-panels to assign elements to lists."""
+class ListManager(object):
+    """A base class to manage one or several lists of items."""
 
-    def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}):
-        """
-        @param parent: FlexTable parent widget for the manager
-        @param keys_dict: dict with the contact keys mapped to data
-        @param contact_list: list of string (the contact JID userhosts)
-        @param offsets: dict to set widget positions offset within parent
-        - "x_first": the x offset for the first widget's row on the grid
-        - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
-        - "y": the y offset for all widgets columns on the grid
+    def __init__(self, container, keys=None, items=None, offsets=None, style=None):
         """
-        self._parent = parent
-        if isinstance(keys_dict, set) or isinstance(keys_dict, list):
-            tmp = {}
-            for key in keys_dict:
-                tmp[key] = {}
-            keys_dict = tmp
-        self.__keys_dict = keys_dict
-        if isinstance(contact_list, set):
-            contact_list = list(contact_list)
-        self.__list = contact_list
-        self.__list.sort()
-        # store the list of contacts that are not assigned yet
-        self.__remaining_list = []
-        self.__remaining_list.extend(self.__list)
-        # mark a change to sort the list before it's used
-        self.__remaining_list_sorted = True
+        @param container (FlexTable): FlexTable parent widget
+        @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items
+            keys to their display config data.
+        @param items (list): list of items
+        @param offsets (dict): define widgets positions offsets within container:
+            - "x_first": the x offset for the first widget's row on the grid
+            - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
+            - "y": the y offset for all widgets columns on the grid
+        @param style (dict): define CSS styles
+        """
+        self.container = container
+        self.keys = {} if keys is None else keys
+        self.items = [] if items is None else items
+        self.items.sort()
+
+        # store the list of items that are not assigned yet
+        self.items_remaining = [item for item in self.items]
+        self.items_remaining_sorted = True
 
         self.offsets = {"x_first": 0, "x": 0, "y": 0}
-        if "x" in offsets and not "x_first" in offsets:
-            offsets["x_first"] = offsets["x"]
-        self.offsets.update(offsets)
+        if offsets is not None:
+            if "x" in offsets and "x_first" not in offsets:
+                offsets["x_first"] = offsets["x"]
+            self.offsets.update(offsets)
 
-        self.style = {
-           "keyItem": "recipientTypeItem",
-           "popupMenuItem": "recipientTypeItem",
-           "buttonCell": "recipientButtonCell",
-           "dragoverPanel": "dragover-recipientPanel",
-           "keyPanel": "recipientPanel",
-           "textBox": "recipientTextBox",
-           "textBox-invalid": "recipientTextBox-invalid",
-           "removeButton": "recipientRemoveButton",
-        }
-        self.style.update(style)
+        self.style = {"keyItem": "itemKey",
+                      "popupMenuItem": "itemKey",
+                      "buttonCell": "itemButtonCell",
+                      "keyPanel": "itemPanel",
+                      "textBox": "itemTextBox",
+                      "textBox-invalid": "itemTextBox-invalid",
+                      "removeButton": "itemRemoveButton",
+                      }
+        if style is not None:
+            self.style.update(style)
 
     def createWidgets(self, title_format="%s"):
-        """Fill the parent grid with all the widgets (some may be hidden during the initialization)."""
-        self.__children = {}
-        for key in self.__keys_dict:
-            self.addContactKey(key, title_format=title_format)
+        """Fill the container widget with one ListPanel per item key (some may be
+        hidden during the initialization).
 
-    def addContactKey(self, key, dict_={}, title_format="%s"):
-        if key not in self.__keys_dict:
-            self.__keys_dict[key] = dict_
-        # copy the key to its associated sub-map
-        self.__keys_dict[key]["title"] = key
-        self._addChild(self.__keys_dict[key], title_format)
+        @param title_format (unicode): format string for the title
+        """
+        self.children = {}
+        for key in self.keys:
+            self.addItemKey(key, title_format=title_format)
+
+    def addItemKey(self, key, data=None, title_format="%s"):
+        """Add to the container a Button and ListPanel for a new item key.
 
-    def removeContactKey(self, key):
-        """Remove a list panel and all its associated data."""
-        contacts = self.__children[key]["panel"].getContacts()
-        (y, x) = self._parent.getIndex(self.__children[key]["button"])
-        self._parent.removeRow(y)
-        del self.__children[key]
-        del self.__keys_dict[key]
-        self.addToRemainingList(contacts)
+        @param key (unicode): item key
+        @param data (dict{unicode: unicode}): config data
+        """
+        key_data = self.keys.setdefault(key, {})
+        if data is not None:
+            key_data.update(data)
+        key_data["title"] = key  # copy the key to its associated sub-map
 
-    def _addChild(self, entry, title_format):
-        """Add a button and FlowPanel for the corresponding map entry."""
-        button = Button(title_format % entry["title"])
+        button = Button(title_format % key)
         button.setStyleName(self.style["keyItem"])
-        if hasattr(entry, "desc"):
-            button.setTitle(entry["desc"])
-        if not "optional" in entry:
-            entry["optional"] = False
-        button.setVisible(not entry["optional"])
-        y = len(self.__children) + self.offsets["y"]
+        if hasattr(key_data, "desc"):
+            button.setTitle(key_data["desc"])
+        if "optional" not in key_data:
+            key_data["optional"] = False
+        button.setVisible(not key_data["optional"])
+        y = len(self.children) + self.offsets["y"]
         x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
 
-        self._parent.insertRow(y)
-        self._parent.setWidget(y, x, button)
-        self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
+        self.container.insertRow(y)
+        self.container.setWidget(y, x, button)
+        self.container.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
 
-        _child = ListPanel(self, entry, self.style)
-        self._parent.setWidget(y, x + 1, _child)
+        _child = ListPanel(self, key_data, self.style)
+        self.container.setWidget(y, x + 1, _child)
 
-        self.__children[entry["title"]] = {}
-        self.__children[entry["title"]]["button"] = button
-        self.__children[entry["title"]]["panel"] = _child
+        self.children[key] = {}
+        self.children[key]["button"] = button
+        self.children[key]["panel"] = _child
 
         if hasattr(self, "popup_menu"):
-            # this is done if self.registerPopupMenuPanel has been called yet
+            # self.registerPopupMenuPanel has been called yet
             self.popup_menu.registerClickSender(button)
 
-    def _refresh(self, visible=True):
-        """Set visible the sub-panels that are non optional or non empty, hide the rest."""
-        for key in self.__children:
-            self.setContactPanelVisible(key, False)
-        if not visible:
+    def removeItemKey(self, key):
+        """Remove from the container a ListPanel representing an item key, and all
+        its associated data.
+
+        @param key (unicode): item key
+        """
+        items = self.children[key]["panel"].getItems()
+        (y, x) = self.container.getIndex(self.children[key]["button"])
+        self.container.removeRow(y)
+        del self.children[key]
+        del self.keys[key]
+        self.addToRemainingList(items)
+
+    def refresh(self, hide_everything=False):
+        """Set visible the sub-panels that are non optional or non empty, hide
+        the rest. Setting the attribute "hide_everything" to True you can also
+        hide everything.
+
+        @param hide_everything (boolean): set to True to hide everything
+        """
+        for key in self.children:
+            self.setItemPanelVisible(key, False)
+        if hide_everything:
             return
-        _map = self.getContacts()
-        for key in _map:
-            if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]:
-                self.setContactPanelVisible(key, True)
+        for key, items in self.getItemsByKey().iteritems():
+            if len(items) > 0 or not self.keys[key]["optional"]:
+                self.setItemPanelVisible(key, True)
 
     def setVisible(self, visible):
-        self._refresh(visible)
+        self.refresh(not visible)
+
+    def setItemPanelVisible(self, key, visible=True, sender=None):
+        """Set the item key's widgets visibility.
 
-    def setContactPanelVisible(self, key, visible=True, sender=None):
-        """Do not remove the "sender" param as it is needed for the context menu."""
-        self.__children[key]["button"].setVisible(visible)
-        self.__children[key]["panel"].setVisible(visible)
-
-    @property
-    def list(self):
-        """Return the full list of potential contacts."""
-        return self.__list
+        @param key (unicode): item key
+        @param visible (bool): set to True to display the widgets
+        @param sender
+        """
+        self.children[key]["button"].setVisible(visible)
+        self.children[key]["panel"].setVisible(visible)
 
     @property
-    def keys(self):
-        return self.__keys_dict.keys()
-
-    @property
-    def keys_dict(self):
-        return self.__keys_dict
-
-    @property
-    def remaining_list(self):
-        """Return the contacts that have not been selected yet."""
-        if not self.__remaining_list_sorted:
-            self.__remaining_list_sorted = True
-            self.__remaining_list.sort()
-        return self.__remaining_list
+    def items_remaining(self):
+        """Return the unused items."""
+        if not self.items_remaining_sorted:
+            self.items_remaining.sort()
+            self.items_remaining_sorted = True
+        return self.items_remaining
 
     def setRemainingListUnsorted(self):
-        """Mark a change (deletion) so the list will be sorted before it's used."""
-        self.__remaining_list_sorted = False
+        """Mark the list of unused items as being unsorted."""
+        self.items_remaining_sorted = False
+
+    def removeFromRemainingList(self, items):
+        """Remove some items from the list of unused items.
 
-    def removeFromRemainingList(self, contacts):
-        """Remove contacts after they have been added to a sub-panel."""
-        if not isinstance(contacts, list):
-            contacts = [contacts]
-        for contact_ in contacts:
-            if contact_ in self.__remaining_list:
-                self.__remaining_list.remove(contact_)
+        @param items (list): items to be removed
+        """
+        for item in items:
+            if item in self.items_remaining:
+                self.items_remaining.remove(item)
 
-    def addToRemainingList(self, contacts, ignore_key=None):
-        """Add contacts after they have been removed from a sub-panel."""
-        if not isinstance(contacts, list):
-            contacts = [contacts]
-        assigned_contacts = set()
-        assigned_map = self.getContacts()
-        for key_ in assigned_map.keys():
-            if ignore_key is not None and key_ == ignore_key:
+    def addToRemainingList(self, items, ignore_key=None):
+        """Add some items to the list of unused items. Check first if the
+        items are really not used in any ListPanel.
+
+        @param items (list): items to be removed
+        @param ignore_key (unicode): item key to be ignored while checking
+        """
+        items_assigned = set()
+        for key, current_items in self.getItemsByKey().iteritems():
+            if ignore_key is not None and key == ignore_key:
                 continue
-            assigned_contacts.update(assigned_map[key_])
-        for contact_ in contacts:
-            if contact_ not in self.__list or contact_ in self.__remaining_list:
+            items_assigned.update(current_items)
+        for item in items:
+            if item not in self.items or item in self.items_remaining or item in items_assigned:
                 continue
-            if contact_ in assigned_contacts:
-                continue  # the contact is assigned somewhere else
-            self.__remaining_list.append(contact_)
+            self.items_remaining.append(item)
             self.setRemainingListUnsorted()
 
-    def setContacts(self, _map={}):
-        """Set the contacts for each contact key."""
-        for key in self.__keys_dict:
-            if key in _map:
-                self.__children[key]["panel"].setContacts(_map[key])
+    def resetItems(self, data={}):
+        """Repopulate all the lists (one per item key) with the given items.
+
+        @param data (dict{unicode: list}): dict binding items keys to items.
+        """
+        for key in self.keys:
+            if key in data:
+                self.children[key]["panel"].resetItems(data[key])
             else:
-                self.__children[key]["panel"].setContacts([])
-        self._refresh()
+                self.children[key]["panel"].resetItems([])
+        self.refresh()
+
+    def getItemsByKey(self):
+        """Get all the items by key.
 
-    def getContacts(self):
-        """Get the contacts for all the lists.
-        @return: a mapping between keys and contact lists."""
-        _map = {}
-        for key in self.__children:
-            _map[key] = self.__children[key]["panel"].getContacts()
-        return _map
+        @return: dict{unicode: set}
+        """
+        return {key: self.children[key]["panel"].getItems() for key in self.children}
+
+    def getKeysByItem(self):
+        """Get all the keys by item.
 
-    @property
-    def target_drop_cell(self):
-        """@return: the panel where something has been dropped."""
-        return self._target_drop_cell
-
-    def setTargetDropCell(self, target_drop_cell):
-        """@param: target_drop_cell: the panel where something has been dropped."""
-        self._target_drop_cell = target_drop_cell
+        @return: dict{object: set(unicode)}
+        """
+        result = {}
+        for key in self.children:
+            for item in self.children[key]["panel"].getItems():
+                result.setdefault(item, set()).add(key)
+        return result
 
     def registerPopupMenuPanel(self, entries, hide, callback):
-        "Register a popup menu panel that will be bound to all contact keys elements."
-        self.popup_menu = base_panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]})
+        """Register a popup menu panel for the item keys buttons.
+
+        @param entries (dict{unicode: dict{unicode: unicode}}): menu entries
+        @param hide (callable): method to call in order to know if a menu item
+            should be hidden from the menu. Takes in the button widget and the
+            item key and returns a boolean.
+        @param callback (callable): common callback for all menu items, takes in
+            the button widget and the item key.
+        """
+        self.popup_menu = base_panel.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]})
 
 
-class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.DragLabel, MouseHandler, FocusHandler):
-    """A draggable AutoCompleteTextBox which is used for representing a contact.
-    This class is NOT generic because of the onDragEnd method which call methods
-    from ListPanel. It's probably not reusable for another scenario.
-    """
+class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget):
+    """A draggable AutoCompleteTextBox which is used for representing an item."""
+
+    def __init__(self, list_panel, event_cbs, style):
+        """
 
-    def __init__(self, parent, event_cbs, style):
+        @param list_panel (ListPanel)
+        @param event_cbs (list[callable])
+        @param style (dict)
+        """
         AutoCompleteTextBox.__init__(self)
-        base_widget.DragLabel.__init__(self, '', 'CONTACT_TEXTBOX')  # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
-        self._parent = parent
+        DragWidget.__init__(self)
+        self.list_panel = list_panel
         self.event_cbs = event_cbs
         self.style = style
+        self.addStyleName(style["textBox"])
+        self.reset()
+
+        # Parent classes already init self as an handler for these events
         self.addMouseListener(self)
         self.addFocusListener(self)
         self.addChangeListener(self)
-        self.addStyleName(style["textBox"])
-        self.reset()
+
+    def onDragStart(self, event):
+        """The user starts dragging the text box."""
+        self.list_panel.manager.target_drop_cell = None
+        self.setSelectionRange(len(self.getText()), 0)
+
+        dt = event.dataTransfer
+        dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX"))
+        dt.setDragImage(self.getElement(), 15, 15)
+
+    def onDragEnd(self, event):
+        """The user dropped the text box."""
+        target = self.list_panel.manager.target_drop_cell  # parent or another ListPanel
+        if self.getText() == "" or target is None:
+            return
+        self.event_cbs["drop"](self, target)
+
+    def onClick(self, sender):
+        """The choices list is clicked"""
+        assert sender == self.choices
+        AutoCompleteTextBox.onClick(self, sender)
+        self.validate()
+
+    def onChange(self, sender):
+        """The list selection or the text has been changed"""
+        assert sender == self.choices or sender == self
+        if sender == self.choices:
+            AutoCompleteTextBox.onChange(self, sender)
+        self.validate()
+
+    def onKeyUp(self, sender, keycode, modifiers):
+        """Listen for key stroke"""
+        assert sender == self
+        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
+        if keycode == KEY_ENTER:
+            self.validate()
+
+    def onMouseMove(self, sender):
+        """Mouse enters the area of a DragAutoCompleteTextBox."""
+        assert sender == self
+        if hasattr(sender, "remove_btn"):
+            sender.remove_btn.setVisible(True)
+
+    def onMouseLeave(self, sender):
+        """Mouse leaves the area of a DragAutoCompleteTextBox."""
+        assert sender == self
+        if hasattr(sender, "remove_btn"):
+            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
+
+    def onFocus(self, sender):
+        """The DragAutoCompleteTextBox has the focus."""
+        assert sender == self
+        # FIXME: this raises runtime JS error "Permission denied to access property..." when you drag the object
+        #sender.setSelectionRange(0, len(sender.getText()))
+        sender.event_cbs["focus"](sender)
 
     def reset(self):
+        """Reset the text box"""
         self.setText("")
         self.setValid()
 
     def setValid(self, valid=True):
+        """Change the style according to the text validity."""
         if self.getText() == "":
             valid = True
         if valid:
@@ -274,138 +347,82 @@
             self.addStyleName(self.style["textBox-invalid"])
         self.valid = valid
 
-    def onDragStart(self, event):
-        self._text = self.getText()
-        base_widget.DragLabel.onDragStart(self, event)
-        self._parent.setTargetDropCell(None)
+    def validate(self):
+        """Check if the text is valid, update the style."""
         self.setSelectionRange(len(self.getText()), 0)
-
-    def onDragEnd(self, event):
-        target = self._parent.target_drop_cell  # parent or another ListPanel
-        if self.getText() == "" or target is None:
-            return
-        self.event_cbs["drop"](self, target)
+        self.event_cbs["validate"](self)
 
     def setRemoveButton(self):
+        """Add the remove button after the text box."""
 
         def remove_cb(sender):
-            """Callback for the button to remove this contact."""
-            self._parent.remove(self)
-            self._parent.remove(self.remove_btn)
+            """Callback for the button to remove this item."""
+            self.list_panel.remove(self)
+            self.list_panel.remove(self.remove_btn)
             self.event_cbs["remove"](self)
 
         self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
         self.remove_btn.setStyleName(self.style["removeButton"])
-        self._parent.add(self.remove_btn)
+        self.list_panel.add(self.remove_btn)
 
     def removeOrReset(self):
+        """Remove the text box if the remove button exists, or reset the text box."""
         if hasattr(self, "remove_btn"):
             self.remove_btn.click()
         else:
             self.reset()
 
-    def onMouseMove(self, sender):
-        """Mouse enters the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            sender.remove_btn.setVisible(True)
-
-    def onMouseLeave(self, sender):
-        """Mouse leaves the area of a DragAutoCompleteTextBox."""
-        if hasattr(sender, "remove_btn"):
-            Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
-
-    def onFocus(self, sender):
-        sender.setSelectionRange(0, len(self.getText()))
-        self.event_cbs["focus"](sender)
-
-    def validate(self):
-        self.setSelectionRange(len(self.getText()), 0)
-        self.event_cbs["validate"](self)
-
-    def onChange(self, sender):
-        """The textbox or list selection is changed"""
-        if isinstance(sender, ListBox):
-            AutoCompleteTextBox.onChange(self, sender)
-        self.validate()
-
-    def onClick(self, sender):
-        """The list is clicked"""
-        AutoCompleteTextBox.onClick(self, sender)
-        self.validate()
-
-    def onKeyUp(self, sender, keycode, modifiers):
-        """Listen for ENTER key stroke"""
-        AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
-        if keycode == KEY_ENTER:
-            self.validate()
-
-
-class DropCell(DropWidget):
-    """A cell where you can drop widgets. This class is NOT generic because of
-    onDrop which uses methods from ListPanel. It has been created to
-    separate the drag and drop methods from the others and add a bit of
-    lisibility, but it's probably not reusable for another scenario.
-    """
-
-    def __init__(self, drop_cbs):
-        DropWidget.__init__(self)
-        self.drop_cbs = drop_cbs
-
-    def onDragEnter(self, event):
-        self.addStyleName(self.style["dragoverPanel"])
-        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, we don't want the style to be removed
-            self.removeStyleName(self.style["dragoverPanel"])
-
-    def onDragOver(self, event):
-        DOM.eventPreventDefault(event)
-
-    def onDrop(self, event):
-        DOM.eventPreventDefault(event)
-        dt = event.dataTransfer
-        # 'text', 'text/plain', and 'Text' are equivalent.
-        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
-        if item_type in self.drop_cbs.keys():
-            self.drop_cbs[item_type](self, item)
-        self.removeStyleName(self.style["dragoverPanel"])
-
 
 VALID = 1
 INVALID = 2
 DELETE = 3
 
 
-class ListPanel(FlowPanel, DropCell):
-    """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel
-    is not fully implemented yet and can not be used with pyjamas.ui.Label."""
+class ListPanel(FlowPanel, libervia_widget.DropCell):
+    """Panel used for listing items sharing the same key. The key is showed as
+    a Button to which you can bind a popup menu and the items are represented
+    with a sequence of DragAutoCompleteTextBox."""
+    # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label
+
+    def __init__(self, manager, data, style={}):
+        """Initialization with a button and a DragAutoCompleteTextBox.
 
-    def __init__(self, parent, entry, style={}):
-        """Initialization with a button and a DragAutoCompleteTextBox."""
-        FlowPanel.__init__(self, Visible=(False if entry["optional"] else True))
-        drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item),
-                    "CONTACT": lambda panel, item: self.addContact(item),
-                    "CONTACT_TITLE": lambda panel, item: self.addContact('@@'),
-                    "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel)
+        @param manager (ListManager)
+        @param data (dict{unicode: unicode})
+        @param style (dict{unicode: unicode})
+        """
+        FlowPanel.__init__(self, Visible=(False if data["optional"] else True))
+
+        def setTargetDropCell(host, item):
+            self.manager.target_drop_cell = self
+
+        # FIXME: dirty magic strings '@' and '@@'
+        drop_cbs = {"GROUP": lambda host, item: self.addItem("@%s" % item),
+                    "CONTACT": lambda host, item: self.addItem(tryJID(item)),
+                    "CONTACT_TITLE": lambda host, item: self.addItem('@@'),
+                    "CONTACT_TEXTBOX": setTargetDropCell
                     }
-        DropCell.__init__(self, drop_cbs)
+        libervia_widget.DropCell.__init__(self, None)
+        self.drop_keys = drop_cbs
         self.style = style
         self.addStyleName(self.style["keyPanel"])
-        self._parent = parent
-        self.key = entry["title"]
+        self.manager = manager
+        self.key = data["title"]
         self._addTextBox()
 
+    def onDrop(self, event):
+        try:
+            libervia_widget.DropCell.onDrop(self, event)
+        except base_widget.NoLiberviaWidgetException:
+            pass
+
     def _addTextBox(self, switchPrevious=False):
-        """Add a text box to the last position. If switchPrevious is True, simulate
-        an insertion before the current last textbox by copying the text and valid state.
-        @return: the created textbox or the previous one if switchPrevious is True.
+        """Add an empty text box to the last position.
+
+        @param switchPrevious (bool): if True, simulate an insertion before the
+            current last textbox by switching the texts and valid states
+        @return: an DragAutoCompleteTextBox, the created text box or the
+            previous one if switchPrevious is True.
         """
         if hasattr(self, "_last_textbox"):
             if self._last_textbox.getText() == "":
@@ -417,24 +434,29 @@
         def focus_cb(sender):
             if sender != self._last_textbox:
                 # save the current value before it's being modified
-                self._parent.addToRemainingList(sender.getText(), ignore_key=self.key)
-            sender.setCompletionItems(self._parent.remaining_list)
+                self.manager.addToRemainingList([tryJID(sender.getText())], ignore_key=self.key)
+
+            items = [unicode(item) for item in self.manager.items_remaining]
+            sender.setCompletionItems(items)
+
+        def add_cb(sender):
+            self.addItem(tryJID(sender.getText()), sender)
 
         def remove_cb(sender):
-            """Callback for the button to remove this contact."""
-            self._parent.addToRemainingList(sender.getText())
-            self._parent.setRemainingListUnsorted()
+            """Callback for the button to remove this item."""
+            self.manager.addToRemainingList([tryJID(sender.getText())])
+            self.manager.setRemainingListUnsorted()
             self._last_textbox.setFocus(True)
 
         def drop_cb(sender, target):
             """Callback when the textbox is drag-n-dropped."""
-            parent = sender._parent
-            if target != parent and target.addContact(sender.getText()):
+            list_panel = sender.list_panel
+            if target != list_panel and target.addItem(tryJID(sender.getText())):
                 sender.removeOrReset()
             else:
-                parent._parent.removeFromRemainingList(sender.getText())
+                list_panel.manager.removeFromRemainingList([tryJID(sender.getText())])
 
-        events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb}
+        events_cbs = {"focus": focus_cb, "validate": add_cb, "remove": remove_cb, "drop": drop_cb}
         textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
         self.add(textbox)
         if switchPrevious:
@@ -445,45 +467,42 @@
         self._last_textbox = textbox
         return previous if switchPrevious else textbox
 
-    def _checkContact(self, contact, modify):
-        """
-        @param contact: the contact to check
-        @param modify: True if the contact is being modified
-        @return:
-        - VALID if the contact is valid
-        - INVALID if the contact is not valid but can be displayed
-        - DELETE if the contact should not be displayed at all
+    def _checkItem(self, item, modify):
         """
-        def countItemInList(list_, item):
-            """For some reason the built-in count function doesn't work..."""
-            count = 0
-            for elem in list_:
-                if elem == item:
-                    count += 1
-            return count
-        if contact is None or contact == "":
+        @param item (object): the item to check
+        @param modify (bool): True if the item is being modified
+        @return: int value defined by one of these constants:
+            - VALID if the item is valid
+            - INVALID if the item is not valid but can be displayed
+            - DELETE if the item should not be displayed at all
+        """
+        def count(list_, item):
+            # XXX: list.count in not implemented by pyjamas
+            return len([elt for elt in list_ if elt == item])
+
+        if not item:
             return DELETE
-        if countItemInList(self.getContacts(), contact) > (1 if modify else 0):
+        if count(self.getItems(), item) > (1 if modify else 0):
             return DELETE
-        return VALID if contact in self._parent.list else INVALID
+        return VALID if item in self.manager.items else INVALID
+
+    def addItem(self, item, sender=None):
+        """Try to add an item. It will be added if it's a valid one.
 
-    def addContact(self, contact, sender=None):
-        """The first parameter type is checked, so it is also possible to call addContact(sender).
-        If contact is not defined, sender.getText() is used. If sender is not defined, contact will
-        be written to the last textbox and a new textbox is added afterward.
-        @param contact: unicode
-        @param sender: DragAutoCompleteTextBox instance
+        @param item (object): item to be added
+        @param (DragAutoCompleteTextBox): widget triggering the event
+        @param sender: if True, the item will be "written" to the last textbox
+            and a new text box will be added afterward.
+        @return: True if the item has been added.
         """
-        if isinstance(contact, DragAutoCompleteTextBox):
-            sender = contact
-            contact = sender.getText()
-        valid = self._checkContact(contact, sender is not None)
+        valid = self._checkItem(item, sender is not None)
+        item_s = unicode(item)
         if sender is None:
-            # method has been called to modify but to add a contact
+            # method has been called not to modify but to add an item
             if valid == VALID:
                 # eventually insert before the last textbox if it's not empty
                 sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
-                sender.setText(contact)
+                sender.setText(item_s)
         else:
             sender.setValid(valid == VALID)
         if valid != VALID:
@@ -492,117 +511,35 @@
             return False
         if sender == self._last_textbox:
             self._addTextBox()
-        try:
-            sender.setVisibleLength(len(contact))
-        except:
-            # IndexSizeError: Index or size is negative or greater than the allowed amount
-            log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)))
-        self._parent.removeFromRemainingList(contact)
+        sender.setVisibleLength(len(item_s))
+        self.manager.removeFromRemainingList([item])
         self._last_textbox.setFocus(True)
         return True
 
-    def emptyContacts(self):
-        """Empty the list of contacts."""
+    def emptyItems(self):
+        """Empty the list of items."""
         for child in self.getChildren():
             if hasattr(child, "remove_btn"):
                 child.remove_btn.click()
 
-    def setContacts(self, tab):
-        """Set the contacts."""
-        self.emptyContacts()
-        if isinstance(tab, set):
-            tab = list(tab)
-        tab.sort()
-        for contact in tab:
-            self.addContact(contact)
-
-    def getContacts(self):
-        """Get the contacts
-        @return: an array of string"""
-        tab = []
-        for widget in self.getChildren():
-            if isinstance(widget, DragAutoCompleteTextBox):
-                # not to be mixed with EMPTY_SELECTION_ITEM
-                if widget.getText() != "":
-                    tab.append(widget.getText())
-        return tab
-
-    @property
-    def target_drop_cell(self):
-        """@return: the panel where something has been dropped."""
-        return self._parent.target_drop_cell
-
-    def setTargetDropCell(self, target_drop_cell):
-        """
-        XXX: Property setter here would not make it, you need a proper method!
-        @param target_drop_cell: the panel where something has been dropped."""
-        self._parent.setTargetDropCell(target_drop_cell)
-
-
-class ContactChooserPanel(DialogBox):
-    """Display the contacts chooser dialog. This has been implemented while
-    prototyping and is currently not used. Left for an eventual later use.
-    Replaced by the popup menu which allows to add a panel for Cc or Bcc.
-    """
-
-    def __init__(self, manager, **kwargs):
-        """Display a listbox for each contact key"""
-        DialogBox.__init__(self, autoHide=False, centered=True, **kwargs)
-        self.setHTML("Select contacts")
-        self.manager = manager
-        self.listboxes = {}
-        self.contacts = manager.getContacts()
-
-        container = VerticalPanel(Visible=True)
-        container.addStyleName("marginAuto")
+    def resetItems(self, items):
+        """Repopulate the items.
 
-        grid = Grid(2, len(self.manager.keys_dict))
-        index = -1
-        for key in self.manager.keys_dict:
-            index += 1
-            grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index)
-            listbox = ListBox()
-            listbox.setMultipleSelect(True)
-            listbox.setVisibleItemCount(15)
-            listbox.addItem(EMPTY_SELECTION_ITEM)
-            for element in manager.list:
-                listbox.addItem(element)
-            self.listboxes[key] = listbox
-            grid.add(listbox, 1, index)
-        self._reset()
-
-        buttons = HorizontalPanel()
-        buttons.addStyleName("marginAuto")
-        btn_close = Button("Cancel", self.hide)
-        buttons.add(btn_close)
-        btn_reset = Button("Reset", self._reset)
-        buttons.add(btn_reset)
-        btn_ok = Button("OK", self._validate)
-        buttons.add(btn_ok)
+        @param items (list): the items to be listed.
+        """
+        self.emptyItems()
+        if isinstance(items, set):
+            items = list(items)
+        items.sort()
+        for item in items:
+            self.addItem(item)
 
-        container.add(grid)
-        container.add(buttons)
-
-        self.add(container)
-        self.center()
+    def getItems(self):
+        """Get the listed items.
 
-    def _reset(self):
-        """Reset the selections."""
-        for key in self.manager.keys_dict:
-            listbox = self.listboxes[key]
-            for i in xrange(0, listbox.getItemCount()):
-                if listbox.getItemText(i) in self.contacts[key]:
-                    listbox.setItemSelected(i, "selected")
-                else:
-                    listbox.setItemSelected(i, "")
-
-    def _validate(self):
-        """Sets back the selected contacts to the good sub-panels."""
-        _map = {}
-        for key in self.manager.keys_dict:
-            selections = self.listboxes[key].getSelectedItemText()
-            if EMPTY_SELECTION_ITEM in selections:
-                selections.remove(EMPTY_SELECTION_ITEM)
-            _map[key] = selections
-        self.manager.setContacts(_map)
-        self.hide()
+        @return: set"""
+        items = set()
+        for widget in self.getChildren():
+            if isinstance(widget, DragAutoCompleteTextBox) and widget.getText() != "":
+                items.add(tryJID(widget.getText()))
+        return items