view 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 source

#!/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()