# HG changeset patch # User souliane # Date 1392214624 -3600 # Node ID c943fd54c90ea950a2710732d62ac18eb269fffd # Parent f1b9ec412769b154a652a0da0b2e9831de29c35c 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 diff -r f1b9ec412769 -r c943fd54c90e browser_side/panels.py --- 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 ('') + self.button = Button('') 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.
") + 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) diff -r f1b9ec412769 -r c943fd54c90e browser_side/richtext.py --- 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('' % 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 = '

%s

%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): diff -r f1b9ec412769 -r c943fd54c90e libervia_server/__init__.py --- 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 diff -r f1b9ec412769 -r c943fd54c90e public/libervia.css --- 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;