Mercurial > libervia-desktop-kivy
comparison src/cagou/plugins/plugin_wid_chat.py @ 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 |
comparison
equal
deleted
inserted
replaced
57:a51ea7874e43 | 58:7aa2ffff9067 |
---|---|
25 from kivy.uix.gridlayout import GridLayout | 25 from kivy.uix.gridlayout import GridLayout |
26 from kivy.uix.stacklayout import StackLayout | 26 from kivy.uix.stacklayout import StackLayout |
27 from kivy.uix.scrollview import ScrollView | 27 from kivy.uix.scrollview import ScrollView |
28 from kivy.uix.textinput import TextInput | 28 from kivy.uix.textinput import TextInput |
29 from kivy.uix.label import Label | 29 from kivy.uix.label import Label |
30 from kivy.uix.image import AsyncImage | |
30 from kivy.metrics import dp | 31 from kivy.metrics import dp |
31 from kivy.utils import escape_markup | 32 from kivy.utils import escape_markup |
32 from kivy import properties | 33 from kivy import properties |
33 from sat_frontends.quick_frontend import quick_widgets | 34 from sat_frontends.quick_frontend import quick_widgets |
34 from sat_frontends.quick_frontend import quick_chat | 35 from sat_frontends.quick_frontend import quick_chat |
57 | 58 |
58 def __init__(self, text): | 59 def __init__(self, text): |
59 super(Escape, self).__init__(text) | 60 super(Escape, self).__init__(text) |
60 | 61 |
61 | 62 |
63 class SimpleXHTMLWidgetEscapedText(Label): | |
64 pass | |
65 | |
66 class SimpleXHTMLWidgetText(Label): | |
67 pass | |
68 | |
69 class SimpleXHTMLWidgetImage(AsyncImage): | |
70 pass | |
71 | |
62 class SimpleXHTMLWidget(StackLayout): | 72 class SimpleXHTMLWidget(StackLayout): |
63 """widget handling simple XHTML parsing""" | 73 """widget handling simple XHTML parsing""" |
64 xhtml = properties.StringProperty() | 74 xhtml = properties.StringProperty() |
65 color = properties.ListProperty([1, 1, 1, 1]) | 75 color = properties.ListProperty([1, 1, 1, 1]) |
66 # XXX: bold is only used for escaped text | 76 # XXX: bold is only used for escaped text |
67 bold = properties.BooleanProperty(False) | 77 bold = properties.BooleanProperty(False) |
78 content_width = properties.NumericProperty(0) | |
79 | |
80 # text/XHTML input | |
68 | 81 |
69 def on_xhtml(self, instance, xhtml): | 82 def on_xhtml(self, instance, xhtml): |
70 """parse xhtml and set content accordingly | 83 """parse xhtml and set content accordingly |
71 | 84 |
72 if xhtml is an instance of Escape, a Label with not markup | 85 if xhtml is an instance of Escape, a Label with not markup |
73 will be used | 86 will be used |
74 """ | 87 """ |
75 self.clear_widgets() | 88 self.clear_widgets() |
76 if isinstance(xhtml, Escape): | 89 if isinstance(xhtml, Escape): |
77 label = Label(text=xhtml, color=self.color) | 90 label = SimpleXHTMLWidgetEscapedText(text=xhtml, color=self.color) |
78 self.bind(color=label.setter('color')) | 91 self.bind(color=label.setter('color')) |
79 self.bind(bold=label.setter('bold')) | 92 self.bind(bold=label.setter('bold')) |
80 self.add_widget(label) | 93 self.add_widget(label) |
81 else: | 94 else: |
82 xhtml = ET.fromstring(xhtml.encode('utf-8')) | 95 xhtml = ET.fromstring(xhtml.encode('utf-8')) |
86 | 99 |
87 def escape(self, text): | 100 def escape(self, text): |
88 """mark that a text need to be escaped (i.e. no markup)""" | 101 """mark that a text need to be escaped (i.e. no markup)""" |
89 return Escape(text) | 102 return Escape(text) |
90 | 103 |
104 # sizing | |
105 | |
106 def on_width(self, instance, width): | |
107 if len(self.children) == 1: | |
108 wid = self.children[0] | |
109 if isinstance(wid, Label): | |
110 try: | |
111 full_width = wid._full_width | |
112 except AttributeError: | |
113 wid.size_hint = (None, None) | |
114 wid.texture_update() | |
115 full_width = wid._full_width = wid.texture_size[0] | |
116 if full_width > width: | |
117 wid.text_size = width, None | |
118 else: | |
119 wid.text_size = None, None | |
120 self.content_width = wid.width + self.padding[0] + self.padding[2] | |
121 else: | |
122 wid.size_hint(1, None) | |
123 wid.height = 100 | |
124 self.content_width = self.width | |
125 else: | |
126 self._do_complexe_sizing(width) | |
127 | |
128 def _do_complexe_sizing(self, width): | |
129 try: | |
130 self.splitted | |
131 except AttributeError: | |
132 # XXX: to make things easier, we split labels in words | |
133 log.debug(u"split start") | |
134 children = self.children[::-1] | |
135 self.clear_widgets() | |
136 for child in children: | |
137 if isinstance(child, Label): | |
138 log.debug(u"label before split: {}".format(child.text)) | |
139 styles = [] | |
140 tag = False | |
141 new_text = [] | |
142 current_tag = [] | |
143 current_value = [] | |
144 current_wid = self._createText() | |
145 value = False | |
146 close = False | |
147 # we will parse the text and create a new widget | |
148 # on each new word (actually each space) | |
149 # FIXME: handle '\n' and other white chars | |
150 for c in child.text: | |
151 if tag: | |
152 # we are parsing a markup tag | |
153 if c == u']': | |
154 current_tag_s = u''.join(current_tag) | |
155 current_style = (current_tag_s, u''.join(current_value)) | |
156 if close: | |
157 for idx, s in enumerate(reversed(styles)): | |
158 if s[0] == current_tag_s: | |
159 del styles[len(styles) - idx - 1] | |
160 break | |
161 else: | |
162 styles.append(current_style) | |
163 current_tag = [] | |
164 current_value = [] | |
165 tag = False | |
166 value = False | |
167 close = False | |
168 elif c == u'/': | |
169 close = True | |
170 elif c == u'=': | |
171 value = True | |
172 elif value: | |
173 current_value.append(c) | |
174 else: | |
175 current_tag.append(c) | |
176 new_text.append(c) | |
177 else: | |
178 # we are parsing regular text | |
179 if c == u'[': | |
180 new_text.append(c) | |
181 tag = True | |
182 elif c == u' ': | |
183 # new word, we do a new widget | |
184 new_text.append(u' ') | |
185 for t, v in reversed(styles): | |
186 new_text.append(u'[/{}]'.format(t)) | |
187 current_wid.text = u''.join(new_text) | |
188 new_text = [] | |
189 self.add_widget(current_wid) | |
190 log.debug(u"new widget: {}".format(current_wid.text)) | |
191 current_wid = self._createText() | |
192 for t, v in styles: | |
193 new_text.append(u'[{tag}{value}]'.format( | |
194 tag = t, | |
195 value = u'={}'.format(v) if v else u'')) | |
196 else: | |
197 new_text.append(c) | |
198 if current_wid.text: | |
199 # we may have a remaining widget after the parsing | |
200 close_styles = [] | |
201 for t, v in reversed(styles): | |
202 close_styles.append(u'[/{}]'.format(t)) | |
203 current_wid.text = u''.join(close_styles) | |
204 self.add_widget(current_wid) | |
205 log.debug(u"new widget: {}".format(current_wid.text)) | |
206 else: | |
207 # non Label widgets, we just add them | |
208 self.add_widget(child) | |
209 self.splitted = True | |
210 log.debug(u"split OK") | |
211 | |
212 # we now set the content width | |
213 # FIXME: for now we just use the full width | |
214 self.content_width = width | |
215 | |
91 # XHTML parsing methods | 216 # XHTML parsing methods |
92 | 217 |
93 def _callParseMethod(self, e): | 218 def _callParseMethod(self, e): |
94 """call the suitable method to parse the element | 219 """call the suitable method to parse the element |
95 | 220 |
111 @param value(unicode): markup value if suitable | 236 @param value(unicode): markup value if suitable |
112 @param append_to_list(bool): if True style we be added to self.styles | 237 @param append_to_list(bool): if True style we be added to self.styles |
113 self.styles is needed to keep track of styles to remove | 238 self.styles is needed to keep track of styles to remove |
114 should most probably be set to True | 239 should most probably be set to True |
115 """ | 240 """ |
116 if append_to_list: | |
117 self.styles.append((tag, value)) | |
118 label = self._getLabel() | 241 label = self._getLabel() |
119 label.text += u'[{tag}{value}]'.format( | 242 label.text += u'[{tag}{value}]'.format( |
120 tag = tag, | 243 tag = tag, |
121 value = u'={}'.format(value) if value else '' | 244 value = u'={}'.format(value) if value else '' |
122 ) | 245 ) |
246 if append_to_list: | |
247 self.styles.append((tag, value)) | |
123 | 248 |
124 def _removeStyle(self, tag, remove_from_list=True): | 249 def _removeStyle(self, tag, remove_from_list=True): |
125 """remove a markup style from the label | 250 """remove a markup style from the label |
126 | 251 |
127 @param tag(unicode): markup tag to remove | 252 @param tag(unicode): markup tag to remove |
149 """add a new Label | 274 """add a new Label |
150 | 275 |
151 current styles will be closed and reopened if needed | 276 current styles will be closed and reopened if needed |
152 """ | 277 """ |
153 self._closeLabel() | 278 self._closeLabel() |
154 label = Label(color=self.color, markup=True) | 279 self.current_wid = self._createText() |
155 self.current_wid = label | |
156 self.bind(color=self.current_wid.setter('color')) | |
157 label.bind(texture_size=label.setter('size')) | |
158 for tag, value in self.styles: | 280 for tag, value in self.styles: |
159 self._addStyle(tag, value, append_to_list=False) | 281 self._addStyle(tag, value, append_to_list=False) |
160 self.add_widget(self.current_wid) | 282 self.add_widget(self.current_wid) |
283 | |
284 def _createText(self): | |
285 label = SimpleXHTMLWidgetText(color=self.color, markup=True) | |
286 self.bind(color=label.setter('color')) | |
287 label.bind(texture_size=label.setter('size')) | |
288 return label | |
161 | 289 |
162 def _closeLabel(self): | 290 def _closeLabel(self): |
163 """close current style tags in current label | 291 """close current style tags in current label |
164 | 292 |
165 needed when you change label to keep style between | 293 needed when you change label to keep style between |
248 self.xhtml_generic(style=False) | 376 self.xhtml_generic(style=False) |
249 | 377 |
250 def xhtml_em(self, elem): | 378 def xhtml_em(self, elem): |
251 self.xhtml_generic(elem, markup='i') | 379 self.xhtml_generic(elem, markup='i') |
252 | 380 |
381 def xhtml_img(self, elem): | |
382 try: | |
383 src = elem.attrib['src'] | |
384 except KeyError: | |
385 log.warning(u"<img> element without src: {}".format(ET.tostring(elem))) | |
386 return | |
387 img = SimpleXHTMLWidgetImage(source=src) | |
388 self.current_wid = img | |
389 self.add_widget(img) | |
390 | |
253 def xhtml_p(self, elem): | 391 def xhtml_p(self, elem): |
254 self._addLabel() | 392 if isinstance(self.current_wid, Label): |
393 self.current_wid.text+="\n\n" | |
255 self.xhtml_generic(elem) | 394 self.xhtml_generic(elem) |
256 | 395 |
257 def xhtml_span(self, elem): | 396 def xhtml_span(self, elem): |
258 self.xhtml_generic(elem) | 397 self.xhtml_generic(elem) |
259 | 398 |