Mercurial > libervia-web
comparison browser_side/richtext.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 | 146fc6739951 |
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 dialog import ConfirmDialog | |
23 from pyjamas.ui.TextArea import TextArea | |
24 from pyjamas.ui.Button import Button | |
25 from dialog import InfoDialog | |
26 from pyjamas.ui.DialogBox import DialogBox | |
27 from pyjamas.ui.Label import Label | |
28 from pyjamas.ui.FlexTable import FlexTable | |
29 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
30 from recipients import RECIPIENT_TYPES, RecipientManager | |
31 | |
32 BUTTONS = { | |
33 "bold": {"tip": "Bold", "icon": "media/icons/dokuwiki/toolbar/16/bold.png"}, | |
34 "italic": {"tip": "Italic", "icon": "media/icons/dokuwiki/toolbar/16/italic.png"}, | |
35 "underline": {"tip": "Underline", "icon": "media/icons/dokuwiki/toolbar/16/underline.png"}, | |
36 "code": {"tip": "Code", "icon": "media/icons/dokuwiki/toolbar/16/mono.png"}, | |
37 "strikethrough": {"tip": "Strikethrough", "icon": "media/icons/dokuwiki/toolbar/16/strike.png"}, | |
38 "heading": {"tip": "Heading", "icon": "media/icons/dokuwiki/toolbar/16/hequal.png"}, | |
39 "numberedlist": {"tip": "Numbered List", "icon": "media/icons/dokuwiki/toolbar/16/ol.png"}, | |
40 "list": {"tip": "List", "icon": "media/icons/dokuwiki/toolbar/16/ul.png"}, | |
41 "link": {"tip": "Link", "icon": "media/icons/dokuwiki/toolbar/16/linkextern.png"}, | |
42 "horizontalrule": {"tip": "Horizontal rule", "icon": "media/icons/dokuwiki/toolbar/16/hr.png"} | |
43 } | |
44 | |
45 # Define here your formats, the key must match the ones used in button. | |
46 # Tupples values must have 3 elements : prefix to the selection or cursor | |
47 # position, sample text to write if the marker is not applied on a selection, | |
48 # suffix to the selection or cursor position. | |
49 FORMATS = {"markdown": {"bold": ("**", "bold", "**"), | |
50 "italic": ("*", "italic", "*"), | |
51 "code": ("`", "code", "`"), | |
52 "heading": ("\n# ", "Heading 1", "\n## Heading 2\n"), | |
53 "list": ("\n* ", "item", "\n + subitem\n"), | |
54 "link": ("[desc](", "link", ")"), | |
55 "horizontalrule": ("\n***\n", "", "") | |
56 }, | |
57 "bbcode": {"bold": ("[b]", "bold", "[/b]"), | |
58 "italic": ("[i]", "italic", "[/i]"), | |
59 "underline": ("[u]", "underline", "[/u]"), | |
60 "strikethrough": ("[s]", "strikethrough", "[/s]"), | |
61 "code": ("[code]", "code", "[/code]"), | |
62 "link": ("[url=", "link", "]desc[/url]"), | |
63 "list": ("\n[list] [*]", "item 1", " [*]item 2 [/list]\n") | |
64 }, | |
65 "dokuwiki": {"bold": ("**", "bold", "**"), | |
66 "italic": ("//", "italic", "//"), | |
67 "underline": ("__", "underline", "__"), | |
68 "strikethrough": ("<del>", "strikethrough", "</del>"), | |
69 "code": ("<code>", "code", "</code>"), | |
70 "heading": ("\n==== ", "Heading 1", " ====\n=== Heading 2 ===\n"), | |
71 "link": ("[[", "link", "|desc]]"), | |
72 "list": ("\n * ", "item\n", "\n * subitem\n"), | |
73 "horizontalrule": ("\n----\n", "", "") | |
74 }, | |
75 "html": {"bold": ("<b>", "bold", "</b>"), | |
76 "italic": ("<i>", "italic", "</i>"), | |
77 "underline": ("<u>", "underline", "</u>"), | |
78 "strikethrough": ("<s>", "strikethrough", "</s>"), | |
79 "code": ("<pre>", "code", "</pre>"), | |
80 "heading": ("\n<h3>", "Heading 1", "</h3>\n<h4>Heading 2</h4>\n"), | |
81 "link": ("<a href=\"", "link", "\">desc</a>"), | |
82 "list": ("\n<ul><li>", "item 1", "</li><li>item 2</li></ul>\n"), | |
83 "horizontalrule": ("\n<hr/>\n", "", "") | |
84 } | |
85 | |
86 } | |
87 | |
88 FORMAT_PARAM_KEY = "Composition and addressing" | |
89 FORMAT_PARAM_NAME = "Format for rich text message composition" | |
90 | |
91 | |
92 class RichTextEditor(FlexTable): | |
93 """Panel for the rich text editor.""" | |
94 | |
95 def __init__(self, host, parent=None, onCloseCallback=None): | |
96 """Fill the editor with recipients panel, toolbar, text area...""" | |
97 | |
98 # TODO: don't forget to comment this before commit | |
99 self._debug = False | |
100 | |
101 # This must be done before FlexTable.__init__ because it is used by setVisible | |
102 self.host = host | |
103 | |
104 offset1 = len(RECIPIENT_TYPES) | |
105 offset2 = len(FORMATS) if self._debug else 1 | |
106 FlexTable.__init__(self, offset1 + offset2 + 2, 2) | |
107 self.addStyleName('richTextEditor') | |
108 | |
109 self._parent = parent | |
110 self._on_close_callback = onCloseCallback | |
111 | |
112 # recipient types sub-panels are automatically added by the manager | |
113 self.recipient = RecipientManager(self) | |
114 self.recipient.createWidgets() | |
115 | |
116 # Rich text tool bar is automatically added by setVisible | |
117 | |
118 self.textarea = TextArea() | |
119 self.textarea.addStyleName('richTextArea') | |
120 | |
121 self.command = HorizontalPanel() | |
122 self.command.addStyleName("marginAuto") | |
123 self.command.add(Button("Cancel", listener=self.cancelWithoutSaving)) | |
124 self.command.add(Button("Back to quick box", listener=self.closeAndSave)) | |
125 self.command.add(Button("Send message", listener=self.sendMessage)) | |
126 | |
127 self.getFlexCellFormatter().setColSpan(offset1 + offset2, 0, 2) | |
128 self.getFlexCellFormatter().setColSpan(offset1 + offset2 + 1, 0, 2) | |
129 self.setWidget(offset1 + offset2, 0, self.textarea) | |
130 self.setWidget(offset1 + offset2 + 1, 0, self.command) | |
131 | |
132 @classmethod | |
133 def getOrCreate(cls, host, parent=None, onCloseCallback=None): | |
134 """Get or create the richtext editor associated to that host. | |
135 Add it to parent if parent is not None, otherwise display it | |
136 in a popup dialog. Information are saved for later the widget | |
137 to be also automatically removed from its parent, or the | |
138 popup to be closed. | |
139 @param host: the host | |
140 @popup parent: parent panel (in a popup if parent == None) . | |
141 @return: the RichTextEditor instance if popup is False, otherwise | |
142 a popup DialogBox containing the RichTextEditor. | |
143 """ | |
144 if not hasattr(host, 'richtext'): | |
145 host.richtext = RichTextEditor(host, parent, onCloseCallback) | |
146 | |
147 def add(widget, parent): | |
148 if widget.getParent() is not None: | |
149 if widget.getParent() != parent: | |
150 widget.removeFromParent() | |
151 parent.add(widget) | |
152 else: | |
153 parent.add(widget) | |
154 widget.setVisible(True) | |
155 | |
156 if parent is None: | |
157 if not hasattr(host.richtext, 'popup'): | |
158 host.richtext.popup = DialogBox(autoHide=False, centered=True) | |
159 host.richtext.popup.setHTML("Compose your message") | |
160 host.richtext.popup.add(host.richtext) | |
161 add(host.richtext, host.richtext.popup) | |
162 host.richtext.popup.center() | |
163 else: | |
164 add(host.richtext, parent) | |
165 host.richtext.syncFromUniBox() | |
166 return host.richtext.popup if parent is None else host.richtext | |
167 | |
168 def setVisible(self, kwargs): | |
169 """Called each time the widget is displayed, after creation or after having been hidden.""" | |
170 self.host.bridge.call('asyncGetParamA', self.setToolBar, FORMAT_PARAM_NAME, FORMAT_PARAM_KEY) | |
171 FlexTable.setVisible(self, kwargs) | |
172 | |
173 def __close(self): | |
174 """Remove the widget from parent or close the popup.""" | |
175 if self._parent is None: | |
176 self.popup.hide() | |
177 else: | |
178 self.setVisible(False) | |
179 if self._on_close_callback is not None: | |
180 self._on_close_callback() | |
181 | |
182 def setToolBar(self, _format): | |
183 """This method is called asynchronously after the parameter | |
184 holding the rich text format is retrieved. It is called at | |
185 each opening of the rich text editor because the user may | |
186 have change his setting since the last time.""" | |
187 if _format is None or _format not in FORMATS.keys(): | |
188 _format = FORMATS.keys()[0] | |
189 if hasattr(self, "_format") and self._format == _format: | |
190 return | |
191 self._format = _format | |
192 offset1 = len(RECIPIENT_TYPES) | |
193 count = 0 | |
194 for _format in FORMATS.keys() if self._debug else [self._format]: | |
195 toolbar = HorizontalPanel() | |
196 toolbar.addStyleName('richTextToolbar') | |
197 for key in FORMATS[_format].keys(): | |
198 self.addToolbarButton(toolbar, _format, key) | |
199 label = Label("Format: %s" % _format) | |
200 label.addStyleName("richTextFormatLabel") | |
201 toolbar.add(label) | |
202 self.getFlexCellFormatter().setColSpan(offset1 + count, 0, 2) | |
203 self.setWidget(offset1 + count, 0, toolbar) | |
204 count += 1 | |
205 | |
206 def addToolbarButton(self, toolbar, _format, key): | |
207 """Add a button with the defined parameters.""" | |
208 button = Button('<img src="%s" class="richTextIcon" />' % | |
209 BUTTONS[key]["icon"]) | |
210 button.setTitle(BUTTONS[key]["tip"]) | |
211 button.addStyleName('richTextToolButton') | |
212 toolbar.add(button) | |
213 | |
214 def button_callback(): | |
215 """Generic callback for a toolbar button.""" | |
216 text = self.textarea.getText() | |
217 cursor_pos = self.textarea.getCursorPos() | |
218 selection_length = self.textarea.getSelectionLength() | |
219 infos = FORMATS[_format][key] | |
220 if selection_length == 0: | |
221 middle_text = infos[1] | |
222 else: | |
223 middle_text = text[cursor_pos:cursor_pos + selection_length] | |
224 self.textarea.setText(text[:cursor_pos] | |
225 + infos[0] | |
226 + middle_text | |
227 + infos[2] | |
228 + text[cursor_pos + selection_length:]) | |
229 self.textarea.setCursorPos(cursor_pos + len(infos[0]) + len(middle_text)) | |
230 self.textarea.setFocus(True) | |
231 | |
232 button.addClickListener(button_callback) | |
233 | |
234 def syncFromUniBox(self): | |
235 """Synchronize from unibox.""" | |
236 data, target = self.host.uni_box.getTargetAndData() | |
237 self.recipient.setRecipients({"To": [target]} if target else {}) | |
238 self.textarea.setText(data if data else "") | |
239 | |
240 def syncToUniBox(self, recipients=None): | |
241 """Synchronize to unibox if a maximum of one recipient is set, | |
242 and it is not set to for optional recipient type. That means | |
243 synchronization is not done if more then one recipients are set | |
244 or if a recipient is set to an optional type (Cc, Bcc). | |
245 @return True if the sync could be done, False otherwise""" | |
246 if recipients is None: | |
247 recipients = self.recipient.getRecipients() | |
248 target = "" | |
249 # we could eventually allow more in the future | |
250 allowed = 1 | |
251 for key in recipients: | |
252 count = len(recipients[key]) | |
253 if count == 0: | |
254 continue | |
255 allowed -= count | |
256 if allowed < 0 or RECIPIENT_TYPES[key]["optional"]: | |
257 return False | |
258 # TODO: change this if later more then one recipients are allowed | |
259 target = recipients[key][0] | |
260 self.host.uni_box.setText(self.textarea.getText()) | |
261 from panels import ChatPanel, MicroblogPanel | |
262 if target == "": | |
263 return True | |
264 if target.startswith("@"): | |
265 _class = MicroblogPanel | |
266 target = None if target == "@@" else target[1:] | |
267 else: | |
268 _class = ChatPanel | |
269 self.host.getOrCreateLiberviaWidget(_class, target) | |
270 return True | |
271 | |
272 def cancelWithoutSaving(self): | |
273 """Ask for confirmation before closing the dialog.""" | |
274 def confirm_cb(answer): | |
275 if answer: | |
276 self.__close() | |
277 | |
278 _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel this message?") | |
279 _dialog.show() | |
280 | |
281 def closeAndSave(self): | |
282 """Synchronize to unibox and close the dialog afterward. Display | |
283 a message and leave the dialog open if the sync was not possible.""" | |
284 if self.syncToUniBox(): | |
285 self.__close() | |
286 return | |
287 InfoDialog("Too many recipients", | |
288 "A message with more than one direct recipient (To)," + | |
289 " or with any special recipient (Cc or Bcc), could not be" + | |
290 " stored in the quick box.\n\nPlease finish your composing" + | |
291 " in the rich text editor, and send your message directly" + | |
292 " from here.", Width="400px").center() | |
293 | |
294 def sendMessage(self): | |
295 """Send the message.""" | |
296 recipients = self.recipient.getRecipients() | |
297 if self.syncToUniBox(recipients): | |
298 # also check that we actually have a message target and data | |
299 if len(recipients["To"]) > 0 and self.textarea.getText() != "": | |
300 from pyjamas.ui.KeyboardListener import KEY_ENTER | |
301 self.host.uni_box.onKeyPress(self.host.uni_box, KEY_ENTER, None) | |
302 self.__close() | |
303 else: | |
304 InfoDialog("Missing information", | |
305 "Some information are missing and the message hasn't been sent," + | |
306 " but it has been stored to the quick box instead.", Width="400px").center() | |
307 return | |
308 InfoDialog("Feature in development", | |
309 "Sending a message to more the one recipient," + | |
310 " to Cc or Bcc is not implemented yet!", Width="400px").center() |