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