Mercurial > libervia-web
comparison 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 |
comparison
equal
deleted
inserted
replaced
231:fab7aa366576 | 232:0ed09cc0566f |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 Libervia: a Salut à Toi frontend | |
6 Copyright (C) 2013 Adrien Cossa <souliane@mailoo.org> | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU Affero General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU Affero General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU Affero General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 from pyjamas.ui.Grid import Grid | |
23 from pyjamas.ui.Button import Button | |
24 from pyjamas.ui.ListBox import ListBox | |
25 from pyjamas.ui.FlowPanel import FlowPanel | |
26 from pyjamas.ui.PopupPanel import PopupPanel | |
27 from pyjamas.ui.AutoComplete import AutoCompleteTextBox | |
28 from pyjamas.ui.Label import Label | |
29 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
30 from pyjamas.ui.VerticalPanel import VerticalPanel | |
31 from pyjamas.ui.DialogBox import DialogBox | |
32 from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler | |
33 from pyjamas.ui.MouseListener import MouseHandler | |
34 from pyjamas.ui.FocusListener import FocusHandler | |
35 from pyjamas.ui.DropWidget import DropWidget | |
36 from pyjamas.ui.DragWidget import DragWidget | |
37 from pyjamas.Timer import Timer | |
38 from pyjamas import DOM | |
39 | |
40 # Map the recipient types to their properties. For convenience, the key | |
41 # value is copied during the initialization to its associated sub-map, | |
42 # stored in the value of a new entry which uses "key" as its key. | |
43 RECIPIENT_TYPES = {"To": {"desc": "Direct recipients", "optional": False}, | |
44 "Cc": {"desc": "Carbon copies", "optional": True}, | |
45 "Bcc": {"desc": "Blind carbon copies", "optional": True}} | |
46 | |
47 # HTML content for the removal button (image or text) | |
48 REMOVE_BUTTON = '<span class="richTextRemoveIcon">x</span>' | |
49 | |
50 # Item to be considered for an empty list box selection. | |
51 # Could be whatever which doesn't look like a JID or a group name. | |
52 EMPTY_SELECTION_ITEM = "" | |
53 | |
54 | |
55 class RecipientManager(): | |
56 """A manager for sub-panels to set the recipients for each recipient type.""" | |
57 | |
58 def __init__(self, parent): | |
59 """""" | |
60 self._parent = parent | |
61 self.host = parent.host | |
62 | |
63 self.__list = [] | |
64 # TODO: be sure we also display empty groups and disconnected contacts + their groups | |
65 # store the full list of potential recipients (groups and contacts) | |
66 self.__list.extend("@%s" % group for group in self.host.contact_panel.getGroups()) | |
67 self.__list.extend(contact for contact in self.host.contact_panel.getContacts()) | |
68 self.__list.sort() | |
69 # store the list of recipients that are not selected yet | |
70 self.__remaining_list = [] | |
71 self.__remaining_list.extend(self.__list) | |
72 # mark a change to sort the list before it's used | |
73 self.__remaining_list_sorted = True | |
74 | |
75 def createWidgets(self): | |
76 """Fill the parent grid with all the widgets but | |
77 only show those for non optional recipient types.""" | |
78 self.__children = {} | |
79 for key in RECIPIENT_TYPES: | |
80 # copy the key to its associated sub-map | |
81 RECIPIENT_TYPES[key]["key"] = key | |
82 self._addChild(RECIPIENT_TYPES[key]) | |
83 | |
84 def _addChild(self, entry): | |
85 """Add a button and FlowPanel for the corresponding map entry.""" | |
86 button = Button("%s: " % entry["key"], self.selectRecipientType) | |
87 button.addStyleName("recipientTypeItem") | |
88 button.setTitle(entry["desc"]) | |
89 button.setVisible(not entry["optional"]) | |
90 self._parent.setWidget(len(self.__children), 0, button) | |
91 self._parent.getCellFormatter().setStyleName(len(self.__children), 0, "recipientButtonCell") | |
92 | |
93 _child = RecipientTypePanel(self, entry) | |
94 self._parent.setWidget(len(self.__children), 1, _child) | |
95 | |
96 self.__children[entry["key"]] = {} | |
97 self.__children[entry["key"]]["button"] = button | |
98 self.__children[entry["key"]]["panel"] = _child | |
99 | |
100 def _refresh(self): | |
101 """Set visible the sub-panels that are non optional or non empty, hide the rest.""" | |
102 for key in self.__children: | |
103 self.setRecipientPanelVisible(key, False) | |
104 _map = self.getRecipients() | |
105 for key in _map: | |
106 if len(_map[key]) > 0 or not RECIPIENT_TYPES[key]["optional"]: | |
107 self.setRecipientPanelVisible(key, True) | |
108 | |
109 def setRecipientPanelVisible(self, key, visible=True): | |
110 self.__children[key]["button"].setVisible(visible) | |
111 self.__children[key]["panel"].setVisible(visible) | |
112 | |
113 def getList(self): | |
114 """Return the full list of potential recipients.""" | |
115 return self.__list | |
116 | |
117 def getRemainingList(self): | |
118 """Return the recipients that have not been selected yet.""" | |
119 if not self.__remaining_list_sorted: | |
120 self.__remaining_list_sorted = True | |
121 self.__remaining_list.sort() | |
122 return self.__remaining_list | |
123 | |
124 def setRemainingListUnsorted(self): | |
125 """Mark a change (deletion) so the list will be sorted before it's used.""" | |
126 self.__remaining_list_sorted = False | |
127 | |
128 def removeFromRemainingList(self, recipient): | |
129 """Remove an available recipient after it has been added to a sub-panel.""" | |
130 if recipient in self.__remaining_list: | |
131 self.__remaining_list.remove(recipient) | |
132 | |
133 def addToRemainingList(self, recipient): | |
134 """Add a recipient after it has been removed from a sub-panel.""" | |
135 self.__remaining_list.append(recipient) | |
136 self.__sort_remaining_list = True | |
137 | |
138 def selectRecipients(self): | |
139 """Display the recipients chooser dialog. This has been implemented while | |
140 prototyping and is currently not used. Left for an eventual later use. | |
141 Replaced by self.selectRecipientType. | |
142 """ | |
143 RecipientChooserPanel(self) | |
144 | |
145 def selectRecipientType(self, sender): | |
146 """Display a context menu to add a new recipient type.""" | |
147 self.context_menu = VerticalPanel() | |
148 self.context_menu.setStyleName("recipientTypeMenu") | |
149 popup = PopupPanel(autoHide=True) | |
150 | |
151 for key in RECIPIENT_TYPES: | |
152 if self.__children[key]["panel"].isVisible(): | |
153 continue | |
154 | |
155 def showPanel(sender): | |
156 self.setRecipientPanelVisible(sender.getText()) | |
157 popup.hide(autoClosed=True) | |
158 | |
159 item = Button(key, showPanel) | |
160 item.setStyleName("recipientTypeItem") | |
161 item.setTitle(RECIPIENT_TYPES[key]["desc"]) | |
162 self.context_menu.add(item) | |
163 | |
164 popup.add(self.context_menu) | |
165 popup.setPopupPosition(sender.getAbsoluteLeft() + sender.getOffsetWidth(), sender.getAbsoluteTop()) | |
166 popup.show() | |
167 | |
168 def setRecipients(self, _map={}): | |
169 """Set the recipients for each recipient types.""" | |
170 for key in RECIPIENT_TYPES: | |
171 if key in _map: | |
172 self.__children[key]["panel"].setRecipients(_map[key]) | |
173 else: | |
174 self.__children[key]["panel"].setRecipients([]) | |
175 self._refresh() | |
176 | |
177 def getRecipients(self): | |
178 """Get the recipients for all the recipient types. | |
179 @return: a mapping between keys from RECIPIENT_TYPES.keys() and recipient arrays.""" | |
180 _map = {} | |
181 for key in self.__children: | |
182 _map[key] = self.__children[key]["panel"].getRecipients() | |
183 return _map | |
184 | |
185 def setTargetDropCell(self, panel): | |
186 """Used to drap and drop the recipients from one panel to another.""" | |
187 self._target_drop_cell = panel | |
188 | |
189 def getTargetDropCell(self): | |
190 """Used to drap and drop the recipients from one panel to another.""" | |
191 return self._target_drop_cell | |
192 | |
193 | |
194 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget): | |
195 """A draggable AutoCompleteTextBox which is used for representing a recipient. | |
196 This class is NOT generic because of the onDragEnd method which call methods | |
197 from RecipientTypePanel. It's probably not reusable for another scenario. | |
198 """ | |
199 | |
200 def __init__(self): | |
201 AutoCompleteTextBox.__init__(self) | |
202 DragWidget.__init__(self) | |
203 | |
204 def onDragStart(self, event): | |
205 dt = event.dataTransfer | |
206 # The group prefix "@" is already in text so we use only the "CONTACT" type | |
207 dt.setData('text/plain', "%s\n%s" % (self.getText(), "RECIPIENT_TEXTBOX")) | |
208 | |
209 def onDragEnd(self, event): | |
210 if self.getText() == "": | |
211 return | |
212 # get the RecipientTypePanel containing self | |
213 parent = self.getParent() | |
214 while parent is not None and not isinstance(parent, RecipientTypePanel): | |
215 parent = parent.getParent() | |
216 if parent is None: | |
217 return | |
218 # it will return parent again or another RecipientTypePanel | |
219 target = parent.getTargetDropCell() | |
220 if target == parent: | |
221 return | |
222 target.addRecipient(self.getText()) | |
223 if hasattr(self, "remove_btn"): | |
224 # self is not the last textbox, just remove it | |
225 self.remove_btn.click() | |
226 else: | |
227 # reset the value of the last textbox | |
228 self.setText("") | |
229 | |
230 | |
231 class DropCell(DropWidget): | |
232 """A cell where you can drop widgets. This class is NOT generic because of | |
233 onDrop which uses methods from RecipientTypePanel. It has been created to | |
234 separate the drag and drop methods from the others and add a bit of | |
235 lisibility, but it's probably not reusable for another scenario. | |
236 """ | |
237 | |
238 def __init__(self, host): | |
239 DropWidget.__init__(self) | |
240 | |
241 def onDragEnter(self, event): | |
242 self.addStyleName('dragover-recipientPanel') | |
243 DOM.eventPreventDefault(event) | |
244 | |
245 def onDragLeave(self, event): | |
246 if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\ | |
247 or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\ | |
248 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: | |
249 # We check that we are inside widget's box, and we don't remove the style in this case because | |
250 # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed | |
251 self.removeStyleName('dragover-recipientPanel') | |
252 | |
253 def onDragOver(self, event): | |
254 DOM.eventPreventDefault(event) | |
255 | |
256 def onDrop(self, event): | |
257 DOM.eventPreventDefault(event) | |
258 dt = event.dataTransfer | |
259 # 'text', 'text/plain', and 'Text' are equivalent. | |
260 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed | |
261 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and | |
262 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report | |
263 if item_type == "GROUP": | |
264 item = "@%s" % item | |
265 self.addRecipient(item) | |
266 elif item_type == "CONTACT": | |
267 self.addRecipient(item) | |
268 elif item_type == "RECIPIENT_TEXTBOX": | |
269 self._parent.setTargetDropCell(self) | |
270 pass | |
271 else: | |
272 return | |
273 self.removeStyleName('dragover-recipientPanel') | |
274 | |
275 | |
276 class RecipientTypePanel(FlowPanel, KeyboardHandler, MouseHandler, FocusHandler, DropCell): | |
277 """Sub-panel used for each recipient type. Beware that pyjamas.ui.FlowPanel | |
278 is not fully implemented yet and can not be used with pyjamas.ui.Label.""" | |
279 | |
280 def __init__(self, parent, entry): | |
281 """Initialization with a button and a DragAutoCompleteTextBox.""" | |
282 FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) | |
283 DropCell.__init__(self) | |
284 self.addStyleName("recipientPanel") | |
285 self._parent = parent | |
286 self.host = parent.host | |
287 | |
288 self._last_textbox = None | |
289 self.__remove_cbs = [] | |
290 | |
291 self.__resetLastTextBox() | |
292 | |
293 def __resetLastTextBox(self, setFocus=True): | |
294 """Reset the last input text box with KeyboardListener.""" | |
295 if self._last_textbox is None: | |
296 self._last_textbox = DragAutoCompleteTextBox() | |
297 self._last_textbox.addStyleName("recipientTextBox") | |
298 self._last_textbox.addKeyboardListener(self) | |
299 self._last_textbox.addFocusListener(self) | |
300 else: | |
301 # ensure we move it to the last position | |
302 self.remove(self._last_textbox) | |
303 self._last_textbox.setText("") | |
304 self.add(self._last_textbox) | |
305 self._last_textbox.setFocus(setFocus) | |
306 | |
307 def onKeyUp(self, sender, keycode, modifiers): | |
308 """This is called after DragAutoCompleteTextBox.onKeyDown, | |
309 so the completion is done before we reset the text box.""" | |
310 if not isinstance(sender, DragAutoCompleteTextBox): | |
311 return | |
312 if keycode == KEY_ENTER: | |
313 self.onLostFocus(sender) | |
314 self._last_textbox.setFocus(True) | |
315 | |
316 def onFocus(self, sender): | |
317 """A DragAutoCompleteTextBox has the focus.""" | |
318 if not isinstance(sender, DragAutoCompleteTextBox): | |
319 return | |
320 if sender != self._last_textbox: | |
321 # save the current value before it's being modified | |
322 text = sender.getText() | |
323 self._focused_textbox_previous_value = text | |
324 self._parent.addToRemainingList(text) | |
325 sender.setCompletionItems(self._parent.getRemainingList()) | |
326 | |
327 def onLostFocus(self, sender): | |
328 """A DragAutoCompleteTextBox has lost the focus.""" | |
329 if not isinstance(sender, DragAutoCompleteTextBox): | |
330 return | |
331 self.changeRecipient(sender) | |
332 | |
333 def changeRecipient(self, sender): | |
334 """Modify the value of a DragAutoCompleteTextBox.""" | |
335 text = sender.getText() | |
336 if sender == self._last_textbox: | |
337 if text != "": | |
338 # a new box is added and the last textbox is reinitialized | |
339 self.addRecipient(text, setFocusToLastTextBox=False) | |
340 return | |
341 if text == "": | |
342 sender.remove_btn.click() | |
343 return | |
344 # text = new value needs to be removed 1. if the value is unchanged, because we | |
345 # added it when we took the focus, or 2. if the value is changed (obvious!) | |
346 self._parent.removeFromRemainingList(text) | |
347 if text == self._focused_textbox_previous_value: | |
348 return | |
349 sender.setVisibleLength(len(text)) | |
350 self._parent.addToRemainingList(self._focused_textbox_previous_value) | |
351 | |
352 def addRecipient(self, recipient, resetLastTextBox=True, setFocusToLastTextBox=True): | |
353 """Add a recipient and signal it to self._parent panel.""" | |
354 if recipient is None or recipient == "": | |
355 return | |
356 textbox = DragAutoCompleteTextBox() | |
357 textbox.addStyleName("recipientTextBox") | |
358 textbox.setText(recipient) | |
359 self.add(textbox) | |
360 try: | |
361 textbox.setVisibleLength(len(recipient)) | |
362 except: | |
363 #TODO: . how come could this happen?! len(recipient) is sometimes 0 but recipient is not empty | |
364 print "len(recipient) returns %d where recipient == %s..." % (len(recipient), recipient) | |
365 self._parent.removeFromRemainingList(recipient) | |
366 | |
367 remove_btn = Button(REMOVE_BUTTON, Visible=False) | |
368 remove_btn.addStyleName("recipientRemoveButton") | |
369 | |
370 def remove_cb(sender): | |
371 """Callback for the button to remove this recipient.""" | |
372 self.remove(textbox) | |
373 self.remove(remove_btn) | |
374 self._parent.addToRemainingList(recipient) | |
375 self._parent.setRemainingListUnsorted() | |
376 | |
377 remove_btn.addClickListener(remove_cb) | |
378 self.__remove_cbs.append(remove_cb) | |
379 self.add(remove_btn) | |
380 self.__resetLastTextBox(setFocus=setFocusToLastTextBox) | |
381 | |
382 textbox.remove_btn = remove_btn | |
383 textbox.addMouseListener(self) | |
384 textbox.addFocusListener(self) | |
385 textbox.addKeyboardListener(self) | |
386 | |
387 def emptyRecipients(self): | |
388 """Empty the list of recipients.""" | |
389 for remove_cb in self.__remove_cbs: | |
390 remove_cb() | |
391 self.__remove_cbs = [] | |
392 | |
393 def onMouseMove(self, sender): | |
394 """Mouse enters the area of a DragAutoCompleteTextBox.""" | |
395 if hasattr(sender, "remove_btn"): | |
396 sender.remove_btn.setVisible(True) | |
397 | |
398 def onMouseLeave(self, sender): | |
399 """Mouse leaves the area of a DragAutoCompleteTextBox.""" | |
400 if hasattr(sender, "remove_btn"): | |
401 Timer(1500, lambda: sender.remove_btn.setVisible(False)) | |
402 | |
403 def setRecipients(self, tab): | |
404 """Set the recipients.""" | |
405 self.emptyRecipients() | |
406 for recipient in tab: | |
407 self.addRecipient(recipient, resetLastTextBox=False) | |
408 self.__resetLastTextBox() | |
409 | |
410 def getRecipients(self): | |
411 """Get the recipients | |
412 @return: an array of string""" | |
413 tab = [] | |
414 for widget in self.getChildren(): | |
415 if isinstance(widget, DragAutoCompleteTextBox): | |
416 # not to be mixed with EMPTY_SELECTION_ITEM | |
417 if widget.getText() != "": | |
418 tab.append(widget.getText()) | |
419 return tab | |
420 | |
421 def getTargetDropCell(self): | |
422 """Returns self or another panel where something has been dropped.""" | |
423 return self._parent.getTargetDropCell() | |
424 | |
425 | |
426 class RecipientChooserPanel(DialogBox): | |
427 """Display the recipients chooser dialog. This has been implemented while | |
428 prototyping and is currently not used. Left for an eventual later use. | |
429 Replaced by the popup menu which allows to add a panel for Cc or Bcc. | |
430 """ | |
431 | |
432 def __init__(self, manager, **kwargs): | |
433 """Display a listbox for each recipient type""" | |
434 DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) | |
435 self.setHTML("Select recipients") | |
436 self.manager = manager | |
437 self.listboxes = {} | |
438 self.recipients = manager.getRecipients() | |
439 | |
440 container = VerticalPanel(Visible=True) | |
441 container.addStyleName("marginAuto") | |
442 | |
443 grid = Grid(2, len(RECIPIENT_TYPES)) | |
444 index = -1 | |
445 for key in RECIPIENT_TYPES: | |
446 index += 1 | |
447 grid.add(Label("%s:" % RECIPIENT_TYPES[key]["desc"]), 0, index) | |
448 listbox = ListBox() | |
449 listbox.setMultipleSelect(True) | |
450 listbox.setVisibleItemCount(15) | |
451 listbox.addItem(EMPTY_SELECTION_ITEM) | |
452 for element in manager.getList(): | |
453 listbox.addItem(element) | |
454 self.listboxes[key] = listbox | |
455 grid.add(listbox, 1, index) | |
456 self._reset() | |
457 | |
458 buttons = HorizontalPanel() | |
459 buttons.addStyleName("marginAuto") | |
460 btn_close = Button("Cancel", self.hide) | |
461 buttons.add(btn_close) | |
462 btn_reset = Button("Reset", self._reset) | |
463 buttons.add(btn_reset) | |
464 btn_ok = Button("OK", self._validate) | |
465 buttons.add(btn_ok) | |
466 | |
467 container.add(grid) | |
468 container.add(buttons) | |
469 | |
470 self.add(container) | |
471 self.center() | |
472 | |
473 def _reset(self): | |
474 """Reset the selections.""" | |
475 for key in RECIPIENT_TYPES: | |
476 listbox = self.listboxes[key] | |
477 for i in xrange(0, listbox.getItemCount()): | |
478 if listbox.getItemText(i) in self.recipients[key]: | |
479 listbox.setItemSelected(i, "selected") | |
480 else: | |
481 listbox.setItemSelected(i, "") | |
482 | |
483 def _validate(self): | |
484 """Sets back the selected recipients to the good sub-panels.""" | |
485 _map = {} | |
486 for key in RECIPIENT_TYPES: | |
487 selections = self.listboxes[key].getSelectedItemText() | |
488 if EMPTY_SELECTION_ITEM in selections: | |
489 selections.remove(EMPTY_SELECTION_ITEM) | |
490 _map[key] = selections | |
491 self.manager.setRecipients(_map) | |
492 self.hide() |