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