Mercurial > libervia-web
changeset 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 | 19153af4f327 |
children | da0487f0a2e7 |
files | browser_side/base_widget.py browser_side/list_manager.py browser_side/recipients.py browser_side/richtext.py |
diffstat | 4 files changed, 556 insertions(+), 559 deletions(-) [+] |
line wrap: on
line diff
--- a/browser_side/base_widget.py Sat Nov 09 08:53:03 2013 +0100 +++ b/browser_side/base_widget.py Sat Nov 09 09:38:17 2013 +0100 @@ -111,7 +111,7 @@ widgets_panel.removeWidget(_new_panel) elif item_type in self.drop_keys: _new_panel = self.drop_keys[item_type](self.host, item) - elif item_type == "RECIPIENT_TEXTBOX": + elif item_type == "CONTACT_TEXTBOX": # eventually open a window? pass else: @@ -623,7 +623,7 @@ _new_panel.getWidgetsPanel().removeWidget(_new_panel) elif item_type in DropCell.drop_keys: _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item) - elif item_type == "RECIPIENT_TEXTBOX": + elif item_type == "CONTACT_TEXTBOX": # eventually open a window? pass else:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser_side/list_manager.py Sat Nov 09 09:38:17 2013 +0100 @@ -0,0 +1,516 @@ +#!/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()
--- a/browser_side/recipients.py Sat Nov 09 08:53:03 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,476 +0,0 @@ -#!/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 - -# 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 "title" 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 - - self.recipient_menu = panels.PopupMenuPanel(entries=RECIPIENT_TYPES, - hide=lambda sender, key: self.__children[key]["panel"].isVisible(), - callback=self.setRecipientPanelVisible, - item_style="recipientTypeItem") - - 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]["title"] = key - self._addChild(RECIPIENT_TYPES[key]) - - def _addChild(self, entry): - """Add a button and FlowPanel for the corresponding map entry.""" - button = Button("%s: " % entry["title"]) - self.recipient_menu.registerClickSender(button) - 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["title"]] = {} - self.__children[entry["title"]]["button"] = button - self.__children[entry["title"]]["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, 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) - - 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 the popup menu which allows to add a panel for Cc or Bcc. - """ - RecipientChooserPanel(self) - - 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(str(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(str(recipient)), str(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()
--- a/browser_side/richtext.py Sat Nov 09 08:53:03 2013 +0100 +++ b/browser_side/richtext.py Sat Nov 09 09:38:17 2013 +0100 @@ -27,73 +27,14 @@ from pyjamas.ui.Label import Label from pyjamas.ui.FlexTable import FlexTable from pyjamas.ui.HorizontalPanel import HorizontalPanel -from recipients import RECIPIENT_TYPES, RecipientManager - -BUTTONS = { - "bold": {"tip": "Bold", "icon": "media/icons/dokuwiki/toolbar/16/bold.png"}, - "italic": {"tip": "Italic", "icon": "media/icons/dokuwiki/toolbar/16/italic.png"}, - "underline": {"tip": "Underline", "icon": "media/icons/dokuwiki/toolbar/16/underline.png"}, - "code": {"tip": "Code", "icon": "media/icons/dokuwiki/toolbar/16/mono.png"}, - "strikethrough": {"tip": "Strikethrough", "icon": "media/icons/dokuwiki/toolbar/16/strike.png"}, - "heading": {"tip": "Heading", "icon": "media/icons/dokuwiki/toolbar/16/hequal.png"}, - "numberedlist": {"tip": "Numbered List", "icon": "media/icons/dokuwiki/toolbar/16/ol.png"}, - "list": {"tip": "List", "icon": "media/icons/dokuwiki/toolbar/16/ul.png"}, - "link": {"tip": "Link", "icon": "media/icons/dokuwiki/toolbar/16/linkextern.png"}, - "horizontalrule": {"tip": "Horizontal rule", "icon": "media/icons/dokuwiki/toolbar/16/hr.png"} - } - -# Define here your formats, the key must match the ones used in button. -# Tupples values must have 3 elements : prefix to the selection or cursor -# position, sample text to write if the marker is not applied on a selection, -# suffix to the selection or cursor position. -# FIXME: must be moved in backend and not harcoded like this -FORMATS = {"markdown": {"bold": ("**", "bold", "**"), - "italic": ("*", "italic", "*"), - "code": ("`", "code", "`"), - "heading": ("\n# ", "Heading 1", "\n## Heading 2\n"), - "list": ("\n* ", "item", "\n + subitem\n"), - "link": ("[desc](", "link", ")"), - "horizontalrule": ("\n***\n", "", "") - }, - "bbcode": {"bold": ("[b]", "bold", "[/b]"), - "italic": ("[i]", "italic", "[/i]"), - "underline": ("[u]", "underline", "[/u]"), - "strikethrough": ("[s]", "strikethrough", "[/s]"), - "code": ("[code]", "code", "[/code]"), - "link": ("[url=", "link", "]desc[/url]"), - "list": ("\n[list] [*]", "item 1", " [*]item 2 [/list]\n") - }, - "dokuwiki": {"bold": ("**", "bold", "**"), - "italic": ("//", "italic", "//"), - "underline": ("__", "underline", "__"), - "strikethrough": ("<del>", "strikethrough", "</del>"), - "code": ("<code>", "code", "</code>"), - "heading": ("\n==== ", "Heading 1", " ====\n=== Heading 2 ===\n"), - "link": ("[[", "link", "|desc]]"), - "list": ("\n * ", "item\n", "\n * subitem\n"), - "horizontalrule": ("\n----\n", "", "") - }, - "XHTML": {"bold": ("<b>", "bold", "</b>"), - "italic": ("<i>", "italic", "</i>"), - "underline": ("<u>", "underline", "</u>"), - "strikethrough": ("<s>", "strikethrough", "</s>"), - "code": ("<pre>", "code", "</pre>"), - "heading": ("\n<h3>", "Heading 1", "</h3>\n<h4>Heading 2</h4>\n"), - "link": ("<a href=\"", "link", "\">desc</a>"), - "list": ("\n<ul><li>", "item 1", "</li><li>item 2</li></ul>\n"), - "horizontalrule": ("\n<hr/>\n", "", "") - } - - } - -PARAM_KEY = "Composition" -PARAM_NAME = "Syntax" +from list_manager import ListManager +from sat.tools.frontends import composition class RichTextEditor(FlexTable): """Panel for the rich text editor.""" - def __init__(self, host, parent=None, onCloseCallback=None): + def __init__(self, host, parent, onCloseCallback=None): """Fill the editor with recipients panel, toolbar, text area...""" # TODO: don't forget to comment this before commit @@ -102,8 +43,8 @@ # This must be done before FlexTable.__init__ because it is used by setVisible self.host = host - offset1 = len(RECIPIENT_TYPES) - offset2 = len(FORMATS) if self._debug else 1 + offset1 = len(composition.RECIPIENT_TYPES) + offset2 = len(composition.RICH_FORMATS) if self._debug else 1 FlexTable.__init__(self, offset1 + offset2 + 2, 2) self.addStyleName('richTextEditor') @@ -112,7 +53,7 @@ # recipient types sub-panels are automatically added by the manager self.recipient = RecipientManager(self) - self.recipient.createWidgets() + self.recipient.createWidgets(title_format="%s: ") # Rich text tool bar is automatically added by setVisible @@ -138,9 +79,9 @@ to be also automatically removed from its parent, or the popup to be closed. @param host: the host - @popup parent: parent panel (in a popup if parent == None) . - @return: the RichTextEditor instance if popup is False, otherwise - a popup DialogBox containing the RichTextEditor. + @param parent: parent panel (or None to display in a popup). + @return: the RichTextEditor instance if parent is not None, + otherwise a popup DialogBox containing the RichTextEditor. """ if not hasattr(host, 'richtext'): host.richtext = RichTextEditor(host, parent, onCloseCallback) @@ -168,7 +109,7 @@ def setVisible(self, kwargs): """Called each time the widget is displayed, after creation or after having been hidden.""" - self.host.bridge.call('asyncGetParamA', self.setToolBar, PARAM_NAME, PARAM_KEY) or self.setToolBar(None) + self.host.bridge.call('asyncGetParamA', self.setToolBar, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) or self.setToolBar(None) FlexTable.setVisible(self, kwargs) def __close(self): @@ -185,17 +126,17 @@ holding the rich text format is retrieved. It is called at each opening of the rich text editor because the user may have change his setting since the last time.""" - if _format is None or _format not in FORMATS.keys(): - _format = FORMATS.keys()[0] + if _format is None or _format not in composition.RICH_FORMATS.keys(): + _format = composition.RICH_FORMATS.keys()[0] if hasattr(self, "_format") and self._format == _format: return self._format = _format - offset1 = len(RECIPIENT_TYPES) + offset1 = len(composition.RECIPIENT_TYPES) count = 0 - for _format in FORMATS.keys() if self._debug else [self._format]: + for _format in composition.RICH_FORMATS.keys() if self._debug else [self._format]: toolbar = HorizontalPanel() toolbar.addStyleName('richTextToolbar') - for key in FORMATS[_format].keys(): + for key in composition.RICH_FORMATS[_format].keys(): self.addToolbarButton(toolbar, _format, key) label = Label("Format: %s" % _format) label.addStyleName("richTextFormatLabel") @@ -207,8 +148,8 @@ def addToolbarButton(self, toolbar, _format, key): """Add a button with the defined parameters.""" button = Button('<img src="%s" class="richTextIcon" />' % - BUTTONS[key]["icon"]) - button.setTitle(BUTTONS[key]["tip"]) + composition.RICH_BUTTONS[key]["icon"]) + button.setTitle(composition.RICH_BUTTONS[key]["tip"]) button.addStyleName('richTextToolButton') toolbar.add(button) @@ -217,7 +158,7 @@ text = self.textarea.getText() cursor_pos = self.textarea.getCursorPos() selection_length = self.textarea.getSelectionLength() - infos = FORMATS[_format][key] + infos = composition.RICH_FORMATS[_format][key] if selection_length == 0: middle_text = infos[1] else: @@ -235,7 +176,7 @@ def syncFromUniBox(self): """Synchronize from unibox.""" data, target = self.host.uni_box.getTargetAndData() - self.recipient.setRecipients({"To": [target]} if target else {}) + self.recipient.setContacts({"To": [target]} if target else {}) self.textarea.setText(data if data else "") def syncToUniBox(self, recipients=None): @@ -245,7 +186,7 @@ or if a recipient is set to an optional type (Cc, Bcc). @return True if the sync could be done, False otherwise""" if recipients is None: - recipients = self.recipient.getRecipients() + recipients = self.recipient.getContacts() target = "" # we could eventually allow more in the future allowed = 1 @@ -254,7 +195,7 @@ if count == 0: continue allowed -= count - if allowed < 0 or RECIPIENT_TYPES[key]["optional"]: + if allowed < 0 or composition.RECIPIENT_TYPES[key]["optional"]: return False # TODO: change this if later more then one recipients are allowed target = recipients[key][0] @@ -294,7 +235,7 @@ def sendMessage(self): """Send the message.""" - recipients = self.recipient.getRecipients() + recipients = self.recipient.getContacts() if self.syncToUniBox(recipients): # also check that we actually have a message target and data if len(recipients["To"]) > 0 and self.textarea.getText() != "": @@ -309,3 +250,19 @@ InfoDialog("Feature in development", "Sending a message to more the one recipient," + " to Cc or Bcc is not implemented yet!", Width="400px").center() + + +class RecipientManager(ListManager): + """A manager for sub-panels to set the recipients for each recipient type.""" + + def __init__(self, parent): + # 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_.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_) + + self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, + hide=lambda sender, key: self.__children[key]["panel"].isVisible(), + callback=self.setContactPanelVisible)