# HG changeset patch # User souliane # Date 1384170513 -3600 # Node ID d3c734669577f85dc4f9268de59fae7f14507836 # Parent 30c01671e338219a93603c54e086d5cbd5e58704 browser_side: improvements for lists and contact groups manager: - use DockPanel to deal with UI problems - fixed issues with the autocomplete list - avoid duplicate contacts in a contact list - signal invalid contacts with a red border - check for invalid contacts in the form before saving - better genericity for the class DragAutoCompleteTextBox diff -r 30c01671e338 -r d3c734669577 browser_side/contact_group.py --- a/browser_side/contact_group.py Mon Nov 11 10:44:44 2013 +0100 +++ b/browser_side/contact_group.py Mon Nov 11 12:48:33 2013 +0100 @@ -20,15 +20,17 @@ """ from pyjamas.ui.FlexTable import FlexTable -from browser_side.dialog import ConfirmDialog -from list_manager import ListManager -import contact +from pyjamas.ui.DockPanel import DockPanel from pyjamas.Timer import Timer from pyjamas.ui.Button import Button from pyjamas.ui.HorizontalPanel import HorizontalPanel from pyjamas.ui.VerticalPanel import VerticalPanel from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui import HasAlignment +from browser_side.dialog import ConfirmDialog, InfoDialog +from list_manager import ListManager import dialog +import contact class ContactGroupManager(ListManager): @@ -44,52 +46,77 @@ def confirm_cb(answer): if answer: - (y, x) = self._parent.getIndex(self.__children[key]["button"]) - self._parent.removeCell(y, x + 1) - self._parent.removeCell(y, x) - del self.__keys_dict[key] - del self.__children[key] - self._parent.add_group_panel.groups.remove(key) + ListManager.removeContactKey(self, key) + self._parent.removeKeyFromAddGroupPanel(key) _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key) _dialog.show() - def removeFromRemainingList(self, contact_): - ListManager.removeFromRemainingList(self, contact_) - self._parent.updateContactList(contact_=contact_) + def removeFromRemainingList(self, contacts): + ListManager.removeFromRemainingList(self, contacts) + self._parent.updateContactList(contacts=contacts) - def addToRemainingList(self, contact_): - ListManager.addToRemainingList(self, contact_) - self._parent.updateContactList(contact_=contact_) + def addToRemainingList(self, contacts, ignore_key=None): + ListManager.addToRemainingList(self, contacts, ignore_key) + self._parent.updateContactList(contacts=contacts) -class ContactGroupEditor(FlexTable): +class ContactGroupEditor(DockPanel): """Panel for the contact groups manager.""" def __init__(self, host, parent=None, onCloseCallback=None): - # This must be done before FlexTable.__init__ because it is used by setVisible + 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 self._on_close_callback = onCloseCallback + self.all_contacts = self.host.contact_panel.getContacts() groups_list = self.host.contact_panel.groups.keys() groups_list.sort() - FlexTable.__init__(self, len(groups_list) + 2, 3) - self.addStyleName('contactGroupEditor') + + self.add_group_panel = self.getAddGroupPanel(groups_list) + south_panel = self.getCloseSaveButtons() + center_panel = self.getContactGroupManager(groups_list) + east_panel = self.getContactList() + + self.add(self.add_group_panel, DockPanel.CENTER) + self.add(east_panel, DockPanel.EAST) + self.add(center_panel, DockPanel.NORTH) + self.add(south_panel, DockPanel.SOUTH) + + self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT) + self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP) + self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT) + self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP) + self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM) + self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT) + self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM) + self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) - def cb(text): - nb_keys = len(self.groups.keys) - self.getFlexCellFormatter().setColSpan(nb_keys + 1, 0, 1) - self.getFlexCellFormatter().setColSpan(nb_keys + 2, 0, 1) - self.remove(self.add_group_panel) - self.remove(self.command) - self.groups.addContactKey(text) - refresh() + # need to be done after the contact list has been initialized + self.groups.setContacts(self.host.contact_panel.groups) + 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(): + self.restore_contact_panel = True + self.host.panel._contactsSwitch() + parent.add(self) + parent.setVisible(True) + if isinstance(parent, DialogBox): + parent.center() + + def getContactGroupManager(self, groups_list): + """Set the list manager for the groups""" + flex_table = FlexTable(len(groups_list), 2) + flex_table.addStyleName('contactGroupEditor') # overwrite the default style which has been set for rich text editor style = { "keyItem": "group", @@ -98,81 +125,76 @@ "buttonCell": "contactGroupButtonCell", "keyPanel": "contactGroupPanel" } - self.all_contacts = self.host.contact_panel.getContacts() - self.groups = ContactGroupManager(self, groups_list, self.all_contacts, style=style) - self.groups.createWidgets() - - self.add_group_panel = dialog.AddGroupPanel(groups_list, cb) - self.add_group_panel.addStyleName("addContactGroupPanel") - - self.command = HorizontalPanel() - self.command.addStyleName("marginAuto") - self.command.add(Button("Cancel", listener=self.cancelWithoutSaving)) - self.command.add(Button("Save", listener=self.closeAndSave)) + self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style) + self.groups.createWidgets() # widgets are automatically added to 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 - contact_panel = VerticalPanel() + def getAddGroupPanel(self, groups_list): + """Add the 'Add group' panel to the FlexTable""" - # checkbox has been replaced by a button - self.checkbox = Button("", self.toggleContacts) - self.checkbox.getChecked = lambda: self.checkbox.checked if hasattr(self.checkbox, "checked") else None - self.checkbox.addStyleName("toggleAssignedContacts") - contact_panel.add(self.checkbox) - self.contacts = contact.GenericContactList(host) - contact_panel.add(self.contacts) - for contact in self.all_contacts: - self.contacts.add(contact) - self.setWidget(0, 2, contact_panel) + def add_group_cb(text): + self.groups.addContactKey(text) + self.add_group_panel.textbox.setFocus(True) + + add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb) + add_group_panel.addStyleName("addContactGroupPanel") + return add_group_panel - def refresh(): - nb_keys = len(self.groups.keys) - self.getFlexCellFormatter().setColSpan(nb_keys + 1, 0, 2) # add group panel - self.setWidget(nb_keys + 1, 0, self.add_group_panel) - self.getFlexCellFormatter().setColSpan(nb_keys + 2, 0, 3) # buttons panel - self.setWidget(nb_keys + 2, 0, self.command) - self.getFlexCellFormatter().setRowSpan(0, 2, nb_keys + 2) # contact list + def getCloseSaveButtons(self): + """Add the buttons to close the dialog / save the groups""" + buttons = HorizontalPanel() + buttons.addStyleName("marginAuto") + buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) + buttons.add(Button("Save", listener=self.closeAndSave)) + return buttons - self.groups.setContacts(self.host.contact_panel.groups) - refresh() - self.restore_contact_panel = False - if self.host.contact_panel.getVisible(): - self.restore_contact_panel = True - self.host.panel._contactsSwitch() - self.toggleContacts() - parent.add(self) - parent.setVisible(True) - if isinstance(parent, DialogBox): - parent.center() + def getContactList(self): + """Add the contact list to the DockPanel""" + self.toggle = Button("", self.toggleContacts) + self.toggle.addStyleName("toggleAssignedContacts") + self.contacts = contact.GenericContactList(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): + def toggleContacts(self, sender=None, showAll=None): + """Callback for the toggle button""" if sender is None: - sender = self.checkbox - if sender.getChecked(): - sender.checked = False + sender = self.toggle + sender.showAll = showAll if showAll is not None else not sender.showAll + if sender.showAll: sender.setText("Hide assigned") else: - sender.checked = True sender.setText("Show assigned") self.updateContactList(sender) - def updateContactList(self, sender=None, contact_=None): - sender = self.checkbox - if sender.getChecked() is None: - # do not update during initialization + def updateContactList(self, sender=None, contacts=None): + """Update the contact list regarding the toggle button""" + if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"): return - if contact_ is not None: - if contact_ not in self.all_contacts or not sender.getChecked(): - return - all_contacts = [contact_] + 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_) else: - all_contacts = self.all_contacts - for contact_ in all_contacts: - if sender.getChecked(): + contacts = self.all_contacts + for contact_ in contacts: + if sender.showAll: + self.contacts.getContactLabel(contact_).setVisible(True) + else: if contact_ in self.groups.remaining_list: self.contacts.getContactLabel(contact_).setVisible(True) else: self.contacts.getContactLabel(contact_).setVisible(False) - else: - self.contacts.getContactLabel(contact_).setVisible(True) def __close(self): """Remove the widget from parent or close the popup.""" @@ -194,13 +216,20 @@ _dialog.show() 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]: - map_[contact_].add(group) + try: + map_[contact_].add(group) + except KeyError: + 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_) diff -r 30c01671e338 -r d3c734669577 browser_side/list_manager.py --- a/browser_side/list_manager.py Mon Nov 11 10:44:44 2013 +0100 +++ b/browser_side/list_manager.py Mon Nov 11 12:48:33 2013 +0100 @@ -28,17 +28,15 @@ 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, KeyboardHandler +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 panels -import panels -from pyjamas.ui import FocusListener, KeyboardListener, MouseListener, Event # HTML content for the removal button (image or text) REMOVE_BUTTON = 'x' @@ -61,7 +59,6 @@ - "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 """ - self.host = parent.host self._parent = parent if isinstance(keys_dict, set) or isinstance(keys_dict, list): tmp = {} @@ -91,6 +88,7 @@ "dragoverPanel": "dragover-recipientPanel", "keyPanel": "recipientPanel", "textBox": "recipientTextBox", + "textBox-invalid": "recipientTextBox-invalid", "removeButton": "recipientRemoveButton", } self.style.update(style) @@ -99,7 +97,7 @@ """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) + self.addContactKey(key, title_format=title_format) def addContactKey(self, key, dict_={}, title_format="%s"): if key not in self.__keys_dict: @@ -108,6 +106,15 @@ self.__keys_dict[key]["title"] = key self._addChild(self.__keys_dict[key], title_format) + 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) + def _addChild(self, entry, title_format): """Add a button and FlowPanel for the corresponding map entry.""" button = Button(title_format % entry["title"]) @@ -120,6 +127,7 @@ 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"]) @@ -173,17 +181,31 @@ """Mark a change (deletion) so the list will be sorted before it's used.""" self.__remaining_list_sorted = False - def removeFromRemainingList(self, contact_): - """Remove an available contact after it has been added to a sub-panel.""" - if contact_ in self.__remaining_list: - self.__remaining_list.remove(contact_) + 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_) - def addToRemainingList(self, contact_): - """Add a contact after it has been removed from a sub-panel.""" - if contact_ not in self.__list or contact_ in self.__remaining_list: - return - self.__remaining_list.append(contact_) - self.__sort_remaining_list = True + 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: + continue + assigned_contacts.update(assigned_map[key_]) + for contact_ in contacts: + if contact_ not in self.__list or contact_ in self.__remaining_list: + continue + if contact_ in assigned_contacts: + continue # the contact is assigned somewhere else + self.__remaining_list.append(contact_) + self.setRemainingListUnsorted() def setContacts(self, _map={}): """Set the contacts for each contact key.""" @@ -202,55 +224,81 @@ _map[key] = self.__children[key]["panel"].getContacts() return _map - def setTargetDropCell(self, panel): - """Used to drag and drop the contacts from one panel to another.""" - self._target_drop_cell = panel + @property + def target_drop_cell(self): + """@return: the panel where something has been dropped.""" + return self.target_drop_cell - def getTargetDropCell(self): - """Used to drag and drop the contacts from one panel to another.""" - return self._target_drop_cell + @target_drop_cell.setter + def target_drop_cell(self, target_drop_cell): + """@param: target_drop_cell: the panel where something has been dropped.""" + self.target_drop_cell = target_drop_cell def registerPopupMenuPanel(self, entries, hide, callback): "Register a popup menu panel that will be bound to all contact keys elements." self.popup_menu = panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, item_style=self.style["popupMenuItem"]) -class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget, MouseHandler): +class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget, 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. """ - def __init__(self): + def __init__(self, parent, event_cbs, style): AutoCompleteTextBox.__init__(self) DragWidget.__init__(self) + self._parent = parent + self.event_cbs = event_cbs + self.style = style self.addMouseListener(self) + self.addFocusListener(self) + self.addChangeListener(self) + self.addStyleName(style["textBox"]) + self.reset() + + def reset(self): + self.setText("") + self.setValid() + + def setValid(self, valid=True): + if self.getText() == "": + valid = True + if valid: + self.removeStyleName(self.style["textBox-invalid"]) + else: + self.addStyleName(self.style["textBox-invalid"]) + self.valid = valid def onDragStart(self, event): dt = event.dataTransfer # The group prefix "@" is already in text so we use only the "CONTACT" type dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX")) + self.setSelectionRange(len(self.getText()), 0) def onDragEnd(self, event): if self.getText() == "": return - # get the ListPanel containing self - parent = self.getParent() - while parent is not None and not isinstance(parent, ListPanel): - parent = parent.getParent() - if parent is None: - return - # it will return parent again or another ListPanel - target = parent.getTargetDropCell() - if target == parent: - return - target.addContact(self.getText()) + target = self._parent.target_drop_cell # parent or another ListPanel + self.event_cbs["drop"](self, target) + + 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) + 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) + + def removeOrReset(self): if hasattr(self, "remove_btn"): - # self is not the last textbox, just remove it self.remove_btn.click() else: - # reset the value of the last textbox - self.setText("") + self.reset() def onMouseMove(self, sender): """Mouse enters the area of a DragAutoCompleteTextBox.""" @@ -262,6 +310,31 @@ if hasattr(sender, "remove_btn"): Timer(1500, lambda: 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 @@ -270,8 +343,9 @@ lisibility, but it's probably not reusable for another scenario. """ - def __init__(self, host): + def __init__(self, drop_cbs): DropWidget.__init__(self) + self.drop_cbs = drop_cbs def onDragEnter(self, event): self.addStyleName(self.style["dragoverPanel"]) @@ -295,135 +369,138 @@ 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 == "GROUP": - item = "@%s" % item - self.addContact(item) - elif item_type == "CONTACT": - self.addContact(item) - elif item_type == "CONTACT_TEXTBOX": - self._parent.setTargetDropCell(self) - pass - else: - return + if item_type in self.drop_cbs.keys(): + self.drop_cbs[item_type](self, item) self.removeStyleName(self.style["dragoverPanel"]) -class ListPanel(FlowPanel, DropCell, FocusHandler, KeyboardHandler): +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.""" def __init__(self, parent, entry, style={}): """Initialization with a button and a DragAutoCompleteTextBox.""" FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) - DropCell.__init__(self) + drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), + "CONTACT": lambda panel, item: self.addContact(item), + "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) + } + DropCell.__init__(self, drop_cbs) self.style = style self.addStyleName(self.style["keyPanel"]) self._parent = parent - self.host = parent.host - - self._last_textbox = None - self.__remove_cbs = [] - - self.__resetLastTextBox() - - def __resetLastTextBox(self, setFocus=True): - """Reset the last input text box with KeyboardListener.""" - if self._last_textbox is None: - self._last_textbox = DragAutoCompleteTextBox() - self._last_textbox.addStyleName(self.style["textBox"]) - self._last_textbox.addKeyboardListener(self) - self._last_textbox.addFocusListener(self) - else: - # ensure we move it to the last position - self.remove(self._last_textbox) - self._last_textbox.setText("") - self.add(self._last_textbox) - self._last_textbox.setFocus(setFocus) - - def onKeyUp(self, sender, keycode, modifiers): - """This is called after DragAutoCompleteTextBox.onKeyDown, - so the completion is done before we reset the text box.""" - if not isinstance(sender, DragAutoCompleteTextBox): - return - if keycode == KEY_ENTER: - self.onLostFocus(sender) - self._last_textbox.setFocus(True) - - def onFocus(self, sender): - """A DragAutoCompleteTextBox has the focus.""" - if not isinstance(sender, DragAutoCompleteTextBox): - return - if sender != self._last_textbox: - # save the current value before it's being modified - text = sender.getText() - self._focused_textbox_previous_value = text - self._parent.addToRemainingList(text) - sender.setCompletionItems(self._parent.remaining_list) + self.key = entry["title"] + self._addTextBox() - def onLostFocus(self, sender): - """A DragAutoCompleteTextBox has lost the focus.""" - if not isinstance(sender, DragAutoCompleteTextBox): - return - self.changeContact(sender) + 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. + """ + if hasattr(self, "_last_textbox"): + if self._last_textbox.getText() == "": + return + self._last_textbox.setRemoveButton() + else: + switchPrevious = False - def changeContact(self, sender): - """Modify the value of a DragAutoCompleteTextBox.""" - text = sender.getText() - if sender == self._last_textbox: - if text != "": - # a new box is added and the last textbox is reinitialized - self.addContact(text, setFocusToLastTextBox=False) - return - if text == "": - sender.remove_btn.click() - return - # text = new value needs to be removed 1. if the value is unchanged, because we - # added it when we took the focus, or 2. if the value is changed (obvious!) - self._parent.removeFromRemainingList(text) - if text == self._focused_textbox_previous_value: - return - sender.setVisibleLength(len(text)) - self._parent.addToRemainingList(self._focused_textbox_previous_value) - - def addContact(self, contact, resetLastTextBox=True, setFocusToLastTextBox=True): - """Add a contact and signal it to self._parent panel.""" - if contact is None or contact == "": - return - textbox = DragAutoCompleteTextBox() - textbox.addStyleName(self.style["textBox"]) - textbox.setText(contact) - self.add(textbox) - try: - textbox.setVisibleLength(len(str(contact))) - except: - #FIXME: . how come could this happen?! len(contact) is sometimes 0 but contact is not empty - print "len(contact) returns %d where contact == %s..." % (len(str(contact)), str(contact)) - self._parent.removeFromRemainingList(contact) - - remove_btn = Button(REMOVE_BUTTON, Visible=False) - remove_btn.setStyleName(self.style["removeButton"]) + 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) def remove_cb(sender): """Callback for the button to remove this contact.""" - self.remove(textbox) - self.remove(remove_btn) - self._parent.addToRemainingList(contact) + self._parent.addToRemainingList(sender.getText()) self._parent.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()): + sender.removeOrReset() + else: + parent._parent.removeFromRemainingList(sender.getText()) + + events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} + textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) + self.add(textbox) + if switchPrevious: + textbox.setText(self._last_textbox.getText()) + textbox.setValid(self._last_textbox.valid) + self._last_textbox.reset() + previous = self._last_textbox + self._last_textbox = textbox + return previous if switchPrevious else textbox - remove_btn.addClickListener(remove_cb) - self.__remove_cbs.append(remove_cb) - self.add(remove_btn) - self.__resetLastTextBox(setFocus=setFocusToLastTextBox) + 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 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 == "": + return DELETE + if countItemInList(self.getContacts(), contact) > (1 if modify else 0): + return DELETE + return VALID if contact in self._parent.list else INVALID - textbox.remove_btn = remove_btn - textbox.addFocusListener(self) - textbox.addKeyboardListener(self) + 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 + """ + if isinstance(contact, DragAutoCompleteTextBox): + sender = contact + contact = sender.getText() + valid = self._checkContact(contact, sender is not None) + if sender is None: + # method has been called to modify but to add a contact + 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) + else: + sender.setValid(valid == VALID) + if valid != VALID: + if sender is not None and valid == DELETE: + sender.removeOrReset() + 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 + print "FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)) + self._parent.removeFromRemainingList(contact) + self._last_textbox.setFocus(True) + return True def emptyContacts(self): """Empty the list of contacts.""" - for remove_cb in self.__remove_cbs: - remove_cb() - self.__remove_cbs = [] + for child in self.getChildren(): + if hasattr(child, "remove_btn"): + child.remove_btn.click() def setContacts(self, tab): """Set the contacts.""" @@ -432,8 +509,7 @@ tab = list(tab) tab.sort() for contact in tab: - self.addContact(contact, resetLastTextBox=False) - self.__resetLastTextBox() + self.addContact(contact) def getContacts(self): """Get the contacts @@ -446,9 +522,18 @@ tab.append(widget.getText()) return tab - def getTargetDropCell(self): - """Returns self or another panel where something has been dropped.""" - return self._parent.getTargetDropCell() + @property + def target_drop_cell(self): + """@return: the panel where something has been dropped.""" + return self._parent.target_drop_cell + + @target_drop_cell.setter + def target_drop_cell(self, target_drop_cell): + """@param target_drop_cell: the panel where something has been dropped.""" + self.setTargetDropCell(target_drop_cell) + + def setTargetDropCell(self, target_drop_cell): + self._parent.target_drop_cell = target_drop_cell class ContactChooserPanel(DialogBox): diff -r 30c01671e338 -r d3c734669577 browser_side/richtext.py --- a/browser_side/richtext.py Mon Nov 11 10:44:44 2013 +0100 +++ b/browser_side/richtext.py Mon Nov 11 12:48:33 2013 +0100 @@ -259,6 +259,7 @@ # TODO: be sure we also display empty groups and disconnected contacts + their groups # store the full list of potential recipients (groups and contacts) list_ = [] + list_.append("@@") list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) list_.extend(contact for contact in parent.host.contact_panel.getContacts()) ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_) diff -r 30c01671e338 -r d3c734669577 public/libervia.css --- a/public/libervia.css Mon Nov 11 10:44:44 2013 +0100 +++ b/public/libervia.css Mon Nov 11 12:48:33 2013 +0100 @@ -1231,6 +1231,13 @@ font-size: 1em; } +.recipientTextBox-invalid { + box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6); + -webkit-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6); + -moz-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6); + border: 1px solid rgb(255, 0, 0); +} + .recipientRemoveButton { margin: 0px 10px 0px 0px; padding: 0px;