view browser_side/richtext.py @ 335:e8c26e24a6c7

browser side: refactored XMLUI to use the new sat_frontends.tools.xmlui.XMLUI class, first draft
author Goffi <goffi@goffi.org>
date Tue, 04 Feb 2014 16:49:20 +0100
parents 6126bd24e7dd
children 2067d6241927
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 pyjamas.ui.TextArea import TextArea
from pyjamas.ui.Button import Button
from pyjamas.ui.DialogBox import DialogBox
from pyjamas.ui.Label import Label
from pyjamas.ui.HTML import HTML
from pyjamas.ui.FlexTable import FlexTable
from pyjamas.ui.HorizontalPanel import HorizontalPanel

from dialog import ConfirmDialog, InfoDialog
from base_panels import ToggleStackPanel
from list_manager import ListManager

from sat_frontends.tools import composition
from sat.core.i18n import _

# used for onCloseCallback
CANCEL, SYNC_NOT_SAVE, SAVE = xrange(0, 3)


class RichTextEditor(FlexTable):
    """Panel for the rich text editor."""

    def __init__(self, host, parent, onCloseCallback=None, options=()):
        """Fill the editor with recipients panel, toolbar, text area...
        @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'...
        """

        # 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.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
        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._parent = parent
        self._on_close_callback = onCloseCallback
        self.original_text = ''

        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)

        current_offset += offset2
        if not self.no_title:
            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

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

        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):
        """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 composition.RICH_FORMATS.keys():
            _format = composition.RICH_FORMATS.keys()[0]
        if hasattr(self, "_format") and self._format == _format:
            return
        self._format = _format
        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)
            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):
        """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)

        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]
            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."""
        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
        if not hasattr(self, 'recipient'):
            setText()
            return True
        if recipients is None:
            recipients = self.recipient.getContacts()
        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:
                return False
            # 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
            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."""
        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):
        """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)
            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.getContacts()
        text = self.textarea.getText()
        targets = []
        for addr in recipients:
            for recipient in recipients[addr]:
                if recipient.startswith("@"):
                    targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr))
                else:
                    targets.append(("chat", recipient, addr))
        # check that we actually have a message target and data
        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)


class RecipientManager(ListManager):
    """A manager for sub-panels to set the recipients for each recipient type."""

    def __init__(self, parent, y_offset=0):
        # TODO: be sure we also display empty groups and disconnected contacts + their groups
        # store the full list of potential recipients (groups and contacts)
        list_ = []
        list_.append("@@")
        list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups())
        list_.extend(contact for contact in parent.host.contact_panel.getContacts())
        ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset})

        self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES,
                                    hide=lambda sender, key: self.__children[key]["panel"].isVisible(),
                                    callback=self.setContactPanelVisible)


class TitlePanel(ToggleStackPanel):
    """A toggle panel to set the message title"""
    def __init__(self):
        ToggleStackPanel.__init__(self, Width="100%")
        self.text_area = TextArea()
        self.add(self.text_area, _("Title"))
        self.addStackChangeListener(self)

    def onStackChanged(self, sender, index, visible=None):
        if visible is None:
            visible = sender.getWidget(index).getVisible()
        text = self.text_area.getText()
        suffix = "" if (visible or not text) else (": %s" % text)
        sender.setStackText(index, _("Title") + suffix)