Mercurial > libervia-web
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