diff cagou/core/simple_xhtml.py @ 325:5868a5575e01

chat: cleaning + some improvments: - code cleaning, removed some dead code - some improvments on the way size is calculated, removed unnecessary sizing methods which were linked to properties - image have now a max size, this avoid gigantic image in the whole screen - in SimpleXHTMLWidget, Label are now splitted when xhtml is set - use a DelayedBoxLayout for messages, as they are really slow to be resized - use of RecycleView has been investigated, but it is not currently usable as dynamic contents are not propertly handled (see https://github.com/kivy/kivy/issues/6580 and https://github.com/kivy/kivy/issues/6582). Furthermore, some tests with RecycleView on Android don't give the expected speed boost, so BoxLayout still seems like the way to go for the moment. To be re-investigated at a later point if necessary.
author Goffi <goffi@goffi.org>
date Fri, 06 Dec 2019 13:25:31 +0100
parents 772c170b47a9
children 597cc207c8e7
line wrap: on
line diff
--- a/cagou/core/simple_xhtml.py	Fri Dec 06 13:23:03 2019 +0100
+++ b/cagou/core/simple_xhtml.py	Fri Dec 06 13:25:31 2019 +0100
@@ -18,17 +18,20 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-from sat.core import log as logging
-log = logging.getLogger(__name__)
+import webbrowser
+from xml.etree import ElementTree as ET
 from kivy.uix.stacklayout import StackLayout
 from kivy.uix.label import Label
 from kivy.utils import escape_markup
-from kivy.metrics import sp
+from kivy.metrics import sp, dp
 from kivy import properties
-from xml.etree import ElementTree as ET
+from sat.core import log as logging
 from sat_frontends.tools import css_color, strings as sat_strings
 from cagou.core.image import AsyncImage
-import webbrowser
+from cagou.core.constants import Const as C
+
+
+log = logging.getLogger(__name__)
 
 
 class Escape(str):
@@ -51,10 +54,9 @@
                 text_elts.append(escape_markup(m.string[0:m.start()]))
                 link_key = 'link_' + str(links)
                 url = m.group()
-                text_elts.append('[color=5500ff][ref={link}]{url}[/ref][/color]'.format(
-                    link = link_key,
-                    url = url
-                    ))
+                escaped_url = escape_markup(url)
+                text_elts.append(
+                    f'[color=5500ff][ref={link_key}]{escaped_url}[/ref][/color]')
                 if not links:
                     self.ref_urls = {link_key: url}
                 else:
@@ -83,57 +85,55 @@
 class SimpleXHTMLWidgetText(Label):
 
     def on_parent(self, instance, parent):
-        self.font_size = parent.font_size
+        if parent is not None:
+            self.font_size = parent.font_size
 
 
 class SimpleXHTMLWidgetImage(AsyncImage):
     # following properties are desired height/width
     # i.e. the ones specified in height/width attributes of <img>
     # (or wanted for whatever reason)
-    # set to 0 to ignore them
-    target_height = properties.NumericProperty()
-    target_width = properties.NumericProperty()
-
-    def _get_parent_container(self):
-        """get parent SimpleXHTMLWidget instance
+    # set to None to ignore them
+    target_height = properties.NumericProperty(allownone=True)
+    target_width = properties.NumericProperty(allownone=True)
 
-        @param warning(bool): if True display a log.error if nothing found
-        @return (SimpleXHTMLWidget, None): found SimpleXHTMLWidget instance
-        """
-        parent = self.parent
-        while parent and not isinstance(parent, SimpleXHTMLWidget):
-            parent = parent.parent
-        if parent is None:
-            log.error("no SimpleXHTMLWidget parent found")
-        return parent
+    def __init__(self, **kwargs):
+        # best calculated size
+        self._best_width = self._best_height = 100
+        super().__init__(**kwargs)
 
-    def _on_source_load(self, value):
-        # this method is called when image is loaded
-        super(SimpleXHTMLWidgetImage, self)._on_source_load(value)
-        if self.parent is not None:
-            container = self._get_parent_container()
-            # image is loaded, we need to recalculate size
-            self.on_container_width(container, container.width)
-
-    def on_container_width(self, container, container_width):
-        """adapt size according to container width
+    def on_texture(self, instance, texture):
+        """Adapt the size according to max size and target_*"""
+        if texture is None:
+            return
+        max_width, max_height = dp(C.IMG_MAX_WIDTH), dp(C.IMG_MAX_HEIGHT)
+        width, height = texture.size
+        if self.target_width:
+            width = min(width, self.target_width)
+        if width > max_width:
+            width = C.IMG_MAX_WIDTH
 
-        called when parent container (SimpleXHTMLWidget) width change
-        """
-        target_size = (self.target_width or self.texture.width, self.target_height or self.texture.height)
-        padding = container.padding
-        padding_h = (padding[0] + padding[2]) if len(padding) == 4 else padding[0]
-        width = container_width - padding_h
-        if target_size[0] < width:
-            self.size = target_size
-        else:
-            height = width / self.image_ratio
-            self.size = (width, height)
+        height = width / self.image_ratio
+
+        if self.target_height:
+            height = min(height, self.target_height)
+
+        if height > max_height:
+            height = max_height
+            width = height * self.image_ratio
+
+        self.width, self.height = self._best_width, self._best_height = width, height
 
     def on_parent(self, instance, parent):
         if parent is not None:
-            container = self._get_parent_container()
-            container.bind(width=self.on_container_width)
+            parent.bind(width=self.on_parent_width)
+
+    def on_parent_width(self, instance, width):
+        if self._best_width > width:
+            self.width = width
+            self.height = width / self.image_ratio
+        else:
+            self.width, self.height = self._best_width, self._best_height
 
 
 class SimpleXHTMLWidget(StackLayout):
@@ -142,7 +142,6 @@
     color = properties.ListProperty([1, 1, 1, 1])
     # XXX: bold is only used for escaped text
     bold = properties.BooleanProperty(False)
-    content_width = properties.NumericProperty(0)
     font_size = properties.NumericProperty(sp(14))
 
     # text/XHTML input
@@ -156,151 +155,117 @@
         if isinstance(xhtml, Escape):
             label = SimpleXHTMLWidgetEscapedText(
                 text=xhtml, color=self.color, bold=self.bold)
+            self.bind(font_size=label.setter('font_size'))
             self.bind(color=label.setter('color'))
             self.bind(bold=label.setter('bold'))
             self.add_widget(label)
         else:
-            xhtml = ET.fromstring(xhtml.encode('utf-8'))
+            xhtml = ET.fromstring(xhtml.encode())
             self.current_wid = None
             self.styles = []
             self._callParseMethod(xhtml)
+        if len(self.children) > 1:
+            self._do_split_labels()
 
     def escape(self, text):
         """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):
-                # we have simple text
-                try:
-                    full_width = wid._full_width
-                except AttributeError:
-                    # on first time, we need the required size
-                    # for the full text, without width limit
-                    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
-                    wid.width = width
-                else:
-                    wid.text_size = None, None
-                    wid.texture_update()
-                    wid.width = wid.texture_size[0]
-                self.content_width = wid.width + self.padding[0] + self.padding[2]
+    def _do_split_labels(self):
+        """Split labels so their content can flow with images"""
+        # XXX: to make things easier, we split labels in words
+        log.debug("labels splitting start")
+        children = self.children[::-1]
+        self.clear_widgets()
+        for child in children:
+            if isinstance(child, Label):
+                log.debug("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 == ']':
+                            current_tag_s = ''.join(current_tag)
+                            current_style = (current_tag_s, ''.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 == '/':
+                            close = True
+                        elif c == '=':
+                            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 == '[':
+                            new_text.append(c)
+                            tag = True
+                        elif c == ' ':
+                            # new word, we do a new widget
+                            new_text.append(' ')
+                            for t, v in reversed(styles):
+                                new_text.append('[/{}]'.format(t))
+                            current_wid.text = ''.join(new_text)
+                            new_text = []
+                            self.add_widget(current_wid)
+                            log.debug("new widget: {}".format(current_wid.text))
+                            current_wid = self._createText()
+                            for t, v in styles:
+                                new_text.append('[{tag}{value}]'.format(
+                                    tag = t,
+                                    value = '={}'.format(v) if v else ''))
+                        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('[/{}]'.format(t))
+                    current_wid.text = ''.join(close_styles)
+                    self.add_widget(current_wid)
+                    log.debug("new widget: {}".format(current_wid.text))
             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("split start")
-            children = self.children[::-1]
-            self.clear_widgets()
-            for child in children:
-                if isinstance(child, Label):
-                    log.debug("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 == ']':
-                                current_tag_s = ''.join(current_tag)
-                                current_style = (current_tag_s, ''.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 == '/':
-                                close = True
-                            elif c == '=':
-                                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 == '[':
-                                new_text.append(c)
-                                tag = True
-                            elif c == ' ':
-                                # new word, we do a new widget
-                                new_text.append(' ')
-                                for t, v in reversed(styles):
-                                    new_text.append('[/{}]'.format(t))
-                                current_wid.text = ''.join(new_text)
-                                new_text = []
-                                self.add_widget(current_wid)
-                                log.debug("new widget: {}".format(current_wid.text))
-                                current_wid = self._createText()
-                                for t, v in styles:
-                                    new_text.append('[{tag}{value}]'.format(
-                                        tag = t,
-                                        value = '={}'.format(v) if v else ''))
-                            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('[/{}]'.format(t))
-                        current_wid.text = ''.join(close_styles)
-                        self.add_widget(current_wid)
-                        log.debug("new widget: {}".format(current_wid.text))
-                else:
-                    # non Label widgets, we just add them
-                    self.add_widget(child)
-            self.splitted = True
-            log.debug("split OK")
-
-        # we now set the content width
-        # FIXME: for now we just use the full width
-        self.content_width = width
+                # non Label widgets, we just add them
+                self.add_widget(child)
+        self.splitted = True
+        log.debug("split OK")
 
     # XHTML parsing methods
 
     def _callParseMethod(self, e):
-        """call the suitable method to parse the element
+        """Call the suitable method to parse the element
 
         self.xhtml_[tag] will be called if it exists, else
         self.xhtml_generic will be used
         @param e(ET.Element): element to parse
         """
         try:
-            method = getattr(self, "xhtml_{}".format(e.tag))
+            method = getattr(self, f"xhtml_{e.tag}")
         except AttributeError:
-            log.warning("Unhandled XHTML tag: {}".format(e.tag))
+            log.warning(f"Unhandled XHTML tag: {e.tag}")
             method = self.xhtml_generic
         method(e)
 
@@ -384,14 +349,14 @@
             try:
                 prop, value = style.split(':')
             except ValueError:
-                log.warning("can't parse style: {}".format(style))
+                log.warning(f"can't parse style: {style}")
                 continue
             prop = prop.strip().replace('-', '_')
             value = value.strip()
             try:
-                method = getattr(self, "css_{}".format(prop))
+                method = getattr(self, f"css_{prop}")
             except AttributeError:
-                log.warning("Unhandled CSS: {}".format(prop))
+                log.warning(f"Unhandled CSS: {prop}")
             else:
                 method(e, value)
         self._css_styles = self.styles[styles_limit:]
@@ -407,7 +372,7 @@
         del self._css_styles
 
     def xhtml_generic(self, elem, style=True, markup=None):
-        """generic method for adding HTML elements
+        """Generic method for adding HTML elements
 
         this method handle content, style and children parsing
         @param elem(ET.Element): element to add
@@ -462,13 +427,13 @@
         try:
             target_height = int(elem.get('height', 0))
         except ValueError:
-            log.warning("Can't parse image height: {}".format(elem.get('height')))
-            target_height = 0
+            log.warning(f"Can't parse image height: {elem.get('height')}")
+            target_height = None
         try:
             target_width = int(elem.get('width', 0))
         except ValueError:
-            log.warning("Can't parse image width: {}".format(elem.get('width')))
-            target_width = 0
+            log.warning(f"Can't parse image width: {elem.get('width')}")
+            target_width = None
 
         img = SimpleXHTMLWidgetImage(source=src, target_height=target_height, target_width=target_width)
         self.current_wid = img