# HG changeset patch # User souliane # Date 1392213491 -3600 # Node ID f488692c49030b4c26c15140ee5c3aedb272a8b7 # Parent 83454ba70a9c043fc5544fbac15f4db2faae707a browser_side: LightTextEditor inheritates from BaseTextEditor + display URL in the status diff -r 83454ba70a9c -r f488692c4903 browser_side/base_panels.py --- 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 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('
%s
' % 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) diff -r 83454ba70a9c -r f488692c4903 browser_side/panels.py --- 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 = '' + EMPTY_STATUS = '<click to set a status>' 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 == " " 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 diff -r 83454ba70a9c -r f488692c4903 browser_side/tools.py --- 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   sequences.""" + cleaned = re.sub(r"^(
| |\s)+", "", html) + cleaned = re.sub(r"(
| |\s)+$", "", cleaned) + return cleaned + + def inlineRoot(xhtml): """ make root element inline """ doc = dom.parseString(xhtml) diff -r 83454ba70a9c -r f488692c4903 libervia.py --- 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) diff -r 83454ba70a9c -r f488692c4903 libervia_server/__init__.py --- 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