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