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()