Mercurial > libervia-web
comparison src/browser/richtext.py @ 449:981ed669d3b3
/!\ reorganize all the file hierarchy, move the code and launching script to src:
- browser_side --> src/browser
- public --> src/browser_side/public
- libervia.py --> src/browser/libervia_main.py
- libervia_server --> src/server
- libervia_server/libervia.sh --> src/libervia.sh
- twisted --> src/twisted
- new module src/common
- split constants.py in 3 files:
- src/common/constants.py
- src/browser/constants.py
- src/server/constants.py
- output --> html (generated by pyjsbuild during the installation)
- new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css
- setup.py installs libervia to the following paths:
- src/common --> <LIB>/libervia/common
- src/server --> <LIB>/libervia/server
- src/twisted --> <LIB>/twisted
- html --> <SHARE>/libervia/html
- server_side --> <SHARE>libervia/server_side
- LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation:
- clean: remove previous installation directories
- purge: remove building and previous installation directories
You may need to update your sat.conf and/or launching script to update the following options/parameters:
- ssl_certificate
- data_dir
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 20 May 2014 06:41:16 +0200 |
parents | browser_side/richtext.py@bbdbee25123a |
children |
comparison
equal
deleted
inserted
replaced
448:14c35f7f1ef5 | 449:981ed669d3b3 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat_frontends.tools import composition | |
21 from sat.core.i18n import _ | |
22 | |
23 from pyjamas.ui.TextArea import TextArea | |
24 from pyjamas.ui.Button import Button | |
25 from pyjamas.ui.CheckBox import CheckBox | |
26 from pyjamas.ui.DialogBox import DialogBox | |
27 from pyjamas.ui.Label import Label | |
28 from pyjamas.ui.HTML import HTML | |
29 from pyjamas.ui.FlexTable import FlexTable | |
30 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
31 from pyjamas import Window | |
32 from pyjamas.ui.KeyboardListener import KeyboardHandler | |
33 from __pyjamas__ import doc | |
34 | |
35 from constants import Const as C | |
36 from dialog import ConfirmDialog, InfoDialog | |
37 from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor | |
38 from list_manager import ListManager | |
39 from html_tools import html_sanitize | |
40 import panels | |
41 | |
42 | |
43 class RichTextEditor(BaseTextEditor, FlexTable): | |
44 """Panel for the rich text editor.""" | |
45 | |
46 def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None): | |
47 """ | |
48 @param host: the SatWebFrontend instance | |
49 @param content: dict with at least a 'text' key | |
50 @param modifiedCb: method to be called when the text has been modified | |
51 @param afterEditCb: method to be called when the edition is done | |
52 @param options: list of UI options (see self.readOptions) | |
53 """ | |
54 self.host = host | |
55 self._debug = False # TODO: don't forget to set it False before commit | |
56 self.wysiwyg = False | |
57 self.__readOptions(options) | |
58 self.style = {'main': 'richTextEditor', | |
59 'title': 'richTextTitle', | |
60 'toolbar': 'richTextToolbar', | |
61 'textarea': 'richTextArea'} | |
62 if isinstance(style, dict): | |
63 self.style.update(style) | |
64 self._prepareUI() | |
65 BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) | |
66 | |
67 def __readOptions(self, options): | |
68 """Set the internal flags according to the given options.""" | |
69 if options is None: | |
70 options = [] | |
71 self.read_only = 'read_only' in options | |
72 self.update_msg = 'update_msg' in options | |
73 self.no_title = 'no_title' in options or self.read_only | |
74 self.no_command = 'no_command' in options or self.read_only | |
75 | |
76 def _prepareUI(self, y_offset=0): | |
77 """Prepare the UI to host title panel, toolbar, text area... | |
78 @param y_offset: Y offset to start from (extra rows on top)""" | |
79 if not self.read_only: | |
80 self.title_offset = y_offset | |
81 self.toolbar_offset = self.title_offset + (0 if self.no_title else 1) | |
82 self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1) | |
83 self.command_offset = self.content_offset + 1 | |
84 else: | |
85 self.title_offset = self.toolbar_offset = self.content_offset = y_offset | |
86 self.command_offset = self.content_offset + 1 | |
87 FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2) | |
88 self.addStyleName(self.style['main']) | |
89 | |
90 def addEditListener(self, listener): | |
91 """Add a method to be called whenever the text is edited. | |
92 @param listener: method taking two arguments: sender, keycode""" | |
93 BaseTextEditor.addEditListener(self, listener) | |
94 if hasattr(self, 'display'): | |
95 self.display.addEditListener(listener) | |
96 | |
97 def refresh(self, edit=None): | |
98 """Refresh the UI for edition/display mode | |
99 @param edit: set to True to display the edition mode""" | |
100 if edit is None: | |
101 edit = hasattr(self, 'textarea') and self.textarea.getVisible() | |
102 | |
103 for widget in ['title_panel', 'command']: | |
104 if hasattr(self, widget): | |
105 getattr(self, widget).setVisible(edit) | |
106 | |
107 if hasattr(self, 'toolbar'): | |
108 self.toolbar.setVisible(False) | |
109 if not hasattr(self, 'display'): | |
110 self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode | |
111 for listener in self.edit_listeners: | |
112 self.display.addEditListener(listener) | |
113 if not self.read_only and not hasattr(self, 'textarea'): | |
114 self.textarea = EditTextArea(self) # for edition mode | |
115 self.textarea.addStyleName(self.style['textarea']) | |
116 | |
117 self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) | |
118 if edit and not self.wysiwyg: | |
119 self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why | |
120 self.setWidget(self.content_offset, 0, self.textarea) | |
121 else: | |
122 self.setWidget(self.content_offset, 0, self.display) | |
123 if not edit: | |
124 return | |
125 | |
126 if not self.no_title and not hasattr(self, 'title_panel'): | |
127 self.title_panel = TitlePanel() | |
128 self.title_panel.addStyleName(self.style['title']) | |
129 self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) | |
130 self.setWidget(self.title_offset, 0, self.title_panel) | |
131 | |
132 if not self.no_command and not hasattr(self, 'command'): | |
133 self.command = HorizontalPanel() | |
134 self.command.addStyleName("marginAuto") | |
135 self.command.add(Button("Cancel", lambda: self.edit(True, True))) | |
136 self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) | |
137 self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) | |
138 self.setWidget(self.command_offset, 0, self.command) | |
139 | |
140 def setToolBar(self, syntax): | |
141 """This method is called asynchronously after the parameter | |
142 holding the rich text syntax is retrieved. It is called at | |
143 each call of self.edit(True) because the user may | |
144 have change his setting since the last time.""" | |
145 if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): | |
146 syntax = composition.RICH_SYNTAXES.keys()[0] | |
147 if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: | |
148 self.toolbar.setVisible(True) | |
149 return | |
150 count = 0 | |
151 for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]: | |
152 self.toolbar = HorizontalPanel() | |
153 self.toolbar.syntax = syntax | |
154 self.toolbar.addStyleName(self.style['toolbar']) | |
155 for key in composition.RICH_SYNTAXES[syntax].keys(): | |
156 self.addToolbarButton(syntax, key) | |
157 self.wysiwyg_button = CheckBox(_('WYSIWYG edition')) | |
158 wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) | |
159 self.wysiwyg_button.addClickListener(wysiywgCb) | |
160 self.toolbar.add(self.wysiwyg_button) | |
161 self.syntax_label = Label(_("Syntax: %s") % syntax) | |
162 self.syntax_label.addStyleName("richTextSyntaxLabel") | |
163 self.toolbar.add(self.syntax_label) | |
164 self.toolbar.setCellWidth(self.syntax_label, "100%") | |
165 self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2) | |
166 self.setWidget(self.toolbar_offset + count, 0, self.toolbar) | |
167 count += 1 | |
168 | |
169 def setWysiwyg(self, wysiwyg, init=False): | |
170 """Toggle the edition mode between rich content syntax and wysiwyg. | |
171 @param wysiwyg: boolean value | |
172 @param init: set to True to re-init without switching the widgets.""" | |
173 def setWysiwyg(): | |
174 self.wysiwyg = wysiwyg | |
175 try: | |
176 self.wysiwyg_button.setChecked(wysiwyg) | |
177 except TypeError: | |
178 pass | |
179 try: | |
180 if wysiwyg: | |
181 self.syntax_label.addStyleName('transparent') | |
182 else: | |
183 self.syntax_label.removeStyleName('transparent') | |
184 except TypeError: | |
185 pass | |
186 if not wysiwyg: | |
187 self.display.removeStyleName('richTextWysiwyg') | |
188 | |
189 if init: | |
190 setWysiwyg() | |
191 return | |
192 | |
193 self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) | |
194 if wysiwyg: | |
195 def syntaxConvertCb(text): | |
196 self.display.setContent({'text': text}) | |
197 self.textarea.removeFromParent() # XXX: force as it is not always done... | |
198 self.setWidget(self.content_offset, 0, self.display) | |
199 self.display.addStyleName('richTextWysiwyg') | |
200 self.display.edit(True) | |
201 content = self.getContent() | |
202 if content['text'] and content['syntax'] != C.SYNTAX_XHTML: | |
203 self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) | |
204 else: | |
205 syntaxConvertCb(content['text']) | |
206 else: | |
207 syntaxConvertCb = lambda text: self.textarea.setText(text) | |
208 text = self.display.getContent()['text'] | |
209 if text and self.toolbar.syntax != C.SYNTAX_XHTML: | |
210 self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) | |
211 else: | |
212 syntaxConvertCb(text) | |
213 self.setWidget(self.content_offset, 0, self.textarea) | |
214 self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why | |
215 | |
216 setWysiwyg() # do it in the end because it affects self.getContent | |
217 | |
218 def addToolbarButton(self, syntax, key): | |
219 """Add a button with the defined parameters.""" | |
220 button = Button('<img src="%s" class="richTextIcon" />' % | |
221 composition.RICH_BUTTONS[key]["icon"]) | |
222 button.setTitle(composition.RICH_BUTTONS[key]["tip"]) | |
223 button.addStyleName('richTextToolButton') | |
224 self.toolbar.add(button) | |
225 | |
226 def buttonCb(): | |
227 """Generic callback for a toolbar button.""" | |
228 text = self.textarea.getText() | |
229 cursor_pos = self.textarea.getCursorPos() | |
230 selection_length = self.textarea.getSelectionLength() | |
231 data = composition.RICH_SYNTAXES[syntax][key] | |
232 if selection_length == 0: | |
233 middle_text = data[1] | |
234 else: | |
235 middle_text = text[cursor_pos:cursor_pos + selection_length] | |
236 self.textarea.setText(text[:cursor_pos] | |
237 + data[0] | |
238 + middle_text | |
239 + data[2] | |
240 + text[cursor_pos + selection_length:]) | |
241 self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) | |
242 self.textarea.setFocus(True) | |
243 self.textarea.onKeyDown() | |
244 | |
245 def wysiwygCb(): | |
246 """Callback for a toolbar button while wysiwyg mode is enabled.""" | |
247 data = composition.COMMANDS[key] | |
248 | |
249 def execCommand(command, arg): | |
250 self.display.setFocus(True) | |
251 doc().execCommand(command, False, arg.strip() if arg else '') | |
252 # use Window.prompt instead of dialog.PromptDialog to not loose the focus | |
253 prompt = lambda command, text: execCommand(command, Window.prompt(text)) | |
254 if isinstance(data, tuple) or isinstance(data, list): | |
255 if data[1]: | |
256 prompt(data[0], data[1]) | |
257 else: | |
258 execCommand(data[0], data[2]) | |
259 else: | |
260 execCommand(data, False, '') | |
261 self.textarea.onKeyDown() | |
262 | |
263 button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) | |
264 | |
265 def getContent(self): | |
266 assert(hasattr(self, 'textarea')) | |
267 assert(hasattr(self, 'toolbar')) | |
268 if self.wysiwyg: | |
269 content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} | |
270 else: | |
271 content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} | |
272 if hasattr(self, 'title_panel'): | |
273 content.update({'title': self.strproc(self.title_panel.getText())}) | |
274 return content | |
275 | |
276 def edit(self, edit=False, abort=False, sync=False): | |
277 """ | |
278 Remark: the editor must be visible before you call this method. | |
279 @param edit: set to True to edit the content or False to only display it | |
280 @param abort: set to True to cancel the edition and loose the changes. | |
281 If edit and abort are both True, self.abortEdition can be used to ask for a | |
282 confirmation. When edit is False and abort is True, abortion is actually done. | |
283 @param sync: set to True to cancel the edition after the content has been saved somewhere else | |
284 """ | |
285 if not (edit and abort): | |
286 self.refresh(edit) # not when we are asking for a confirmation | |
287 BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed | |
288 if (edit and abort): | |
289 return # self.abortEdition is called by BaseTextEditor.edit | |
290 self.setWysiwyg(False, init=True) # after BaseTextEditor (it affects self.getContent) | |
291 if sync: | |
292 return | |
293 # the following must NOT be done at each UI refresh! | |
294 content = self._original_content | |
295 if edit: | |
296 def getParamCb(syntax): | |
297 # set the editable text in the current user-selected syntax | |
298 def syntaxConvertCb(text=None): | |
299 if text is not None: | |
300 # Important: this also update self._original_content | |
301 content.update({'text': text}) | |
302 content.update({'syntax': syntax}) | |
303 self.textarea.setText(content['text']) | |
304 if hasattr(self, 'title_panel') and 'title' in content: | |
305 self.title_panel.setText(content['title']) | |
306 self.title_panel.setStackVisible(0, content['title'] != '') | |
307 self.setToolBar(syntax) | |
308 if content['text'] and content['syntax'] != syntax: | |
309 self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) | |
310 else: | |
311 syntaxConvertCb() | |
312 self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) | |
313 else: | |
314 if not self.initialized: | |
315 # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification | |
316 self.setDisplayContent() | |
317 self.display.edit(False) | |
318 | |
319 def setDisplayContent(self): | |
320 """Set the content of the HTMLTextEditor which is used for display/wysiwyg""" | |
321 content = self._original_content | |
322 text = content['text'] | |
323 if 'title' in content and content['title']: | |
324 text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text']) | |
325 self.display.setContent({'text': text}) | |
326 | |
327 def setFocus(self, focus): | |
328 self.textarea.setFocus(focus) | |
329 | |
330 def abortEdition(self, content): | |
331 """Ask for confirmation before closing the dialog.""" | |
332 def confirm_cb(answer): | |
333 if answer: | |
334 self.edit(False, True) | |
335 _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) | |
336 _dialog.cancel_button.setText(_("No")) | |
337 _dialog.show() | |
338 | |
339 | |
340 class RichMessageEditor(RichTextEditor): | |
341 """Use the rich text editor for sending messages with extended addressing. | |
342 Recipient panels are on top and data may be synchronized from/to the unibox.""" | |
343 | |
344 @classmethod | |
345 def getOrCreate(cls, host, parent=None, callback=None): | |
346 """Get or create the message editor associated to that host. | |
347 Add it to parent if parent is not None, otherwise display it | |
348 in a popup dialog. | |
349 @param host: the host | |
350 @param parent: parent panel (or None to display in a popup). | |
351 @return: the RichTextEditor instance if parent is not None, | |
352 otherwise a popup DialogBox containing the RichTextEditor. | |
353 """ | |
354 if not hasattr(host, 'richtext'): | |
355 modifiedCb = lambda content: True | |
356 | |
357 def afterEditCb(content): | |
358 if hasattr(host.richtext, 'popup'): | |
359 host.richtext.popup.hide() | |
360 else: | |
361 host.richtext.setVisible(False) | |
362 callback() | |
363 options = ['no_title'] | |
364 style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'} | |
365 host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style) | |
366 | |
367 def add(widget, parent): | |
368 if widget.getParent() is not None: | |
369 if widget.getParent() != parent: | |
370 widget.removeFromParent() | |
371 parent.add(widget) | |
372 else: | |
373 parent.add(widget) | |
374 widget.setVisible(True) | |
375 widget.initialized = False # fake a new creation | |
376 widget.edit(True) | |
377 | |
378 if parent is None: | |
379 if not hasattr(host.richtext, 'popup'): | |
380 host.richtext.popup = DialogBox(autoHide=False, centered=True) | |
381 host.richtext.popup.setHTML("Compose your message") | |
382 host.richtext.popup.add(host.richtext) | |
383 add(host.richtext, host.richtext.popup) | |
384 host.richtext.popup.center() | |
385 else: | |
386 add(host.richtext, parent) | |
387 return host.richtext.popup if parent is None else host.richtext | |
388 | |
389 def _prepareUI(self, y_offset=0): | |
390 """Prepare the UI to host recipients panel, toolbar, text area... | |
391 @param y_offset: Y offset to start from (extra rows on top)""" | |
392 self.recipient_offset = y_offset | |
393 self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES) | |
394 RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1) | |
395 | |
396 def refresh(self, edit=None): | |
397 """Refresh the UI between edition/display mode | |
398 @param edit: set to True to display the edition mode""" | |
399 if edit is None: | |
400 edit = hasattr(self, 'textarea') and self.textarea.getVisible() | |
401 RichTextEditor.refresh(self, edit) | |
402 | |
403 for widget in ['recipient', 'recipient_spacer']: | |
404 if hasattr(self, widget): | |
405 getattr(self, widget).setVisible(edit) | |
406 | |
407 if not edit: | |
408 return | |
409 | |
410 if not hasattr(self, 'recipient'): | |
411 # recipient types sub-panels are automatically added by the manager | |
412 self.recipient = RecipientManager(self, self.recipient_offset) | |
413 self.recipient.createWidgets(title_format="%s: ") | |
414 self.recipient_spacer = HTML('') | |
415 self.recipient_spacer.setStyleName('recipientSpacer') | |
416 self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2) | |
417 self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer) | |
418 | |
419 if not hasattr(self, 'sync_button'): | |
420 self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True)) | |
421 self.command.insert(self.sync_button, 1) | |
422 | |
423 def syncToEditor(self): | |
424 """Synchronize from unibox.""" | |
425 def setContent(target, data): | |
426 if hasattr(self, 'recipient'): | |
427 self.recipient.setContacts({"To": [target]} if target else {}) | |
428 self.setContent({'text': data if data else '', 'syntax': ''}) | |
429 self.textarea.setText(data if data else '') | |
430 data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None) | |
431 setContent(target, data) | |
432 | |
433 def __syncToUniBox(self, recipients=None, emptyText=False): | |
434 """Synchronize to unibox if a maximum of one recipient is set. | |
435 @return True if the sync could be done, False otherwise""" | |
436 if not self.host.uni_box: | |
437 return | |
438 setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text']) | |
439 if not hasattr(self, 'recipient'): | |
440 setText() | |
441 return True | |
442 if recipients is None: | |
443 recipients = self.recipient.getContacts() | |
444 target = "" | |
445 # we could eventually allow more in the future | |
446 allowed = 1 | |
447 for key in recipients: | |
448 count = len(recipients[key]) | |
449 if count == 0: | |
450 continue | |
451 allowed -= count | |
452 if allowed < 0: | |
453 return False | |
454 # TODO: change this if later more then one recipients are allowed | |
455 target = recipients[key][0] | |
456 setText() | |
457 if target == "": | |
458 return True | |
459 if target.startswith("@"): | |
460 _class = panels.MicroblogPanel | |
461 target = None if target == "@@" else target[1:] | |
462 else: | |
463 _class = panels.ChatPanel | |
464 self.host.getOrCreateLiberviaWidget(_class, target) | |
465 return True | |
466 | |
467 def syncFromEditor(self, content): | |
468 """Synchronize to unibox and close the dialog afterward. Display | |
469 a message and leave the dialog open if the sync was not possible.""" | |
470 if self.__syncToUniBox(): | |
471 self._afterEditCb(content) | |
472 return | |
473 InfoDialog("Too many recipients", | |
474 "A message with more than one direct recipient (To)," + | |
475 " or with any special recipient (Cc or Bcc), could not be" + | |
476 " stored in the quick box.\n\nPlease finish your composing" + | |
477 " in the rich text editor, and send your message directly" + | |
478 " from here.", Width="400px").center() | |
479 | |
480 def edit(self, edit=True, abort=False, sync=False): | |
481 if not edit and not abort and not sync: # force sending message even when the text has not been modified | |
482 if not self.__sendMessage(): # message has not been sent (missing information), do nothing | |
483 return | |
484 RichTextEditor.edit(self, edit, abort, sync) | |
485 | |
486 def __sendMessage(self): | |
487 """Send the message.""" | |
488 recipients = self.recipient.getContacts() | |
489 targets = [] | |
490 for addr in recipients: | |
491 for recipient in recipients[addr]: | |
492 if recipient.startswith("@"): | |
493 targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr)) | |
494 else: | |
495 targets.append(("chat", recipient, addr)) | |
496 # check that we actually have a message target and data | |
497 content = self.getContent() | |
498 if content['text'] == "" or len(targets) == 0: | |
499 InfoDialog("Missing information", | |
500 "Some information are missing and the message hasn't been sent.", Width="400px").center() | |
501 return None | |
502 self.__syncToUniBox(recipients, emptyText=True) | |
503 extra = {'content_rich': content['text']} | |
504 if hasattr(self, 'title_panel'): | |
505 extra.update({'title': content['title']}) | |
506 self.host.send(targets, content['text'], extra=extra) | |
507 return True | |
508 | |
509 | |
510 class RecipientManager(ListManager): | |
511 """A manager for sub-panels to set the recipients for each recipient type.""" | |
512 | |
513 def __init__(self, parent, y_offset=0): | |
514 # TODO: be sure we also display empty groups and disconnected contacts + their groups | |
515 # store the full list of potential recipients (groups and contacts) | |
516 list_ = [] | |
517 list_.append("@@") | |
518 list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) | |
519 list_.extend(contact for contact in parent.host.contact_panel.getContacts()) | |
520 ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset}) | |
521 | |
522 self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, | |
523 hide=lambda sender, key: self.__children[key]["panel"].isVisible(), | |
524 callback=self.setContactPanelVisible) | |
525 | |
526 | |
527 class EditTextArea(TextArea, KeyboardHandler): | |
528 def __init__(self, _parent): | |
529 TextArea.__init__(self) | |
530 self._parent = _parent | |
531 KeyboardHandler.__init__(self) | |
532 self.addKeyboardListener(self) | |
533 | |
534 def onKeyDown(self, sender=None, keycode=None, modifiers=None): | |
535 for listener in self._parent.edit_listeners: | |
536 listener(self, keycode) |