changeset 349:f488692c4903

browser_side: LightTextEditor inheritates from BaseTextEditor + display URL in the status
author souliane <souliane@mailoo.org>
date Wed, 12 Feb 2014 14:58:11 +0100
parents 83454ba70a9c
children f1b9ec412769
files browser_side/base_panels.py browser_side/panels.py browser_side/tools.py libervia.py libervia_server/__init__.py
diffstat 5 files changed, 217 insertions(+), 47 deletions(-) [+]
line wrap: on
line diff
--- a/browser_side/base_panels.py	Wed Feb 12 14:51:13 2014 +0100
+++ b/browser_side/base_panels.py	Wed Feb 12 14:58:11 2014 +0100
@@ -35,7 +35,7 @@
 from datetime import datetime
 from time import time
 
-from tools import html_sanitize, html_clean, inlineRoot
+from tools import html_sanitize, html_strip, inlineRoot
 
 from sat_frontends.tools.strings import addURLToText
 from sat.core.i18n import _
@@ -137,7 +137,7 @@
     certain items and also easily define their callbacks. The menu can be
     bound to any of the mouse button (left, middle, right).
     """
-    def __init__(self, entries, hide=None, callback=None, vertical=True, style={}, **kwargs):
+    def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
         """
         @param entries: a dict of dicts, where each sub-dict is representing
         one menu item: the sub-dict key can be used as the item text and
@@ -157,7 +157,8 @@
         self._callback = callback
         self.vertical = vertical
         self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
-        self.style.update(style)
+        if isinstance(style, dict):
+            self.style.update(style)
         self._senders = {}
 
     def _show(self, sender):
@@ -289,44 +290,176 @@
         self.text_area.setText(text)
 
 
-class LightTextEditor(HTML, FocusHandler, KeyboardHandler):
+class BaseTextEditor(object):
+    """Basic definition of a text editor. The method edit gets a boolean parameter which
+    should be set to True when you want to edit the text and False to only display it."""
+
+    def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
+        """
+        Remark when inheriting this class: since the setContent method could be
+        overwritten by the child class, you should consider calling this __init__
+        after all the parameters affecting this setContent method have been set.
+        @param content: dict with at least a 'text' key
+        @param strproc: method to be applied on strings to clean the content
+        @param modifiedCb: method to be called when the text has been modified.
+        If this method returns:
+        - True: the modification will be saved and afterEditCb called;
+        - False: the modification won't be saved and afterEditCb called;
+        - None: the modification won't be saved and afterEditCb not called.
+        @param afterEditCb: method to be called when the edition is done
+        """
+        if content is None:
+            content = {'text': ''}
+        assert('text' in content)
+        if strproc is None:
+            def strproc(text):
+                try:
+                    return text.strip()
+                except (TypeError, AttributeError):
+                    return text
+        self.strproc = strproc
+        self.__modifiedCb = modifiedCb
+        self._afterEditCb = afterEditCb
+        self.initialized = False
+        self.setContent(content)
+
+    def setContent(self, content=None):
+        """Set the editable content. The displayed content, which is set from the child class, could differ.
+        @param content: dict with at least a 'text' key
+        """
+        if content is None:
+            content = {'text': ''}
+        elif not isinstance(content, dict):
+            content = {'text': content}
+        assert('text' in content)
+        self._original_content = {}
+        for key in content:
+            self._original_content[key] = self.strproc(content[key])
+
+    def getContent(self):
+        """Get the current edited or editable content.
+        @return: dict with at least a 'text' key
+        """
+        raise NotImplementedError
+
+    def modified(self, content=None):
+        """Check if the content has been modified.
+        Remark: we don't use the direct comparison because we want to ignore empty elements
+        @content: content to be check against the original content or None to use the current content
+        @return: True if the content has been modified.
+        """
+        if content is None:
+            content = self.getContent()
+        # the following method returns True if one non empty element exists in a but not in b
+        diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
+        # the following method returns True if the values for the common keys are not equals
+        diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
+        # finally the combination of both to return True if a difference is found
+        diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
 
-    def __init__(self, content='', single_line=False, callback=None):
+        return diff(content, self._original_content)
+
+    def edit(self, edit, 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
+        """
+        if edit:
+            if not self.initialized:
+                self.syncToEditor()  # e.g.: use the selected target and unibox content
+            self.setFocus(True)
+            if abort:
+                content = self.getContent()
+                if not self.modified(content) or self.abortEdition(content):  # e.g: ask for confirmation
+                    self.edit(False, True, sync)
+                    return
+            if sync:
+                self.syncFromEditor(content)  # e.g.: save the content to unibox
+                return
+        else:
+            if not self.initialized:
+                return
+            content = self.getContent()
+            if abort:
+                self._afterEditCb(content)
+                return
+            if self.__modifiedCb and self.modified(content):
+                result = self.__modifiedCb(content)  # e.g.: send a message or update something
+                if result is not None:
+                    if self._afterEditCb:
+                        self._afterEditCb(content)  # e.g.: restore the display mode
+                    if result is True:
+                        self.setContent(content)
+            elif self._afterEditCb:
+                self._afterEditCb(content)
+
+        self.initialized = True
+
+    def setFocus(self, focus):
+        """
+        @param focus: set to True to focus the editor
+        """
+        raise NotImplementedError
+
+    def syncToEditor(self):
+        pass
+
+    def syncFromEditor(self, content):
+        pass
+
+    def abortEdition(self, content):
+        return True
+
+
+class LightTextEditor(BaseTextEditor, HTML, FocusHandler, KeyboardHandler):
+    """Manage a simple text editor whith the HTML 5 "contenteditable" property."""
+
+    def __init__(self, content=None, modifiedCb=None, afterEditCb=None, single_line=False):
+        """
+        @param content
+        @param modifiedCb
+        @param afterEditCb
+        @param single_line: set to True to manage a single line editor. In that case
+        the edition would be terminated when the focus is lost or <enter> key is pressed.
+        """
         HTML.__init__(self)
         if single_line:
             FocusHandler.__init__(self)
             KeyboardHandler.__init__(self)
         self.__single_line = single_line
-        self.__callback = callback
-        self.setContent(content)
+        strproc = lambda text: html_sanitize(html_strip(text)) if self.__single_line else html_strip(text)
+        BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
 
-    def setContent(self, content=''):
-        self.__original_content = html_clean(content) if self.__single_line else content.strip()
-        self.setHTML('<div>%s</div>' % self.__original_content)
+    def setContent(self, content=None):
+        BaseTextEditor.setContent(self, content)
 
     def getContent(self):
-        content = DOM.getInnerHTML(self.getElement().firstChild)
-        return html_clean(content) if self.__single_line else content.strip()
+        text = DOM.getInnerHTML(self.getElement())
+        return {'text': self.strproc(text) if text else ''}
 
-    def modified(self, content=None):
-        if content is None:
-            content = self.getContent()
-        return content != self.__original_content
-
-    def edit(self, edit):
+    def edit(self, edit, abort=False, sync=False):
+        if edit:
+            self.setHTML(self._original_content['text'])
         self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
+        BaseTextEditor.edit(self, edit)
         if edit:
-            self.setFocus(True)
             if self.__single_line:
                 self.addFocusListener(self)
                 self.addKeyboardListener(self)
         else:
+            self.setDisplayContent()
             if self.__single_line:
-                self.removeFocusListener(self)
-                self.removeKeyboardListener(self)
-            content = self.getContent()
-            if self.modified(content) and self.__callback:
-                self.__callback(content)
+                if self in self._focusListeners:
+                    self.removeFocusListener(self)
+                if self in self._keyboardListeners:
+                    self.removeKeyboardListener(self)
+
+    def setDisplayContent(self):
+        self.setHTML(addURLToText(self._original_content['text']))
 
     def setFocus(self, focus):
         if focus:
@@ -334,9 +467,10 @@
         else:
             self.getElement().blur()
 
-    def onKeyDown(self, sender, keycode, modifiers):
+    def onKeyPress(self, sender, keycode, modifiers):
         if keycode == KEY_ENTER:
-            self.setFocus(False)
+            self.setFocus(False)  # finish the edition
 
     def onLostFocus(self, sender):
+        """Finish the edition when focus is lost"""
         self.edit(False)
--- a/browser_side/panels.py	Wed Feb 12 14:51:13 2014 +0100
+++ b/browser_side/panels.py	Wed Feb 12 14:58:11 2014 +0100
@@ -40,7 +40,7 @@
 from __pyjamas__ import doc
 
 from tools import html_sanitize, setPresenceStyle
-from base_panels import ChatText, OccupantsList, PopupMenuPanel, LightTextEditor
+from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor
 from datetime import datetime
 from time import time
 from card_game import CardPanel
@@ -53,8 +53,9 @@
 
 from constants import Const
 from plugin_xep_0085 import ChatStateMachine
+from sat_frontends.tools.strings import addURLToText
 from sat_frontends.tools.games import SYMBOLS
-from sat_frontends.tools.strings import addURLToText
+from sat.core.i18n import _
 
 
 class UniBoxPanel(HorizontalPanel):
@@ -899,22 +900,46 @@
 
 class StatusPanel(LightTextEditor, ClickHandler):
 
-    EMPTY_STATUS = '<click to set a status>'
+    EMPTY_STATUS = '&lt;click to set a status&gt;'
 
     def __init__(self, host, status=''):
         self.host = host
-        self.status = status
-        LightTextEditor.__init__(self, self.__getStatus(), True, lambda status: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, status))
+        modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True
+        LightTextEditor.__init__(self, {'text': status}, modifiedCb, None, True)
+        self.edit(False)
         self.setStyleName('statusPanel')
         ClickHandler.__init__(self)
         self.addClickListener(self)
 
-    def __getStatus(self):
-        return html_sanitize(self.status or self.EMPTY_STATUS)
+    @property
+    def status(self):
+        return self._original_content['text']
+
+    def __cleanContent(self, content):
+        status = content['text']
+        if status == self.EMPTY_STATUS or status in Const.PRESENCE.values():
+            content['text'] = ''
+        return content
+
+    def getContent(self):
+        return self.__cleanContent(LightTextEditor.getContent(self))
 
-    def changeStatus(self, new_status):
-        self.status = new_status
-        self.setContent(self.__getStatus())
+    def setContent(self, content):
+        content = self.__cleanContent(content)
+        BaseTextEditor.setContent(self, content)
+
+    def setDisplayContent(self):
+        status = self._original_content['text']
+        try:
+            presence = self.host.status_panel.presence
+        except AttributeError:  # during initialization
+            presence = None
+        if not status:
+            if presence and presence in Const.PRESENCE:
+                status = Const.PRESENCE[presence]
+            else:
+                status = self.EMPTY_STATUS
+        self.setHTML(addURLToText(status))
 
     def onClick(self, sender):
         self.edit(True)
@@ -948,21 +973,26 @@
         panel.setStyleName("marginAuto")
         self.add(panel)
 
+        self.status_panel.edit(False)
+
         ClickHandler.__init__(self)
         self.addClickListener(self)
 
-    def getPresence(self):
-        return self.presence
+    @property
+    def presence(self):
+        return self._presence
+
+    @property
+    def status(self):
+        return self.status_panel._original_content['text']
 
     def setPresence(self, presence):
-        status = self.status_panel.status
-        if not status.strip() or status == "&nbsp;" or (self.presence in Const.PRESENCE and status == Const.PRESENCE[self.presence]):
-            self.changeStatus(Const.PRESENCE[presence])
-        self.presence = presence
-        setPresenceStyle(self.presence_button, self.presence)
+        self._presence = presence
+        setPresenceStyle(self.presence_button, self._presence)
 
-    def changeStatus(self, new_status):
-        self.status_panel.changeStatus(new_status)
+    def setStatus(self, status):
+        self.status_panel.setContent({'text': status})
+        self.status_panel.setDisplayContent()
 
     def onClick(self, sender):
         # As status is the default target of uniBar, we don't want to select anything if click on it
--- a/browser_side/tools.py	Wed Feb 12 14:51:13 2014 +0100
+++ b/browser_side/tools.py	Wed Feb 12 14:58:11 2014 +0100
@@ -56,6 +56,13 @@
     return cleaned.strip()
 
 
+def html_strip(html):
+    """Strip leading/trailing white spaces, HTML line breaks and &nbsp; sequences."""
+    cleaned = re.sub(r"^(<br/?>|&nbsp;|\s)+", "", html)
+    cleaned = re.sub(r"(<br/?>|&nbsp;|\s)+$", "", cleaned)
+    return cleaned
+
+
 def inlineRoot(xhtml):
     """ make root element inline """
     doc = dom.parseString(xhtml)
--- a/libervia.py	Wed Feb 12 14:51:13 2014 +0100
+++ b/libervia.py	Wed Feb 12 14:58:11 2014 +0100
@@ -664,7 +664,7 @@
         if self.whoami and self.whoami == entity_jid:  # XXX: QnD way to get our presence/status
             self.status_panel.setPresence(show)
             if statuses:
-                self.status_panel.changeStatus(statuses.values()[0])
+                self.status_panel.setStatus(statuses.values()[0])
         else:
             self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses)
 
--- a/libervia_server/__init__.py	Wed Feb 12 14:51:13 2014 +0100
+++ b/libervia_server/__init__.py	Wed Feb 12 14:58:11 2014 +0100
@@ -308,7 +308,6 @@
         d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
         return d
 
-
     def jsonrpc_getPresenceStatus(self):
         """Get Presence information for connected contacts"""
         profile = ISATSession(self.session).profile