Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser_side/richtext.py Tue Oct 08 14:12:38 2013 +0200 @@ -0,0 +1,310 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Libervia: a Salut à Toi frontend +Copyright (C) 2013 Adrien Cossa <souliane@mailoo.org> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +from dialog import ConfirmDialog +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.Button import Button +from dialog import InfoDialog +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.Label import Label +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from recipients import RECIPIENT_TYPES, RecipientManager + +BUTTONS = { + "bold": {"tip": "Bold", "icon": "media/icons/dokuwiki/toolbar/16/bold.png"}, + "italic": {"tip": "Italic", "icon": "media/icons/dokuwiki/toolbar/16/italic.png"}, + "underline": {"tip": "Underline", "icon": "media/icons/dokuwiki/toolbar/16/underline.png"}, + "code": {"tip": "Code", "icon": "media/icons/dokuwiki/toolbar/16/mono.png"}, + "strikethrough": {"tip": "Strikethrough", "icon": "media/icons/dokuwiki/toolbar/16/strike.png"}, + "heading": {"tip": "Heading", "icon": "media/icons/dokuwiki/toolbar/16/hequal.png"}, + "numberedlist": {"tip": "Numbered List", "icon": "media/icons/dokuwiki/toolbar/16/ol.png"}, + "list": {"tip": "List", "icon": "media/icons/dokuwiki/toolbar/16/ul.png"}, + "link": {"tip": "Link", "icon": "media/icons/dokuwiki/toolbar/16/linkextern.png"}, + "horizontalrule": {"tip": "Horizontal rule", "icon": "media/icons/dokuwiki/toolbar/16/hr.png"} + } + +# Define here your formats, the key must match the ones used in button. +# Tupples values must have 3 elements : prefix to the selection or cursor +# position, sample text to write if the marker is not applied on a selection, +# suffix to the selection or cursor position. +FORMATS = {"markdown": {"bold": ("**", "bold", "**"), + "italic": ("*", "italic", "*"), + "code": ("`", "code", "`"), + "heading": ("\n# ", "Heading 1", "\n## Heading 2\n"), + "list": ("\n* ", "item", "\n + subitem\n"), + "link": ("[desc](", "link", ")"), + "horizontalrule": ("\n***\n", "", "") + }, + "bbcode": {"bold": ("[b]", "bold", "[/b]"), + "italic": ("[i]", "italic", "[/i]"), + "underline": ("[u]", "underline", "[/u]"), + "strikethrough": ("[s]", "strikethrough", "[/s]"), + "code": ("[code]", "code", "[/code]"), + "link": ("[url=", "link", "]desc[/url]"), + "list": ("\n[list] [*]", "item 1", " [*]item 2 [/list]\n") + }, + "dokuwiki": {"bold": ("**", "bold", "**"), + "italic": ("//", "italic", "//"), + "underline": ("__", "underline", "__"), + "strikethrough": ("<del>", "strikethrough", "</del>"), + "code": ("<code>", "code", "</code>"), + "heading": ("\n==== ", "Heading 1", " ====\n=== Heading 2 ===\n"), + "link": ("[[", "link", "|desc]]"), + "list": ("\n * ", "item\n", "\n * subitem\n"), + "horizontalrule": ("\n----\n", "", "") + }, + "html": {"bold": ("<b>", "bold", "</b>"), + "italic": ("<i>", "italic", "</i>"), + "underline": ("<u>", "underline", "</u>"), + "strikethrough": ("<s>", "strikethrough", "</s>"), + "code": ("<pre>", "code", "</pre>"), + "heading": ("\n<h3>", "Heading 1", "</h3>\n<h4>Heading 2</h4>\n"), + "link": ("<a href=\"", "link", "\">desc</a>"), + "list": ("\n<ul><li>", "item 1", "</li><li>item 2</li></ul>\n"), + "horizontalrule": ("\n<hr/>\n", "", "") + } + + } + +FORMAT_PARAM_KEY = "Composition and addressing" +FORMAT_PARAM_NAME = "Format for rich text message composition" + + +class RichTextEditor(FlexTable): + """Panel for the rich text editor.""" + + def __init__(self, host, parent=None, onCloseCallback=None): + """Fill the editor with recipients panel, toolbar, text area...""" + + # TODO: don't forget to comment this before commit + self._debug = False + + # This must be done before FlexTable.__init__ because it is used by setVisible + self.host = host + + offset1 = len(RECIPIENT_TYPES) + offset2 = len(FORMATS) if self._debug else 1 + FlexTable.__init__(self, offset1 + offset2 + 2, 2) + self.addStyleName('richTextEditor') + + self._parent = parent + self._on_close_callback = onCloseCallback + + # recipient types sub-panels are automatically added by the manager + self.recipient = RecipientManager(self) + self.recipient.createWidgets() + + # Rich text tool bar is automatically added by setVisible + + self.textarea = TextArea() + self.textarea.addStyleName('richTextArea') + + self.command = HorizontalPanel() + self.command.addStyleName("marginAuto") + self.command.add(Button("Cancel", listener=self.cancelWithoutSaving)) + self.command.add(Button("Back to quick box", listener=self.closeAndSave)) + self.command.add(Button("Send message", listener=self.sendMessage)) + + self.getFlexCellFormatter().setColSpan(offset1 + offset2, 0, 2) + self.getFlexCellFormatter().setColSpan(offset1 + offset2 + 1, 0, 2) + self.setWidget(offset1 + offset2, 0, self.textarea) + self.setWidget(offset1 + offset2 + 1, 0, self.command) + + @classmethod + def getOrCreate(cls, host, parent=None, onCloseCallback=None): + """Get or create the richtext editor associated to that host. + Add it to parent if parent is not None, otherwise display it + in a popup dialog. Information are saved for later the widget + to be also automatically removed from its parent, or the + popup to be closed. + @param host: the host + @popup parent: parent panel (in a popup if parent == None) . + @return: the RichTextEditor instance if popup is False, otherwise + a popup DialogBox containing the RichTextEditor. + """ + if not hasattr(host, 'richtext'): + host.richtext = RichTextEditor(host, parent, onCloseCallback) + + def add(widget, parent): + if widget.getParent() is not None: + if widget.getParent() != parent: + widget.removeFromParent() + parent.add(widget) + else: + parent.add(widget) + widget.setVisible(True) + + if parent is None: + if not hasattr(host.richtext, 'popup'): + host.richtext.popup = DialogBox(autoHide=False, centered=True) + host.richtext.popup.setHTML("Compose your message") + host.richtext.popup.add(host.richtext) + add(host.richtext, host.richtext.popup) + host.richtext.popup.center() + else: + add(host.richtext, parent) + host.richtext.syncFromUniBox() + return host.richtext.popup if parent is None else host.richtext + + def setVisible(self, kwargs): + """Called each time the widget is displayed, after creation or after having been hidden.""" + self.host.bridge.call('asyncGetParamA', self.setToolBar, FORMAT_PARAM_NAME, FORMAT_PARAM_KEY) + FlexTable.setVisible(self, kwargs) + + def __close(self): + """Remove the widget from parent or close the popup.""" + if self._parent is None: + self.popup.hide() + else: + self.setVisible(False) + if self._on_close_callback is not None: + self._on_close_callback() + + def setToolBar(self, _format): + """This method is called asynchronously after the parameter + holding the rich text format is retrieved. It is called at + each opening of the rich text editor because the user may + have change his setting since the last time.""" + if _format is None or _format not in FORMATS.keys(): + _format = FORMATS.keys()[0] + if hasattr(self, "_format") and self._format == _format: + return + self._format = _format + offset1 = len(RECIPIENT_TYPES) + count = 0 + for _format in FORMATS.keys() if self._debug else [self._format]: + toolbar = HorizontalPanel() + toolbar.addStyleName('richTextToolbar') + for key in FORMATS[_format].keys(): + self.addToolbarButton(toolbar, _format, key) + label = Label("Format: %s" % _format) + label.addStyleName("richTextFormatLabel") + toolbar.add(label) + self.getFlexCellFormatter().setColSpan(offset1 + count, 0, 2) + self.setWidget(offset1 + count, 0, toolbar) + count += 1 + + def addToolbarButton(self, toolbar, _format, key): + """Add a button with the defined parameters.""" + button = Button('<img src="%s" class="richTextIcon" />' % + BUTTONS[key]["icon"]) + button.setTitle(BUTTONS[key]["tip"]) + button.addStyleName('richTextToolButton') + toolbar.add(button) + + def button_callback(): + """Generic callback for a toolbar button.""" + text = self.textarea.getText() + cursor_pos = self.textarea.getCursorPos() + selection_length = self.textarea.getSelectionLength() + infos = FORMATS[_format][key] + if selection_length == 0: + middle_text = infos[1] + else: + middle_text = text[cursor_pos:cursor_pos + selection_length] + self.textarea.setText(text[:cursor_pos] + + infos[0] + + middle_text + + infos[2] + + text[cursor_pos + selection_length:]) + self.textarea.setCursorPos(cursor_pos + len(infos[0]) + len(middle_text)) + self.textarea.setFocus(True) + + button.addClickListener(button_callback) + + def syncFromUniBox(self): + """Synchronize from unibox.""" + data, target = self.host.uni_box.getTargetAndData() + self.recipient.setRecipients({"To": [target]} if target else {}) + self.textarea.setText(data if data else "") + + def syncToUniBox(self, recipients=None): + """Synchronize to unibox if a maximum of one recipient is set, + and it is not set to for optional recipient type. That means + synchronization is not done if more then one recipients are set + or if a recipient is set to an optional type (Cc, Bcc). + @return True if the sync could be done, False otherwise""" + if recipients is None: + recipients = self.recipient.getRecipients() + target = "" + # we could eventually allow more in the future + allowed = 1 + for key in recipients: + count = len(recipients[key]) + if count == 0: + continue + allowed -= count + if allowed < 0 or RECIPIENT_TYPES[key]["optional"]: + return False + # TODO: change this if later more then one recipients are allowed + target = recipients[key][0] + self.host.uni_box.setText(self.textarea.getText()) + from panels import ChatPanel, MicroblogPanel + if target == "": + return True + if target.startswith("@"): + _class = MicroblogPanel + target = None if target == "@@" else target[1:] + else: + _class = ChatPanel + self.host.getOrCreateLiberviaWidget(_class, target) + return True + + def cancelWithoutSaving(self): + """Ask for confirmation before closing the dialog.""" + def confirm_cb(answer): + if answer: + self.__close() + + _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel this message?") + _dialog.show() + + def closeAndSave(self): + """Synchronize to unibox and close the dialog afterward. Display + a message and leave the dialog open if the sync was not possible.""" + if self.syncToUniBox(): + self.__close() + return + InfoDialog("Too many recipients", + "A message with more than one direct recipient (To)," + + " or with any special recipient (Cc or Bcc), could not be" + + " stored in the quick box.\n\nPlease finish your composing" + + " in the rich text editor, and send your message directly" + + " from here.", Width="400px").center() + + def sendMessage(self): + """Send the message.""" + recipients = self.recipient.getRecipients() + if self.syncToUniBox(recipients): + # also check that we actually have a message target and data + if len(recipients["To"]) > 0 and self.textarea.getText() != "": + from pyjamas.ui.KeyboardListener import KEY_ENTER + self.host.uni_box.onKeyPress(self.host.uni_box, KEY_ENTER, None) + self.__close() + else: + InfoDialog("Missing information", + "Some information are missing and the message hasn't been sent," + + " but it has been stored to the quick box instead.", Width="400px").center() + return + InfoDialog("Feature in development", + "Sending a message to more the one recipient," + + " to Cc or Bcc is not implemented yet!", Width="400px").center()