changeset 58:7aa2ffff9067

chat: <img/> tag handling first draft: We need to have several widgets to handle <img/> (label(s) + image(s)), which make sizing and positioning complicated. To make things simpler, we use a simple trick when several widgets are present: we split the labels in as many labels as there are words, so we can take profit of the StackLayout. The split is done after the XHTML is parsed, so after all the widgets are present, and is done only once. This means that label need to be reparsed to be splitted. This is not perfect, but should be a reasonable solutions until we implement a real XHTML engine (probably CEF widget and Webview). image sizing and alignment is not handled correcly now, should be fixed soon.
author Goffi <goffi@goffi.org>
date Wed, 28 Sep 2016 22:02:36 +0200
parents a51ea7874e43
children 2aa44a82d0e7
files src/cagou/plugins/plugin_wid_chat.kv src/cagou/plugins/plugin_wid_chat.py
diffstat 2 files changed, 161 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/src/cagou/plugins/plugin_wid_chat.kv	Sun Sep 25 16:06:56 2016 +0200
+++ b/src/cagou/plugins/plugin_wid_chat.kv	Wed Sep 28 22:02:36 2016 +0200
@@ -15,6 +15,17 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
+<SimpleXHTMLWidgetEscapedText>:
+    size_hint: None, None
+    size: self.texture_size
+
+<SimpleXHTMLWidgetText>:
+    size_hint: None, None
+    size: self.texture_size
+
+<SimpleXHTMLWidgetImage>:
+    size_hint: 1, None
+
 <MessAvatar>:
     size_hint: None, None
     size: dp(30), dp(30)
@@ -61,14 +72,15 @@
                 BorderImage:
                     source: app.expand("{media}/misc/black.png") if root.mess_data.type == "info" else app.expand("{media}/misc/borders/{}.jpg", "blue" if root.mess_data.own_mess else "gray")
                     pos: self.pos
-                    size: self.size
+                    size: self.content_width, self.height
             id: mess_xhtml
+            size_hint: 0.8, None
+            height: self.minimum_height
             xhtml: root.message_xhtml or self.escape(root.message or u' ')
             color: (0.74,0.74,0.24,1) if root.mess_data.type == "info" else (0, 0, 0, 1)
             padding: root.mess_padding
             bold: True if root.mess_data.type == "info" else False
 
-
 <MessageInputWidget>:
     size_hint: 1, None
     height: dp(40)
--- a/src/cagou/plugins/plugin_wid_chat.py	Sun Sep 25 16:06:56 2016 +0200
+++ b/src/cagou/plugins/plugin_wid_chat.py	Wed Sep 28 22:02:36 2016 +0200
@@ -27,6 +27,7 @@
 from kivy.uix.scrollview import ScrollView
 from kivy.uix.textinput import TextInput
 from kivy.uix.label import Label
+from kivy.uix.image import AsyncImage
 from kivy.metrics import dp
 from kivy.utils import escape_markup
 from kivy import properties
@@ -59,12 +60,24 @@
         super(Escape, self).__init__(text)
 
 
+class SimpleXHTMLWidgetEscapedText(Label):
+    pass
+
+class SimpleXHTMLWidgetText(Label):
+    pass
+
+class SimpleXHTMLWidgetImage(AsyncImage):
+    pass
+
 class SimpleXHTMLWidget(StackLayout):
     """widget handling simple XHTML parsing"""
     xhtml = properties.StringProperty()
     color = properties.ListProperty([1, 1, 1, 1])
     # XXX: bold is only used for escaped text
     bold = properties.BooleanProperty(False)
+    content_width = properties.NumericProperty(0)
+
+    # text/XHTML input
 
     def on_xhtml(self, instance, xhtml):
         """parse xhtml and set content accordingly
@@ -74,7 +87,7 @@
         """
         self.clear_widgets()
         if isinstance(xhtml, Escape):
-            label = Label(text=xhtml, color=self.color)
+            label = SimpleXHTMLWidgetEscapedText(text=xhtml, color=self.color)
             self.bind(color=label.setter('color'))
             self.bind(bold=label.setter('bold'))
             self.add_widget(label)
@@ -88,6 +101,118 @@
         """mark that a text need to be escaped (i.e. no markup)"""
         return Escape(text)
 
+    # sizing
+
+    def on_width(self, instance, width):
+        if len(self.children) == 1:
+            wid = self.children[0]
+            if isinstance(wid, Label):
+                try:
+                    full_width = wid._full_width
+                except AttributeError:
+                    wid.size_hint = (None, None)
+                    wid.texture_update()
+                    full_width = wid._full_width = wid.texture_size[0]
+                if full_width > width:
+                    wid.text_size = width, None
+                else:
+                    wid.text_size = None, None
+                self.content_width = wid.width + self.padding[0] + self.padding[2]
+            else:
+                wid.size_hint(1, None)
+                wid.height = 100
+                self.content_width = self.width
+        else:
+            self._do_complexe_sizing(width)
+
+    def _do_complexe_sizing(self, width):
+        try:
+            self.splitted
+        except AttributeError:
+            # XXX: to make things easier, we split labels in words
+            log.debug(u"split start")
+            children = self.children[::-1]
+            self.clear_widgets()
+            for child in children:
+                if isinstance(child, Label):
+                    log.debug(u"label before split: {}".format(child.text))
+                    styles = []
+                    tag = False
+                    new_text = []
+                    current_tag = []
+                    current_value = []
+                    current_wid = self._createText()
+                    value = False
+                    close = False
+                    # we will parse the text and create a new widget
+                    # on each new word (actually each space)
+                    # FIXME: handle '\n' and other white chars
+                    for c in child.text:
+                        if tag:
+                            # we are parsing a markup tag
+                            if c == u']':
+                                current_tag_s = u''.join(current_tag)
+                                current_style = (current_tag_s, u''.join(current_value))
+                                if close:
+                                    for idx, s in enumerate(reversed(styles)):
+                                        if s[0] == current_tag_s:
+                                            del styles[len(styles) - idx - 1]
+                                            break
+                                else:
+                                    styles.append(current_style)
+                                current_tag = []
+                                current_value = []
+                                tag = False
+                                value = False
+                                close = False
+                            elif c == u'/':
+                                close = True
+                            elif c == u'=':
+                                value = True
+                            elif value:
+                                current_value.append(c)
+                            else:
+                                current_tag.append(c)
+                            new_text.append(c)
+                        else:
+                            # we are parsing regular text
+                            if c == u'[':
+                                new_text.append(c)
+                                tag = True
+                            elif c == u' ':
+                                # new word, we do a new widget
+                                new_text.append(u' ')
+                                for t, v in reversed(styles):
+                                    new_text.append(u'[/{}]'.format(t))
+                                current_wid.text = u''.join(new_text)
+                                new_text = []
+                                self.add_widget(current_wid)
+                                log.debug(u"new widget: {}".format(current_wid.text))
+                                current_wid = self._createText()
+                                for t, v in styles:
+                                    new_text.append(u'[{tag}{value}]'.format(
+                                        tag = t,
+                                        value = u'={}'.format(v) if v else u''))
+                            else:
+                                new_text.append(c)
+                    if current_wid.text:
+                        # we may have a remaining widget after the parsing
+                        close_styles = []
+                        for t, v in reversed(styles):
+                            close_styles.append(u'[/{}]'.format(t))
+                        current_wid.text = u''.join(close_styles)
+                        self.add_widget(current_wid)
+                        log.debug(u"new widget: {}".format(current_wid.text))
+                else:
+                    # non Label widgets, we just add them
+                    self.add_widget(child)
+            self.splitted = True
+            log.debug(u"split OK")
+
+        # we now set the content width
+        # FIXME: for now we just use the full width
+        self.content_width = width
+
     # XHTML parsing methods
 
     def _callParseMethod(self, e):
@@ -113,13 +238,13 @@
             self.styles is needed to keep track of styles to remove
             should most probably be set to True
         """
-        if append_to_list:
-            self.styles.append((tag, value))
         label = self._getLabel()
         label.text += u'[{tag}{value}]'.format(
             tag = tag,
             value = u'={}'.format(value) if value else ''
             )
+        if append_to_list:
+            self.styles.append((tag, value))
 
     def _removeStyle(self, tag, remove_from_list=True):
         """remove a markup style from the label
@@ -151,14 +276,17 @@
         current styles will be closed and reopened if needed
         """
         self._closeLabel()
-        label = Label(color=self.color, markup=True)
-        self.current_wid = label
-        self.bind(color=self.current_wid.setter('color'))
-        label.bind(texture_size=label.setter('size'))
+        self.current_wid = self._createText()
         for tag, value in self.styles:
             self._addStyle(tag, value, append_to_list=False)
         self.add_widget(self.current_wid)
 
+    def _createText(self):
+        label = SimpleXHTMLWidgetText(color=self.color, markup=True)
+        self.bind(color=label.setter('color'))
+        label.bind(texture_size=label.setter('size'))
+        return label
+
     def _closeLabel(self):
         """close current style tags in current label
 
@@ -250,8 +378,19 @@
     def xhtml_em(self, elem):
         self.xhtml_generic(elem, markup='i')
 
+    def xhtml_img(self, elem):
+        try:
+            src = elem.attrib['src']
+        except KeyError:
+            log.warning(u"<img> element without src: {}".format(ET.tostring(elem)))
+            return
+        img = SimpleXHTMLWidgetImage(source=src)
+        self.current_wid = img
+        self.add_widget(img)
+
     def xhtml_p(self, elem):
-        self._addLabel()
+        if isinstance(self.current_wid, Label):
+            self.current_wid.text+="\n\n"
         self.xhtml_generic(elem)
 
     def xhtml_span(self, elem):