changeset 351:c943fd54c90e

browser_side: heavy refactorisation for microblogs: - RichTextEditor inheritates from BaseTextEditor - stuff related to display/edition have been moved from MicroblogEntry to LightTextEditor and RichTextEditor. Now the editors has two modes for display/edition and the microblog bubble is actually the editor itself. - RichTextEditor's display mode uses a LightTextEditor (this will be used for WYSIWYG edition) - addressing stuff of RichTextEditor have been moved to a child class RichMessageEditor (used for the rich text editor when clicking on the button left to the unibox) - handle blog titles TODO: - fix encode/decode errors when sending special chars - fix images maximal width in the bubble - rich content WYSIWYG edition
author souliane <souliane@mailoo.org>
date Wed, 12 Feb 2014 15:17:04 +0100
parents f1b9ec412769
children 2610443b05a2
files browser_side/panels.py browser_side/richtext.py libervia_server/__init__.py public/libervia.css
diffstat 4 files changed, 377 insertions(+), 329 deletions(-) [+]
line wrap: on
line diff
--- a/browser_side/panels.py	Wed Feb 12 15:01:33 2014 +0100
+++ b/browser_side/panels.py	Wed Feb 12 15:17:04 2014 +0100
@@ -72,24 +72,24 @@
         enable = self.host.params_ui['unibox']['value']
         self.setVisible(enable)
         if enable and not self.unibox:
-            self.button = Button ('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
+            self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
             self.button.setTitle('Open the rich text editor')
             self.button.addStyleName('uniBoxButton')
             self.add(self.button)
             self.unibox = UniBox(self.host)
             self.add(self.unibox)
             self.setCellWidth(self.unibox, '100%')
-            self.button.addClickListener(self.openRichTextEditor)
+            self.button.addClickListener(self.openRichMessageEditor)
             self.unibox.addKey("@@: ")
 
-    def openRichTextEditor(self):
+    def openRichMessageEditor(self):
         """Open the rich text editor."""
         self.button.setVisible(False)
         self.unibox.setVisible(False)
         self.setCellWidth(self.unibox, '0px')
         self.host.panel._contactsMove(self)
 
-        def onCloseCallback():
+        def afterEditCb():
             Window.removeWindowResizeListener(self)
             self.host.panel._contactsMove(self.host.panel._hpanel)
             self.setCellWidth(self.unibox, '100%')
@@ -97,7 +97,7 @@
             self.unibox.setVisible(True)
             self.host.resize()
 
-        richtext.RichTextEditor.getOrCreate(self.host, self, onCloseCallback)
+        richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
         Window.addWindowResizeListener(self)
         self.host.resize()
 
@@ -336,8 +336,10 @@
         self.id = data['id']
         self.type = data.get('type', 'main_item')
         self.empty = data.get('new', False)
+        self.title = data.get('title', '')
+        self.title_xhtml = data.get('title_xhtml', '')
         self.content = data.get('content', '')
-        self.xhtml = data.get('xhtml', '')
+        self.content_xhtml = data.get('content_xhtml', '')
         self.author = data['author']
         self.updated = float(data.get('updated', 0))  # XXX: int doesn't work here
         try:
@@ -399,21 +401,17 @@
         self.addClickListener(self)
 
         self.pub_data = (self.hash[0], self.hash[1], self.id)
-        self._setContent()
+        self.__setContent()
 
-    def _setContent(self):
+    def __setContent(self):
         """Actually set the entry content (header, icons, bubble...)"""
         self.delete_label = self.update_label = self.comment_label = None
-        self.bubble = self.editbox = self._current_comment = None
-        self._setHeader()
-        if self.empty:
-            self.editable_content = ['', Const.SYNTAX_XHTML]
-        else:
-            self.editable_content = [self.xhtml, Const.SYNTAX_XHTML] if self.xhtml else [self.content, None]
-            self.setEntryDialog()
-        self._setIcons()
+        self.bubble = self._current_comment = None
+        self.__setHeader()
+        self.__setBubble()
+        self.__setIcons()
 
-    def _setHeader(self):
+    def __setHeader(self):
         """Set the entry header"""
         if self.empty:
             return
@@ -427,7 +425,7 @@
                                             }
                             )
 
-    def _setIcons(self):
+    def __setIcons(self):
         """Set the entry icons (delete, update, comment)"""
         if self.empty:
             return
@@ -456,128 +454,83 @@
         elif sender == self.delete_label:
             self._delete()
         elif sender == self.update_label:
-            self.setEntryDialog(edit=True)
+            self.bubble.edit(True)
         elif sender == self.comment_label:
             self._comment()
 
-    def onKeyUp(self, sender, keycode, modifiers):
-        """Update is done when ENTER key is pressed within the raw editbox"""
-        if sender != self.editbox or not self.editbox.getVisible():
-            return
-        if keycode == KEY_ENTER:
-            self._updateContent()
-
-    def onLostFocus(self, sender):
-        """Update is done when the focus leaves the raw editbox"""
-        if sender != self.editbox or not self.editbox.getVisible():
-            return
-        self._updateContent()
-
-    def _updateContent(self, cancel=False):
-        """Send the new content to the backend, remove the entry if it was
-        an empty one (used for creating a new blog post)"""
-        if not self.editbox or not self.editbox.getVisible():
-            return
-        self.entry_dialog.setWidth("auto")
-        self.entry_dialog.remove(self.edit_panel)
-        self.entry_dialog.add(self.bubble)
-        new_text = self.editbox.getText().strip()
-        self.edit_panel = self.editbox = None
-
-        def removeNewEntry():
-            if self.empty:
-                self._blog_panel.removeEntry(self.type, self.id)
-                if self.type == 'main_item':  # restore the "New message" button
-                    self._blog_panel.setUniBox(enable=False)
-                else:  # allow to create a new comment
-                    self._parent_entry._current_comment = None
-
-        if cancel or new_text == self.editable_content[0] or new_text == "":
-            removeNewEntry()
-            return
-        self.editable_content[0] = new_text
+    def __modifiedCb(self, content):
+        """Send the new content to the backend
+        @return: False to restore the original content if a deletion has been cancelled
+        """
+        if not content['text']:  # previous content has been emptied
+            self._delete(True)
+            return False
         extra = {'published': str(self.published)}
-        if self.empty or self.xhtml:
-            extra.update({'rich': new_text})
+        if self.empty or self.content_xhtml:
+            # TODO: if the user change his parameters after the message edition started,
+            # the message syntax could be different then the current syntax: pass the
+            # message syntax in extra for the frontend to use it instead of current syntax.
+            extra.update({'content_rich': content['text'], 'title': content['title']})
         if self.empty:
             if self.type == 'main_item':
-                self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, new_text, extra)
-            else:
-                self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, new_text, extra)
-        else:
-            self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, new_text, extra)
-        removeNewEntry()
-
-    def setEntryDialog(self, edit=False):
-        """Set the bubble or the editor
-        @param edit: set to True to display the editor"""
-        if edit:
-            if self.editbox and self.editbox.getVisible():
-                self.editbox.setFocus(True)
-                return
-            if self.empty or self.xhtml:
-                def cb(result):
-                    self._updateContent(result == richtext.CANCEL)
-
-                options = ['no_recipient', 'no_sync_unibox', 'no_style', 'no_close']
-                if not self.empty:
-                    options.append('update_msg')
-                editor = richtext.RichTextEditor(self._blog_panel.host, self.panel, cb, options=options)
-                editor.setWidth('100%')
-                self.editbox = editor.textarea
-                editor.setVisible(True)  # needed to build the toolbar
-                if self.editable_content[0]:
-                    self._blog_panel.host.bridge.call('syntaxConvert', lambda d: self._setOriginalText(d, editor),
-                                                      self.editable_content[0], self.editable_content[1])
-                else:
-                    self._setOriginalText("", editor)
+                self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra)
             else:
-                if not self.editbox:
-                    self.editbox = TextArea()
-                    self.editbox.addFocusListener(self)
-                    self.editbox.addKeyboardListener(self)
-                self._setOriginalText(self.editable_content[0], self.editbox)
+                self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
         else:
-            if not self.bubble:
-                self.bubble = HTML()
-                self.bubble.setStyleName("bubble")
+            self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, content['text'], extra)
+        return True
 
-            self.bubble.setHTML(addURLToText(html_sanitize(self.content)) if not self.xhtml else self.xhtml)
-            self.entry_dialog.add(self.bubble)
+    def __afterEditCb(self, content):
+        """Remove the entry if it was an empty one (used for creating a new blog post).
+        Data for the actual new blog post will be received from the bridge"""
+        if self.empty:
+            self._blog_panel.removeEntry(self.type, self.id)
+            if self.type == 'main_item':  # restore the "New message" button
+                self._blog_panel.refresh()
+            else:  # allow to create a new comment
+                self._parent_entry._current_comment = None
 
-    def _setOriginalText(self, text, container):
-        """Set the original text to be modified in the editor"""
-        text = text.strip()
-        container.original_text = text
-        self.editbox.setWidth('100%')
-        self.editbox.setText(text)
-        panel = SimplePanel()
-        panel.add(container)
-        panel.setStyleName("bubble")
-        panel.addStyleName('bubble-editbox')
-        if self.bubble:
-            self.entry_dialog.remove(self.bubble)
-        self.entry_dialog.add(panel)
-        self.editbox.setFocus(True)
-        if text:
-            self.editbox.setSelectionRange(len(text), 0)
-        self.edit_panel = panel
-        self.editable_content = [text, container.format if isinstance(container, richtext.RichTextEditor) else None]
+    def __setBubble(self):
+        """Set the bubble displaying the initial content."""
+        content = {'text': self.content_xhtml if self.content_xhtml else self.content,
+                   'title': self.title_xhtml if self.title_xhtml else self.title}
+        if self.empty or self.content_xhtml:  # new message and rich text message
+            content.update({'syntax': Const.SYNTAX_XHTML})
+            if self.author != self._blog_panel.host.whoami.bare:
+                options = ['read_only']
+            else:
+                options = [] if self.empty else ['update_msg']
+            self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
+        else:  # assume raw text message have no title
+            self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, True)
+        self.bubble.setStyleName("bubble")
+        self.entry_dialog.add(self.bubble)
+        self.bubble.edit(False)
 
-    def _delete(self):
-        """Ask confirmation for deletion"""
+    def _delete(self, empty=False):
+        """Ask confirmation for deletion.
+        @return: False if the deletion has been cancelled."""
         def confirm_cb(answer):
             if answer:
                 self._blog_panel.host.bridge.call('deleteMblog', None, self.pub_data, self.comments)
+            else:  # restore the text if it has been emptied during the edition
+                self.bubble.setContent(self.bubble._original_content)
 
-        target = 'message and all its comments' if self.comments else 'comment'
-        _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete this %s?" % target)
-        _dialog.show()
+        if self.empty:
+            text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
+            dialog.InfoDialog(_("Information"), text).show()
+            return
+        text = ""
+        if empty:
+            text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
+        target = _('message and all its comments') if self.comments else _('comment')
+        text += _("Do you really want to delete this %s?") % target
+        dialog.ConfirmDialog(confirm_cb, text=text).show()
 
     def _comment(self):
         """Add an empty entry for a new comment"""
         if self._current_comment:
-            self._current_comment.editbox.setFocus(True)
+            self._current_comment.bubble.setFocus(True)
             return
         data = {'id': str(time()),
                 'new': True,
@@ -589,7 +542,7 @@
         entry = self._blog_panel.addEntry(data)
         entry._parent_entry = self
         self._current_comment = entry
-        entry.setEntryDialog(edit=True)
+        entry.bubble.edit(True)
 
 
 class MicroblogPanel(base_widget.LiberviaWidget):
@@ -625,7 +578,7 @@
                         'author': self.host.whoami.bare,
                         }
                 entry = self.addEntry(data)
-                entry.setEntryDialog(edit=True)
+                entry.bubble.edit(True)
             self.new_button = Button("New message", listener=addBox)
             self.new_button.setStyleName("microblogNewButton")
             self.vpanel.insert(self.new_button, 0)
--- a/browser_side/richtext.py	Wed Feb 12 15:01:33 2014 +0100
+++ b/browser_side/richtext.py	Wed Feb 12 15:17:04 2014 +0100
@@ -26,188 +26,133 @@
 from pyjamas.ui.HorizontalPanel import HorizontalPanel
 
 from dialog import ConfirmDialog, InfoDialog
-from base_panels import TitlePanel
+from base_panels import TitlePanel, BaseTextEditor, LightTextEditor
 from list_manager import ListManager
+import panels
 
 from sat_frontends.tools import composition
-
-# used for onCloseCallback
-CANCEL, SYNC_NOT_SAVE, SAVE = xrange(0, 3)
+from sat.core.i18n import _
 
 
-class RichTextEditor(FlexTable):
+class RichTextEditor(BaseTextEditor, FlexTable):
     """Panel for the rich text editor."""
 
-    def __init__(self, host, parent, onCloseCallback=None, options=()):
-        """Fill the editor with recipients panel, toolbar, text area...
+    def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None):
+        """
         @param host: the SatWebFrontend instance
-        @param parent: the parent panel
-        @param onCloseCallback: method to call when the dialog is closed
-        @param options: list of options: 'no_recipient', 'no_command', 'no_style'...
+        @param content: dict with at least a 'text' key
+        @param modifiedCb: method to be called when the text has been modified
+        @param afterEditCb: method to be called when the edition is done
+        @param options: list of UI options (see self.readOptions)
         """
-
-        # 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
+        self._debug = False  # TODO: don't forget to set  it False before commit
+        self.__readOptions(options)
+        self.style = {'main': 'richTextEditor',
+                      'title': 'richTextTitle',
+                      'toolbar': 'richTextToolbaar',
+                      'textarea': 'richTextArea'}
+        if isinstance(style, dict):
+            self.style.update(style)
+        self._prepareUI()
+        BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb)
 
-        self.no_title = 'no_title' in options
-        self.no_title = True  # XXX: remove this line when titles are managed
-        self.no_close = 'no_close' in options
+    def __readOptions(self, options):
+        """Set the internal flags according to the given options."""
+        if options is None:
+            options = []
+        self.read_only = 'read_only' in options
         self.update_msg = 'update_msg' in options
-        self.no_recipient = 'no_recipient' in options
-        self.no_command = 'no_command' in options
-        self.no_sync_unibox = self.no_command or 'no_sync_unibox' in options
-        self.no_main_style = 'no_style' in options or 'no_main_style' in options
-        self.no_title_style = 'no_style' in options or 'no_title_style' in options
-        self.no_textarea_style = 'no_style' in options or 'no_textarea_style' in options
-
-        offset1 = 0 if self.no_recipient else (len(composition.RECIPIENT_TYPES) + 1)
-        offset2 = 0 if self.no_title else 1
-        offset3 = len(composition.RICH_FORMATS) if self._debug else 1
-        offset4 = 0 if self.no_command else 1
-        FlexTable.__init__(self, offset1 + offset2 + offset3 + 1 + offset4, 2)
-        if not self.no_main_style:
-            self.addStyleName('richTextEditor')
+        self.no_title = 'no_title' in options or self.read_only
+        self.no_command = 'no_command' in options or self.read_only
 
-        self._parent = parent
-        self._on_close_callback = onCloseCallback
-        self.original_text = ''
+    def _prepareUI(self, y_offset=0):
+        """Prepare the UI to host title panel, toolbar, text area...
+         @param y_offset: Y offset to start from (extra rows on top)"""
+        if not self.read_only:
+            self.title_offset = y_offset
+            self.toolbar_offset = self.title_offset + (0 if self.no_title else 1)
+            self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1)
+            self.command_offset = self.content_offset + 1
+        FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2)
+        self.addStyleName(self.style['main'])
+
+    def refresh(self, edit=None):
+        """Refresh the UI for edition/display mode
+        @param edit: set to True to display the edition mode"""
+        if edit is None:
+            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
 
-        current_offset = offset1
-        if not self.no_recipient:
-            # recipient types sub-panels are automatically added by the manager
-            self.recipient = RecipientManager(self)
-            self.recipient.createWidgets(title_format="%s: ")
-            self.getFlexCellFormatter().setColSpan(current_offset - 1, 0, 2)
-            spacer = HTML('')
-            spacer.setStyleName('recipientSpacer')
-            self.setWidget(current_offset - 1, 0, spacer)
+        for widget in ['title_panel', 'command']:
+            if hasattr(self, widget):
+                getattr(self, widget).setVisible(edit)
 
-        current_offset += offset2
-        if not self.no_title:
+        self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2)
+        if edit:
+            if not hasattr(self, 'textarea'):
+                self.textarea = TextArea()  # for edition mode
+                self.textarea.addStyleName(self.style['textarea'])
+            self.textarea.setWidth('100%')  # CSS width doesn't do it, don't know why
+            self.setWidget(self.content_offset, 0, self.textarea)
+        else:
+            if hasattr(self, 'toolbar'):
+                self.toolbar.setVisible(False)
+            if not hasattr(self, 'display'):
+                self.display = LightTextEditor()  # for display mode
+            self.setWidget(self.content_offset, 0, self.display)
+            return
+
+        if not self.no_title and not hasattr(self, 'title_panel'):
             self.title_panel = TitlePanel()
-            self.title_panel.addStyleName("richTextTitle")
-            self.getFlexCellFormatter().setColSpan(current_offset - 1, 0, 2)
-            self.setWidget(current_offset - 1, 0, self.title_panel)
-
-        # Rich text tool bar is automatically added by setVisible
-        self.offset_toolbar = offset1 + offset2
+            self.title_panel.addStyleName(self.style['title'])
+            self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2)
+            self.setWidget(self.title_offset, 0, self.title_panel)
 
-        current_offset += offset3 + 1
-        self.textarea = TextArea()
-        if not self.no_textarea_style:
-            self.textarea.addStyleName('richTextArea')
-        self.getFlexCellFormatter().setColSpan(current_offset - 1, 0, 2)
-        self.setWidget(current_offset - 1, 0, self.textarea)
-
-        current_offset += offset4
-        if not self.no_command:
+        if not self.no_command and not hasattr(self, 'command'):
             self.command = HorizontalPanel()
             self.command.addStyleName("marginAuto")
-            self.command.add(Button("Cancel", listener=self.cancelWithoutSaving))
-            if not self.no_sync_unibox:
-                self.command.add(Button("Back to quick box", listener=self.closeAndSave))
-            self.command.add(Button("Update" if self.update_msg else "Send message",
-                                    listener=self.__close if (self.update_msg or self.no_recipient) else self.sendMessage))
-            self.getFlexCellFormatter().setColSpan(current_offset - 1, 0, 2)
-            self.setWidget(current_offset - 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
-        @param parent: parent panel (or None to display in a popup).
-        @return: the RichTextEditor instance if parent is not None,
-        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)
+            self.command.add(Button("Cancel", lambda: self.edit(True, True)))
+            self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False)))
+            self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2)
+            self.setWidget(self.command_offset, 0, self.command)
 
-        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, visible):
-        """Called each time the widget is displayed, after creation or after having been hidden."""
-        FlexTable.setVisible(self, visible)
-        if visible:
-            self.host.bridge.call('asyncGetParamA', self.setToolBar, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) or self.setToolBar(None)
-
-    def __close(self, result=SAVE):
-        """Remove the widget from parent or close the popup."""
-        if not self.no_close:
-            if self._parent is None:
-                self.popup.hide()
-            else:
-                self.setVisible(False)
-        if self._on_close_callback is not None:
-            self._on_close_callback(result)
-
-    def setToolBar(self, _format):
+    def setToolBar(self, syntax):
         """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
+        holding the rich text syntax is retrieved. It is called at
+        each call of self.edit(True) because the user may
         have change his setting since the last time."""
-        if _format is None or _format not in composition.RICH_FORMATS.keys():
-            _format = composition.RICH_FORMATS.keys()[0]
-        if hasattr(self, "_format") and self._format == _format:
-            return
-        self._format = _format
+        if syntax is None or syntax not in composition.RICH_SYNTAXES.keys():
+            syntax = composition.RICH_SYNTAXES.keys()[0]
+        if hasattr(self, "toolbar") and self.toolbar.syntax == syntax:
+            self.toolbar.setVisible(True)
         count = 0
-        for _format in composition.RICH_FORMATS.keys() if self._debug else [self._format]:
-            toolbar = HorizontalPanel()
-            toolbar.addStyleName('richTextToolbar')
-            for key in composition.RICH_FORMATS[_format].keys():
-                self.addToolbarButton(toolbar, _format, key)
-            label = Label("Format: %s" % _format)
-            label.addStyleName("richTextFormatLabel")
-            toolbar.add(label)
-            self.getFlexCellFormatter().setColSpan(self.offset_toolbar + count, 0, 2)
-            self.setWidget(self.offset_toolbar + count, 0, toolbar)
+        for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]:
+            self.toolbar = HorizontalPanel()
+            self.toolbar.syntax = syntax
+            self.toolbar.addStyleName(self.style['toolbar'])
+            for key in composition.RICH_SYNTAXES[syntax].keys():
+                self.addToolbarButton(syntax, key)
+            label = Label(_("Syntax: %s") % syntax)
+            label.addStyleName("richTextSyntaxLabel")
+            self.toolbar.add(label)
+            self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2)
+            self.setWidget(self.toolbar_offset + count, 0, self.toolbar)
             count += 1
 
-    @property
-    def format(self):
-        """Get the current text format"""
-        return self._format if hasattr(self, '_format') else None
-
-    def addToolbarButton(self, toolbar, _format, key):
+    def addToolbarButton(self, syntax, key):
         """Add a button with the defined parameters."""
         button = Button('<img src="%s" class="richTextIcon" />' %
                         composition.RICH_BUTTONS[key]["icon"])
         button.setTitle(composition.RICH_BUTTONS[key]["tip"])
         button.addStyleName('richTextToolButton')
-        toolbar.add(button)
+        self.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 = composition.RICH_FORMATS[_format][key]
+            infos = composition.RICH_SYNTAXES[syntax][key]
             if selection_length == 0:
                 middle_text = infos[1]
             else:
@@ -222,23 +167,168 @@
 
         button.addClickListener(button_callback)
 
-    def syncFromUniBox(self):
+    def getContent(self):
+        assert(hasattr(self, 'textarea'))
+        assert(hasattr(self, 'toolbar'))
+        content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax}
+        if hasattr(self, 'title_panel'):
+            content.update({'title': self.strproc(self.title_panel.getText())})
+        return content
+
+    def edit(self, edit=False, abort=False, sync=False):
+        """
+        Remark: the editor must be visible before you call this method.
+        @param edit: set to True to edit the content or False to only display it
+        @param abort: set to True to cancel the edition and loose the changes.
+        If edit and abort are both True, self.abortEdition can be used to ask for a
+        confirmation. When edit is False and abort is True, abortion is actually done.
+        @param sync: set to True to cancel the edition after the content has been saved somewhere else
+        """
+        self.refresh(edit)
+        BaseTextEditor.edit(self, edit, abort, sync)
+        if (edit and abort) or sync:
+            return
+
+        # the following must NOT be done at each UI refresh!
+        content = self._original_content
+        if edit:
+            def getParamCb(syntax):
+                # set the editable text in the current user-selected syntax
+                def syntaxConvertCb(text=None):
+                    if text is not None:
+                        # Important: this also update self._original_content
+                        content.update({'text': text})
+                    content.update({'syntax': syntax})
+                    self.textarea.setText(content['text'])
+                    if hasattr(self, 'title_panel') and 'title' in content:
+                        self.title_panel.setText(content['title'])
+                        self.title_panel.setStackVisible(0, content['title'] != '')
+                if content['text'] and content['syntax'] != syntax:
+                    self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'])
+                else:
+                    syntaxConvertCb()
+                self.setToolBar(syntax)
+            self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) or self.setToolBar(None)
+        else:
+            if not self.initialized:
+                # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification
+                text = content['text']
+                if 'title' in content and content['title']:
+                    text = '<h1>%s</h1>%s' % (content['title'], content['text'])
+                self.display.setContent({'text': text})
+                self.display.edit(False)
+
+    def setFocus(self, focus):
+        self.textarea.setFocus(focus)
+
+    def abortEdition(self, content):
+        """Ask for confirmation before closing the dialog."""
+        def confirm_cb(answer):
+            if answer:
+                self.edit(False, True)
+        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message"))
+        _dialog.show()
+
+
+class RichMessageEditor(RichTextEditor):
+    """Use the rich text editor for sending messages with extended addressing.
+    Recipient panels are on top and data may be synchronized from/to the unibox."""
+
+    @classmethod
+    def getOrCreate(cls, host, parent=None, callback=None):
+        """Get or create the message editor associated to that host.
+        Add it to parent if parent is not None, otherwise display it
+        in a popup dialog.
+        @param host: the host
+        @param parent: parent panel (or None to display in a popup).
+        @return: the RichTextEditor instance if parent is not None,
+        otherwise a popup DialogBox containing the RichTextEditor.
+        """
+        if not hasattr(host, 'richtext'):
+            modifiedCb = lambda content: True
+
+            def afterEditCb(content):
+                if hasattr(host.richtext, 'popup'):
+                    host.richtext.popup.hide()
+                else:
+                    host.richtext.setVisible(False)
+                callback()
+                host.richtext.initialized = False
+            options = ['no_title']
+            style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'}
+            host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style)
+
+        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)
+            widget.edit(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)
+        return host.richtext.popup if parent is None else host.richtext
+
+    def _prepareUI(self, y_offset=0):
+        """Prepare the UI to host recipients panel, toolbar, text area...
+        @param y_offset: Y offset to start from (extra rows on top)"""
+        self.recipient_offset = y_offset
+        self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES)
+        RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1)
+
+    def refresh(self, edit=None):
+        """Refresh the UI between edition/display mode
+        @param edit: set to True to display the edition mode"""
+        if edit is None:
+            edit = hasattr(self, 'textarea') and self.textarea.getVisible()
+        RichTextEditor.refresh(self, edit)
+
+        for widget in ['recipient', 'recipient_spacer']:
+            if hasattr(self, widget):
+                getattr(self, widget).setVisible(edit)
+
+        if not edit:
+            return
+
+        if not hasattr(self, 'recipient'):
+            # recipient types sub-panels are automatically added by the manager
+            self.recipient = RecipientManager(self, self.recipient_offset)
+            self.recipient.createWidgets(title_format="%s: ")
+            self.recipient_spacer = HTML('')
+            self.recipient_spacer.setStyleName('recipientSpacer')
+            self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2)
+            self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer)
+
+        if not hasattr(self, 'sync_button'):
+            self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True))
+            self.command.insert(self.sync_button, 1)
+
+    def syncToEditor(self):
         """Synchronize from unibox."""
+        def setContent(target, data):
+            if hasattr(self, 'recipient'):
+                self.recipient.setContacts({"To": [target]} if target else {})
+            self.setContent({'text': data if data else '', 'syntax': ''})
+            self.textarea.setText(data if data else '')
+        data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None)
+        setContent(target, data)
+
+    def __syncToUniBox(self, recipients=None, emptyText=False):
+        """Synchronize to unibox if a maximum of one recipient is set.
+        @return True if the sync could be done, False otherwise"""
         if not self.host.uni_box:
             return
-        data, target = self.host.uni_box.getTargetAndData()
-        if hasattr(self, 'recipient'):
-            self.recipient.setContacts({"To": [target]} if target else {})
-        self.textarea.setText(data if data else "")
-
-    def syncToUniBox(self, recipients=None, emptyText=False):
-        """Synchronize to unibox if a maximum of one recipient is set.
-        @return True if the sync could be done, False otherwise"""
-        def setText():
-            self.host.uni_box.setText("" if emptyText else self.textarea.getText())
-
-        if not self.host.uni_box:
-            return
+        setText = lambda: self.host.uni_box.setText("" if emptyText else self.textarea.getText())
         if not hasattr(self, 'recipient'):
             setText()
             return True
@@ -257,35 +347,21 @@
             # TODO: change this if later more then one recipients are allowed
             target = recipients[key][0]
         setText()
-        from panels import ChatPanel, MicroblogPanel
         if target == "":
             return True
         if target.startswith("@"):
-            _class = MicroblogPanel
+            _class = panels.MicroblogPanel
             target = None if target == "@@" else target[1:]
         else:
-            _class = ChatPanel
+            _class = panels.ChatPanel
         self.host.getOrCreateLiberviaWidget(_class, target)
         return True
 
-    def cancelWithoutSaving(self):
-        """Ask for confirmation before closing the dialog."""
-        if self.textarea.getText() == self.original_text:
-            self.__close(CANCEL)
-            return
-
-        def confirm_cb(answer):
-            if answer:
-                self.__close(CANCEL)
-
-        _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message"))
-        _dialog.show()
-
-    def closeAndSave(self):
+    def syncFromEditor(self, content):
         """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(SYNC_NOT_SAVE)
+        if self.__syncToUniBox():
+            self._afterEditCb(content)
             return
         InfoDialog("Too many recipients",
                    "A message with more than one direct recipient (To)," +
@@ -294,10 +370,15 @@
                    " in the rich text editor, and send your message directly" +
                    " from here.", Width="400px").center()
 
-    def sendMessage(self):
+    def edit(self, edit=True, abort=False, sync=False):
+        if not edit and not abort and not sync:  # force sending message even when the text has not been modified
+            if not self.__sendMessage():  # message has not been sent (missing information), do nothing
+                return
+        RichTextEditor.edit(self, edit, abort, sync)
+
+    def __sendMessage(self):
         """Send the message."""
         recipients = self.recipient.getContacts()
-        text = self.textarea.getText()
         targets = []
         for addr in recipients:
             for recipient in recipients[addr]:
@@ -306,13 +387,17 @@
                 else:
                     targets.append(("chat", recipient, addr))
         # check that we actually have a message target and data
+        text = self.textarea.getText()
         if text == "" or len(targets) == 0:
             InfoDialog("Missing information",
                        "Some information are missing and the message hasn't been sent.", Width="400px").center()
-            return
-        self.syncToUniBox(recipients, emptyText=True)
-        self.host.send(targets, text, extra={'rich': text})
-        self.__close(SAVE)
+            return None
+        self.__syncToUniBox(recipients, emptyText=True)
+        extra = {'content_rich': text}
+        if hasattr(self, 'title_panel'):
+            extra.update({'title': self.title_panel.getText()})
+        self.host.send(targets, text, extra=extra)
+        return True
 
 
 class RecipientManager(ListManager):
--- a/libervia_server/__init__.py	Wed Feb 12 15:01:33 2014 +0100
+++ b/libervia_server/__init__.py	Wed Feb 12 15:17:04 2014 +0100
@@ -308,6 +308,7 @@
         d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
         return d
 
+
     def jsonrpc_getPresenceStatus(self):
         """Get Presence information for connected contacts"""
         profile = ISATSession(self.session).profile
--- a/public/libervia.css	Wed Feb 12 15:01:33 2014 +0100
+++ b/public/libervia.css	Wed Feb 12 15:17:04 2014 +0100
@@ -71,8 +71,9 @@
 }
 
 .bubble img {
-    max-width: 100%;
+    # if you set max-width, check that the rich text editor icons are still well displayed
 }
+
 /* styles for displaying rich text - END */
 
 blockquote, q { quotes: none; }
@@ -863,6 +864,7 @@
     border-color: #C1C1C1;
     border-width: 1px;
     border-style: solid;
+    display: block;
 }
 
 .bubble:after {
@@ -1250,27 +1252,45 @@
     text-decoration: underline
 }
 
-/* Rich Text Editor */
+/* Rich Text/Message Editor */
 
 .richTextEditor {
-	width: 100%;
-	margin: 9px 18px;
+}
+
+.richMessageEditor {
+    width: 100%;
+    margin: 9px 18px;
+}
+
+.richTextTitle {
+    margin-bottom: 5px;
+}
+
+.richTextTitle textarea {
+    height: 23px;
+    width: 99%;
+    margin: auto;
+    display: block;
 }
 
 .richTextToolbar {
 }
 
-.richTextFormatLabel {
+.richTextArea {
+    width: 100%;
+}
+
+.richMessageArea {
+    width: 100%;
+    height: 250px;
+}
+
+.richTextSyntaxLabel {
 	text-align: right;
 	margin: 14px 0px 0px 14px;
 	font-size: 12px;
 }
 
-.richTextArea {
-    width:100%;
-    height:250px;
-}
-
 .richTextToolButton {
 	cursor: pointer;
     width:26px;
@@ -1289,17 +1309,6 @@
     -o-transition: color 0.2s linear;
 }
 
-.richTextTitle {
-    margin-bottom: 5px;
-}
-
-.richTextTitle textarea {
-    height: 23px;
-    width: 99%;
-    margin: auto;
-    display: block;
-}
-
 .richTextIcon {
     width:16px;
     height:16px;