Mercurial > libervia-web
view browser_side/list_manager.py @ 254:28d3315a8003
browser_side: isolate the basic stuff of RecipientManager in a new class ListManager:
- renamed most occurences of "recipient" to "contact" and "recipient type" to "contact key" or "list"
- data to represent the lists and autocomplete values are parametrized
- UI elements styles are set by default but can be ovewritten by a sub-class
- popup menu for the list Button element has to be set with registerPopupMenuPanel
- richtext UI uses the definitions from sat.tool.frontends.composition
Know issues:
- drag and drop AutoCompleteTextBox corrupts the list of remaining autocomplete values
- selecting an autocomplete value with the mouse and not keybord is not working properly
author | souliane <souliane@mailoo.org> |
---|---|
date | Sat, 09 Nov 2013 09:38:17 +0100 |
parents | browser_side/recipients.py@86055ccf69c3 |
children | 0e7f3944bd27 |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- """ Libervia: a Salut à Toi frontend Copyright (C) 2013 Adrien Cossa <souliane@mailoo.org> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ 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, KeyboardHandler 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 from pyjamas.ui import FocusListener, KeyboardListener, MouseListener, Event # HTML content for the removal button (image or text) REMOVE_BUTTON = '<span class="richTextRemoveIcon">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 = "" class ListManager(): """A manager for sub-panels to assign elements to lists.""" 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 """ self.host = parent.host 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 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) self.style = { "keyItem": "recipientTypeItem", "buttonCell": "recipientButtonCell", "dragoverPanel": "dragover-recipientPanel", "keyPanel": "recipientPanel", "textBox": "recipientTextBox", "removeButton": "recipientRemoveButton", } 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) 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) def _addChild(self, entry, title_format): """Add a button and FlowPanel for the corresponding map entry.""" button = Button(title_format % entry["title"]) button.addStyleName(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"] x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] self._parent.setWidget(y, x, button) self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) _child = ListPanel(self, entry, self.style) self._parent.setWidget(y, x + 1, _child) self.__children[entry["title"]] = {} self.__children[entry["title"]]["button"] = button self.__children[entry["title"]]["panel"] = _child if hasattr(self, "popup_menu"): # this is done if self.registerPopupMenuPanel has been called yet self.popup_menu.registerClickSender(button) def _refresh(self): """Set visible the sub-panels that are non optional or non empty, hide the rest.""" for key in self.__children: self.setContactPanelVisible(key, False) _map = self.getContacts() for key in _map: if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: self.setContactPanelVisible(key, True) 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 @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 setRemainingListUnsorted(self): """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 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 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]) else: self.__children[key]["panel"].setContacts([]) self._refresh() 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 def setTargetDropCell(self, panel): """Used to drag and drop the contacts from one panel to another.""" self._target_drop_cell = panel def getTargetDropCell(self): """Used to drag and drop the contacts from one panel to another.""" return self._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="recipientTypeItem") class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget, MouseHandler): """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): AutoCompleteTextBox.__init__(self) DragWidget.__init__(self) self.addMouseListener(self) 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")) 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()) 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("") 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: sender.remove_btn.setVisible(False)) 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, host): DropWidget.__init__(self) 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 == "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 self.removeStyleName(self.style["dragoverPanel"]) class ListPanel(FlowPanel, DropCell, FocusHandler, KeyboardHandler): """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) 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) def onLostFocus(self, sender): """A DragAutoCompleteTextBox has lost the focus.""" if not isinstance(sender, DragAutoCompleteTextBox): return self.changeContact(sender) 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 remove_cb(sender): """Callback for the button to remove this contact.""" self.remove(textbox) self.remove(remove_btn) self._parent.addToRemainingList(contact) self._parent.setRemainingListUnsorted() remove_btn.addClickListener(remove_cb) self.__remove_cbs.append(remove_cb) self.add(remove_btn) self.__resetLastTextBox(setFocus=setFocusToLastTextBox) textbox.remove_btn = remove_btn textbox.addFocusListener(self) textbox.addKeyboardListener(self) def emptyContacts(self): """Empty the list of contacts.""" for remove_cb in self.__remove_cbs: remove_cb() self.__remove_cbs = [] def setContacts(self, tab): """Set the contacts.""" self.emptyContacts() for contact in tab: self.addContact(contact, resetLastTextBox=False) self.__resetLastTextBox() 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 def getTargetDropCell(self): """Returns self or another panel where something has been dropped.""" return self._parent.getTargetDropCell() 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") 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) container.add(grid) container.add(buttons) self.add(container) self.center() 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()