Mercurial > libervia-desktop-kivy
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