Mercurial > libervia-web
diff browser_side/recipients.py @ 232:0ed09cc0566f
browser_side: added UIs for rich text editor and addressing to multiple recipients
The rich text format is set according to a user parameter which is for now not created,
so you will get a warning on the backend and no toolbar will be displayed.
For testing purpose:
- you can set _debug to True in RichTextEditor: that will display one toolbar per format.
- you can add this parameter to any plugin (the same will be added later in XEP-0071):
# DEBUG: TO BE REMOVED LATER, THIS BELONGS TO RICH TEXT EDITOR
FORMATS = {"markdown": {}, "bbcode": {}, "dokuwiki": {}, "html": {}}
FORMAT_PARAM_KEY = "Composition and addressing"
FORMAT_PARAM_NAME = "Format for rich text message composition"
# In the parameter definition:
<category name="%(format_category_name)s" label="%(format_category_label)s">
<param name="%(format_param_name)s" label="%(format_param_label)s"
value="%(format_param_default)s" type="list" security="0">
%(format_options)s
</param>
</category>
# Strings for the placeholders:
'format_category_name': FORMAT_PARAM_KEY,
'format_category_label': _(FORMAT_PARAM_KEY),
'format_param_name': FORMAT_PARAM_NAME,
'format_param_label': _(FORMAT_PARAM_NAME),
'format_param_default': FORMATS.keys()[0],
'format_options': ['<option value="%s"/>' % format for format in FORMATS.keys()]
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 08 Oct 2013 14:12:38 +0200 |
parents | |
children | 86055ccf69c3 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser_side/recipients.py Tue Oct 08 14:12:38 2013 +0200 @@ -0,0 +1,492 @@ +#!/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.PopupPanel import PopupPanel +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 + +# Map the recipient types to their properties. For convenience, the key +# value is copied during the initialization to its associated sub-map, +# stored in the value of a new entry which uses "key" as its key. +RECIPIENT_TYPES = {"To": {"desc": "Direct recipients", "optional": False}, + "Cc": {"desc": "Carbon copies", "optional": True}, + "Bcc": {"desc": "Blind carbon copies", "optional": True}} + +# 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 RecipientManager(): + """A manager for sub-panels to set the recipients for each recipient type.""" + + def __init__(self, parent): + """""" + self._parent = parent + self.host = parent.host + + self.__list = [] + # TODO: be sure we also display empty groups and disconnected contacts + their groups + # store the full list of potential recipients (groups and contacts) + self.__list.extend("@%s" % group for group in self.host.contact_panel.getGroups()) + self.__list.extend(contact for contact in self.host.contact_panel.getContacts()) + self.__list.sort() + # store the list of recipients that are not selected 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 + + def createWidgets(self): + """Fill the parent grid with all the widgets but + only show those for non optional recipient types.""" + self.__children = {} + for key in RECIPIENT_TYPES: + # copy the key to its associated sub-map + RECIPIENT_TYPES[key]["key"] = key + self._addChild(RECIPIENT_TYPES[key]) + + def _addChild(self, entry): + """Add a button and FlowPanel for the corresponding map entry.""" + button = Button("%s: " % entry["key"], self.selectRecipientType) + button.addStyleName("recipientTypeItem") + button.setTitle(entry["desc"]) + button.setVisible(not entry["optional"]) + self._parent.setWidget(len(self.__children), 0, button) + self._parent.getCellFormatter().setStyleName(len(self.__children), 0, "recipientButtonCell") + + _child = RecipientTypePanel(self, entry) + self._parent.setWidget(len(self.__children), 1, _child) + + self.__children[entry["key"]] = {} + self.__children[entry["key"]]["button"] = button + self.__children[entry["key"]]["panel"] = _child + + def _refresh(self): + """Set visible the sub-panels that are non optional or non empty, hide the rest.""" + for key in self.__children: + self.setRecipientPanelVisible(key, False) + _map = self.getRecipients() + for key in _map: + if len(_map[key]) > 0 or not RECIPIENT_TYPES[key]["optional"]: + self.setRecipientPanelVisible(key, True) + + def setRecipientPanelVisible(self, key, visible=True): + self.__children[key]["button"].setVisible(visible) + self.__children[key]["panel"].setVisible(visible) + + def getList(self): + """Return the full list of potential recipients.""" + return self.__list + + def getRemainingList(self): + """Return the recipients 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, recipient): + """Remove an available recipient after it has been added to a sub-panel.""" + if recipient in self.__remaining_list: + self.__remaining_list.remove(recipient) + + def addToRemainingList(self, recipient): + """Add a recipient after it has been removed from a sub-panel.""" + self.__remaining_list.append(recipient) + self.__sort_remaining_list = True + + def selectRecipients(self): + """Display the recipients chooser dialog. This has been implemented while + prototyping and is currently not used. Left for an eventual later use. + Replaced by self.selectRecipientType. + """ + RecipientChooserPanel(self) + + def selectRecipientType(self, sender): + """Display a context menu to add a new recipient type.""" + self.context_menu = VerticalPanel() + self.context_menu.setStyleName("recipientTypeMenu") + popup = PopupPanel(autoHide=True) + + for key in RECIPIENT_TYPES: + if self.__children[key]["panel"].isVisible(): + continue + + def showPanel(sender): + self.setRecipientPanelVisible(sender.getText()) + popup.hide(autoClosed=True) + + item = Button(key, showPanel) + item.setStyleName("recipientTypeItem") + item.setTitle(RECIPIENT_TYPES[key]["desc"]) + self.context_menu.add(item) + + popup.add(self.context_menu) + popup.setPopupPosition(sender.getAbsoluteLeft() + sender.getOffsetWidth(), sender.getAbsoluteTop()) + popup.show() + + def setRecipients(self, _map={}): + """Set the recipients for each recipient types.""" + for key in RECIPIENT_TYPES: + if key in _map: + self.__children[key]["panel"].setRecipients(_map[key]) + else: + self.__children[key]["panel"].setRecipients([]) + self._refresh() + + def getRecipients(self): + """Get the recipients for all the recipient types. + @return: a mapping between keys from RECIPIENT_TYPES.keys() and recipient arrays.""" + _map = {} + for key in self.__children: + _map[key] = self.__children[key]["panel"].getRecipients() + return _map + + def setTargetDropCell(self, panel): + """Used to drap and drop the recipients from one panel to another.""" + self._target_drop_cell = panel + + def getTargetDropCell(self): + """Used to drap and drop the recipients from one panel to another.""" + return self._target_drop_cell + + +class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget): + """A draggable AutoCompleteTextBox which is used for representing a recipient. + This class is NOT generic because of the onDragEnd method which call methods + from RecipientTypePanel. It's probably not reusable for another scenario. + """ + + def __init__(self): + AutoCompleteTextBox.__init__(self) + DragWidget.__init__(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(), "RECIPIENT_TEXTBOX")) + + def onDragEnd(self, event): + if self.getText() == "": + return + # get the RecipientTypePanel containing self + parent = self.getParent() + while parent is not None and not isinstance(parent, RecipientTypePanel): + parent = parent.getParent() + if parent is None: + return + # it will return parent again or another RecipientTypePanel + target = parent.getTargetDropCell() + if target == parent: + return + target.addRecipient(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("") + + +class DropCell(DropWidget): + """A cell where you can drop widgets. This class is NOT generic because of + onDrop which uses methods from RecipientTypePanel. 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('dragover-recipientPanel') + 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('dragover-recipientPanel') + + 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.addRecipient(item) + elif item_type == "CONTACT": + self.addRecipient(item) + elif item_type == "RECIPIENT_TEXTBOX": + self._parent.setTargetDropCell(self) + pass + else: + return + self.removeStyleName('dragover-recipientPanel') + + +class RecipientTypePanel(FlowPanel, KeyboardHandler, MouseHandler, FocusHandler, DropCell): + """Sub-panel used for each recipient type. Beware that pyjamas.ui.FlowPanel + is not fully implemented yet and can not be used with pyjamas.ui.Label.""" + + def __init__(self, parent, entry): + """Initialization with a button and a DragAutoCompleteTextBox.""" + FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) + DropCell.__init__(self) + self.addStyleName("recipientPanel") + 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("recipientTextBox") + 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.getRemainingList()) + + def onLostFocus(self, sender): + """A DragAutoCompleteTextBox has lost the focus.""" + if not isinstance(sender, DragAutoCompleteTextBox): + return + self.changeRecipient(sender) + + def changeRecipient(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.addRecipient(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 addRecipient(self, recipient, resetLastTextBox=True, setFocusToLastTextBox=True): + """Add a recipient and signal it to self._parent panel.""" + if recipient is None or recipient == "": + return + textbox = DragAutoCompleteTextBox() + textbox.addStyleName("recipientTextBox") + textbox.setText(recipient) + self.add(textbox) + try: + textbox.setVisibleLength(len(recipient)) + except: + #TODO: . how come could this happen?! len(recipient) is sometimes 0 but recipient is not empty + print "len(recipient) returns %d where recipient == %s..." % (len(recipient), recipient) + self._parent.removeFromRemainingList(recipient) + + remove_btn = Button(REMOVE_BUTTON, Visible=False) + remove_btn.addStyleName("recipientRemoveButton") + + def remove_cb(sender): + """Callback for the button to remove this recipient.""" + self.remove(textbox) + self.remove(remove_btn) + self._parent.addToRemainingList(recipient) + 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.addMouseListener(self) + textbox.addFocusListener(self) + textbox.addKeyboardListener(self) + + def emptyRecipients(self): + """Empty the list of recipients.""" + for remove_cb in self.__remove_cbs: + remove_cb() + self.__remove_cbs = [] + + 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)) + + def setRecipients(self, tab): + """Set the recipients.""" + self.emptyRecipients() + for recipient in tab: + self.addRecipient(recipient, resetLastTextBox=False) + self.__resetLastTextBox() + + def getRecipients(self): + """Get the recipients + @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 RecipientChooserPanel(DialogBox): + """Display the recipients 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 recipient type""" + DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) + self.setHTML("Select recipients") + self.manager = manager + self.listboxes = {} + self.recipients = manager.getRecipients() + + container = VerticalPanel(Visible=True) + container.addStyleName("marginAuto") + + grid = Grid(2, len(RECIPIENT_TYPES)) + index = -1 + for key in RECIPIENT_TYPES: + index += 1 + grid.add(Label("%s:" % RECIPIENT_TYPES[key]["desc"]), 0, index) + listbox = ListBox() + listbox.setMultipleSelect(True) + listbox.setVisibleItemCount(15) + listbox.addItem(EMPTY_SELECTION_ITEM) + for element in manager.getList(): + 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 RECIPIENT_TYPES: + listbox = self.listboxes[key] + for i in xrange(0, listbox.getItemCount()): + if listbox.getItemText(i) in self.recipients[key]: + listbox.setItemSelected(i, "selected") + else: + listbox.setItemSelected(i, "") + + def _validate(self): + """Sets back the selected recipients to the good sub-panels.""" + _map = {} + for key in RECIPIENT_TYPES: + selections = self.listboxes[key].getSelectedItemText() + if EMPTY_SELECTION_ITEM in selections: + selections.remove(EMPTY_SELECTION_ITEM) + _map[key] = selections + self.manager.setRecipients(_map) + self.hide()