changeset 600:32dbbc941123 frontends_multi_profiles

browser_side: fixes the contact group manager
author souliane <souliane@mailoo.org>
date Fri, 06 Feb 2015 17:53:01 +0100
parents a6b9809b9a68
children 49ccfc22116c 917e271975d9
files src/browser/public/libervia.css src/browser/sat_browser/base_panels.py src/browser/sat_browser/contact_group.py src/browser/sat_browser/contact_list.py src/browser/sat_browser/list_manager.py
diffstat 5 files changed, 473 insertions(+), 441 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/public/libervia.css	Fri Feb 06 19:31:30 2015 +0100
+++ b/src/browser/public/libervia.css	Fri Feb 06 17:53:01 2015 +0100
@@ -1341,23 +1341,23 @@
 
 /* Recipients panel */
 
-.recipientButtonCell {
+.itemButtonCell {
     width:55px;
 }
 
-.recipientTypeMenu {
+.itemKeyMenu {
 }
 
-.recipientTypeItem {
+.itemKey {
     cursor: pointer;
     border-radius: 5px;
     width: 50px;
 }
 
-.recipientPanel {
+.itemPanel {
 }
 
-.recipientTextBox {
+.itemTextBox {
     cursor: pointer;
     width: auto;
     border-radius: 5px 5px 5px 5px;
@@ -1369,27 +1369,27 @@
     font-size: 1em;
 }
 
-.recipientTextBox-invalid {
+.itemTextBox-invalid {
     -webkit-box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6);
     box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6);
     border: 1px solid rgb(255, 0, 0);
 }
 
-.recipientRemoveButton {
+.itemRemoveButton {
     margin: 0px 10px 0px 0px;
     padding: 0px;
     border: 1px dashed red;
     border-radius: 5px 5px 5px 5px;
 }
 
-.recipientRemoveIcon {
+.itemRemoveIcon {
     color: red;
     width:15px;
     height:15px;
     vertical-align: baseline;
 }
 
-.dragover-recipientPanel {
+.itemPanel-dragover {
     border-radius: 5px;
     background: none repeat scroll 0% 0% rgb(135, 179, 255);
     border: 1px dashed rgb(35,79,255);
--- a/src/browser/sat_browser/base_panels.py	Fri Feb 06 19:31:30 2015 +0100
+++ b/src/browser/sat_browser/base_panels.py	Fri Feb 06 17:53:01 2015 +0100
@@ -164,7 +164,7 @@
         self._hide = hide
         self._callback = callback
         self.vertical = vertical
-        self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
+        self.style = {"selected": None, "menu": "itemKeyMenu", "item": "popupMenuItem"}
         if isinstance(style, dict):
             self.style.update(style)
         self._senders = {}
--- a/src/browser/sat_browser/contact_group.py	Fri Feb 06 19:31:30 2015 +0100
+++ b/src/browser/sat_browser/contact_group.py	Fri Feb 06 17:53:01 2015 +0100
@@ -31,11 +31,24 @@
 import contact_list
 
 
+unicode = str  # FIXME: pyjamas workaround
+
+
 class ContactGroupManager(list_manager.ListManager):
-    """A manager for sub-panels to assign contacts to each group."""
 
-    def __init__(self, parent, keys_dict, contacts, offsets, style):
-        list_manager.ListManager.__init__(self, parent, keys_dict, contacts, offsets, style)
+    def __init__(self, container, keys, contacts, offsets, style):
+        """
+        @param container (FlexTable): FlexTable parent widget
+        @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items
+            keys to their display config data.
+        @param contacts (list): list of contacts
+        @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
+        """
+        list_manager.ListManager.__init__(self, container, keys, contacts, offsets, style)
         self.registerPopupMenuPanel(entries={"Remove group": {}},
                                     callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key)))
 
@@ -44,43 +57,51 @@
 
         def confirm_cb(answer):
             if answer:
-                list_manager.ListManager.removeContactKey(self, key)
-                self._parent.removeKeyFromAddGroupPanel(key)
+                list_manager.ListManager.removeItemKey(self, key)
+                self.container.removeKeyFromAddGroupPanel(key)
 
         _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key)
         _dialog.show()
 
     def removeFromRemainingList(self, contacts):
         list_manager.ListManager.removeFromRemainingList(self, contacts)
-        self._parent.updateContactList(contacts=contacts)
+        self.container.updateContactList(contacts)
 
     def addToRemainingList(self, contacts, ignore_key=None):
         list_manager.ListManager.addToRemainingList(self, contacts, ignore_key)
-        self._parent.updateContactList(contacts=contacts)
+        self.container.updateContactList(contacts)
 
 
 class ContactGroupEditor(DockPanel):
-    """Panel for the contact groups manager."""
+    """A big panel including a ContactGroupManager and other UI stuff."""
+
+    def __init__(self, host, container=None, onCloseCallback=None):
+        """
 
-    def __init__(self, host, parent=None, onCloseCallback=None):
+        @param host (SatWebFrontend)
+        @param container (PanelBase): parent panel or None to display in a popup
+        @param onCloseCallback (callable)
+        """
         DockPanel.__init__(self)
         self.host = host
 
         # eventually display in a popup
-        if parent is None:
-            parent = DialogBox(autoHide=False, centered=True)
-            parent.setHTML("Manage contact groups")
-        self._parent = parent
+        if container is None:
+            container = DialogBox(autoHide=False, centered=True)
+            container.setHTML("Manage contact groups")
+        self.container = container
         self._on_close_callback = onCloseCallback
-        self.all_contacts = self.host.contact_panel.getContacts()
 
-        groups_list = self.host.contact_panel.groups.keys()
-        groups_list.sort()
+        self.all_contacts = contact_list.JIDList(self.host.contact_list.roster_entities)
+        roster_entities_by_group = self.host.contact_list.roster_entities_by_group
+        del roster_entities_by_group[None]  # remove the empty group
+        roster_groups = roster_entities_by_group.keys()
+        roster_groups.sort()
 
-        self.add_group_panel = self.getAddGroupPanel(groups_list)
-        south_panel = self.getCloseSaveButtons()
-        center_panel = self.getContactGroupManager(groups_list)
-        east_panel = self.getContactList()
+        self.add_group_panel = self.initAddGroupPanel(roster_groups)
+        south_panel = self.initCloseSaveButtons()
+        center_panel = self.initContactGroupManager(roster_groups)
+        east_panel = self.initContactList()
 
         self.add(self.add_group_panel, DockPanel.CENTER)
         self.add(east_panel, DockPanel.EAST)
@@ -97,108 +118,123 @@
         self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER)
 
         # need to be done after the contact list has been initialized
-        self.groups.setContacts(self.host.contact_panel.groups)
+        self.groups.resetItems(roster_entities_by_group)
         self.toggleContacts(showAll=True)
 
         # Hide the contacts list from the main panel to not confuse the user
         self.restore_contact_panel = False
-        if self.host.contact_panel.getVisible():
+        clist = self.host.contact_list
+        if clist.getVisible():
             self.restore_contact_panel = True
             self.host.panel._contactsSwitch()
 
-        parent.add(self)
-        parent.setVisible(True)
-        if isinstance(parent, DialogBox):
-            parent.center()
+        container.add(self)
+        container.setVisible(True)
+        if isinstance(container, DialogBox):
+            container.center()
 
-    def getContactGroupManager(self, groups_list):
-        """Set the list manager for the groups"""
-        flex_table = FlexTable(len(groups_list), 2)
+    def initContactGroupManager(self, groups):
+        """Initialise the contact group manager.
+
+        @param groups (list[unicode]): contact groups
+        """
+        flex_table = FlexTable()
         flex_table.addStyleName('contactGroupEditor')
+
         # overwrite the default style which has been set for rich text editor
-        style = {
-           "keyItem": "group",
-           "popupMenuItem": "popupMenuItem",
-           "removeButton": "contactGroupRemoveButton",
-           "buttonCell": "contactGroupButtonCell",
-           "keyPanel": "contactGroupPanel"
-        }
-        self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style)
-        self.groups.createWidgets()  # widgets are automatically added to FlexTable
+        style = {"keyItem": "group",
+                 "popupMenuItem": "popupMenuItem",
+                 "removeButton": "contactGroupRemoveButton",
+                 "buttonCell": "contactGroupButtonCell",
+                 "keyPanel": "contactGroupPanel"
+                 }
+
+        groups = {group: {} for group in groups}
+        self.groups = ContactGroupManager(flex_table, groups, self.all_contacts, style=style)
+        self.groups.createWidgets()  # widgets are automatically added to the FlexTable
+
         # FIXME: clean that part which is dangerous
         flex_table.updateContactList = self.updateContactList
         flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove
+
         return flex_table
 
-    def getAddGroupPanel(self, groups_list):
-        """Add the 'Add group' panel to the FlexTable"""
+    def initAddGroupPanel(self, groups):
+        """Initialise the 'Add group' panel.
 
-        def add_group_cb(text):
-            self.groups.addContactKey(text)
+        @param groups (list[unicode]): contact groups
+        """
+
+        def add_group_cb(key):
+            self.groups.addItemKey(key)
             self.add_group_panel.textbox.setFocus(True)
 
-        add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb)
+        add_group_panel = dialog.AddGroupPanel(groups, add_group_cb)
         add_group_panel.addStyleName("addContactGroupPanel")
         return add_group_panel
 
-    def getCloseSaveButtons(self):
-        """Add the buttons to close the dialog / save the groups"""
+    def initCloseSaveButtons(self):
+        """Add the buttons to close the dialog and save the groups."""
         buttons = HorizontalPanel()
         buttons.addStyleName("marginAuto")
         buttons.add(Button("Save", listener=self.closeAndSave))
         buttons.add(Button("Cancel", listener=self.cancelWithoutSaving))
         return buttons
 
-    def getContactList(self):
-        """Add the contact list to the DockPanel"""
+    def initContactList(self):
+        """Add the contact list to the DockPanel."""
         self.toggle = Button("", self.toggleContacts)
         self.toggle.addStyleName("toggleAssignedContacts")
-        self.contacts = contact_list.BaseContactPanel(self.host)
-        for contact_ in self.all_contacts:
-            self.contacts.add(contact_)
+        self.contacts = contact_list.BaseContactsPanel(self.host)
+        for contact in self.all_contacts:
+            self.contacts.add(contact)
         contact_panel = VerticalPanel()
         contact_panel.add(self.toggle)
         contact_panel.add(self.contacts)
         return contact_panel
 
     def toggleContacts(self, sender=None, showAll=None):
-        """Callback for the toggle button"""
-        if sender is None:
-            sender = self.toggle
-        sender.showAll = showAll if showAll is not None else not sender.showAll
-        if sender.showAll:
-            sender.setText("Hide assigned")
-        else:
-            sender.setText("Show assigned")
-        self.updateContactList(sender)
+        """Toggle the button to show contacts and the contact list.
 
-    def updateContactList(self, sender=None, contacts=None):
-        """Update the contact list regarding the toggle button"""
+        @param sender (Button)
+        @param showAll (bool): if set, initialise with True to show all contacts
+            or with False to show only the ones that are not assigned yet.
+        """
+        self.toggle.showAll = (not self.toggle.showAll) if showAll is None else showAll
+        self.toggle.setText("Hide assigned" if self.toggle.showAll else "Show assigned")
+        self.updateContactList()
+
+    def updateContactList(self, contacts=None):
+        """Update the contact list's items visibility, depending of the toggle
+        button and the "contacts" attribute.
+
+        @param contacts (list): contacts to be updated, or None to update all.
+        """
         if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"):
             return
-        sender = self.toggle
         if contacts is not None:
-            if not isinstance(contacts, list):
-                contacts = [contacts]
-            for contact_ in contacts:
-                if contact_ not in self.all_contacts:
-                    contacts.remove(contact_)
+            to_remove = set()
+            for contact in contacts:
+                if contact not in self.all_contacts:
+                    to_remove.add(contact)
+            for contact in to_remove:
+                contacts.remove(contact)
         else:
             contacts = self.all_contacts
-        for contact_ in contacts:
-            if sender.showAll:
-                self.contacts.getContactBox(contact_).setVisible(True)
+        for contact in contacts:
+            if self.toggle.showAll:
+                self.contacts.getContactBox(contact).setVisible(True)
             else:
-                if contact_ in self.groups.remaining_list:
-                    self.contacts.getContactBox(contact_).setVisible(True)
+                if contact in self.groups.items_remaining:
+                    self.contacts.getContactBox(contact).setVisible(True)
                 else:
-                    self.contacts.getContactBox(contact_).setVisible(False)
+                    self.contacts.getContactBox(contact).setVisible(False)
 
     def __close(self):
         """Remove the widget from parent or close the popup."""
-        if isinstance(self._parent, DialogBox):
-            self._parent.hide()
-        self._parent.remove(self)
+        if isinstance(self.container, DialogBox):
+            self.container.hide()
+        self.container.remove(self)
         if self._on_close_callback is not None:
             self._on_close_callback()
         if self.restore_contact_panel:
@@ -215,22 +251,21 @@
 
     def closeAndSave(self):
         """Call bridge methods to save the changes and close the dialog"""
-        map_ = {}
-        for contact_ in self.all_contacts:
-            map_[contact_] = set()
-        contacts = self.groups.getContacts()
-        for group in contacts.keys():
-            for contact_ in contacts[group]:
-                try:
-                    map_[contact_].add(group)
-                except KeyError:
-                    dialog.InfoDialog("Invalid contact",
-                           "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) +
-                           "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
-                    return
-        for contact_ in map_.keys():
-            groups = map_[contact_]
-            current_groups = self.host.contact_panel.getContactGroups(contact_)
-            if groups != current_groups:
-                self.host.bridge.call('updateContact', None, contact_, '', list(groups))
+        old_groups_by_entity = contact_list.JIDDict(self.host.contact_list.roster_groups_by_entity)
+        old_entities = old_groups_by_entity.keys()
+        groups_by_entity = contact_list.JIDDict(self.groups.getKeysByItem())
+        entities = groups_by_entity.keys()
+
+        for invalid in entities.difference(self.all_contacts):
+            dialog.InfoDialog("Invalid contact(s)",
+                              "The contact '%s' is not in your contact list but has been assigned to: '%s'." % (invalid, "', '".join(groups_by_entity[invalid])) +
+                              "Your changes could not be saved: please check your assignments and save again.", Width="400px").center()
+            return
+
+        for entity in old_entities.difference(entities):
+            self.host.bridge.call('updateContact', None, unicode(entity), '', [])
+
+        for entity, groups in groups_by_entity.iteritems():
+            if entity not in old_groups_by_entity or groups != old_groups_by_entity[entity]:
+                self.host.bridge.call('updateContact', None, unicode(entity), '', list(groups))
         self.__close()
--- a/src/browser/sat_browser/contact_list.py	Fri Feb 06 19:31:30 2015 +0100
+++ b/src/browser/sat_browser/contact_list.py	Fri Feb 06 17:53:01 2015 +0100
@@ -256,6 +256,7 @@
 
         @param contact_jid (jid.JID): the contact
         @return: ContactBox instance if present, else None"""
+        assert isinstance(contact_jid, jid.JID)
         for wid in self:
             if isinstance(wid, ContactBox) and wid.jid == contact_jid:
                 return wid
@@ -585,3 +586,42 @@
     # def refresh(self):
     #     """Show or hide disconnected contacts and empty groups"""
     #     self.updateVisibility(self._contacts_panel.contacts, self.groups.keys())
+
+
+def mayContainJID(iterable, item):
+    """Tells if the given item is in the iterable, works with JID.
+
+    @param iterable(object): list, set or another iterable object
+    @param item (object): element
+    @return: bool
+    """
+    # Pyjamas JID-friendly implementation of the "in" operator. Since our JID
+    # doesn't inherit from str, without this method the test would return True
+    # only when the objects references are the same.
+    if isinstance(item, jid.JID):
+        return hash(item) in [hash(other) for other in iterable if isinstance(other, jid.JID)]
+    return super(type(iterable), iterable).__contains__(self, item)
+
+
+class JIDSet(set):
+    """JID set implementation for Pyjamas"""
+
+    def __contains__(self, item):
+        return mayContainJID(self, item)
+
+
+class JIDList(list):
+    """JID list implementation for Pyjamas"""
+
+    def __contains__(self, item):
+        return mayContainJID(self, item)
+
+
+class JIDDict(dict):
+    """JID dict implementation for Pyjamas (a dict with JID keys)"""
+
+    def __contains__(self, item):
+        return mayContainJID(self, item)
+
+    def keys(self):
+        return JIDSet(dict.keys(self))
--- a/src/browser/sat_browser/list_manager.py	Fri Feb 06 19:31:30 2015 +0100
+++ b/src/browser/sat_browser/list_manager.py	Fri Feb 06 17:53:01 2015 +0100
@@ -19,15 +19,10 @@
 
 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
@@ -38,221 +33,245 @@
 import base_panels
 import base_widget
 
-# HTML content for the removal button (image or text)
-REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>'
+from sat_frontends.tools import jid
+
+
+unicode = str  # FIXME: pyjamas workaround
 
-# 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 = ""
+# HTML content for the removal button (image or text)
+REMOVE_BUTTON = '<span class="itemRemoveIcon">x</span>'
+
+
+# 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",
+                      "dragoverPanel": "itemPanel-dragover",
+                      "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_panels.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.
-    """
+    """A draggable AutoCompleteTextBox which is used for representing an item."""
+    # XXX: this class is NOT generic because of the onDragEnd method which calls methods from ListPanel. It's probably not reusable for another scenario.
 
-    def __init__(self, parent, event_cbs, style):
+    def __init__(self, list_panel, 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
+        self.list_panel = list_panel
         self.event_cbs = event_cbs
         self.style = style
         self.addMouseListener(self)
@@ -277,11 +296,11 @@
     def onDragStart(self, event):
         self._text = self.getText()
         base_widget.DragLabel.onDragStart(self, event)
-        self._parent.setTargetDropCell(None)
+        self.list_panel.manager.target_drop_cell = None
         self.setSelectionRange(len(self.getText()), 0)
 
     def onDragEnd(self, event):
-        target = self._parent.target_drop_cell  # parent or another ListPanel
+        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)
@@ -289,14 +308,14 @@
     def setRemoveButton(self):
 
         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):
         if hasattr(self, "remove_btn"):
@@ -344,10 +363,14 @@
     """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.
+    readability, but it's probably not reusable for another scenario.
     """
 
     def __init__(self, drop_cbs):
+        """
+
+        @param drop_cbs (list[callable])
+        """
         DropWidget.__init__(self)
         self.drop_cbs = drop_cbs
 
@@ -384,28 +407,43 @@
 
 
 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."""
+    """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 DragAutoCompleteTextBoxeditable."""
+    # 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(panel, item):
+            self.manager.target_drop_cell = panel
+
+        # FIXME: dirty magic strings '@' and '@@'
+        drop_cbs = {"GROUP": lambda panel, item: self.addItem("@%s" % item),
+                    "CONTACT": lambda panel, item: self.addItem(tryJID(item)),
+                    "CONTACT_TITLE": lambda panel, item: self.addItem('@@'),
+                    "CONTACT_TEXTBOX": setTargetDropCell
                     }
         DropCell.__init__(self, 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 _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 +455,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 +488,41 @@
         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):
+        """
 
-    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.
         """
-        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 +531,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