Mercurial > libervia-desktop-kivy
comparison src/cagou/plugins/plugin_wid_chat.py @ 57:a51ea7874e43
chat: XHTML parsing first draft:
a XHTML mini parser is used so rich text can be handled. For now it manages only a few basic elements (bold, italic, color).
Body sizing/alignment is currenly broken.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 25 Sep 2016 16:06:56 +0200 |
parents | 514c187afebc |
children | 7aa2ffff9067 |
comparison
equal
deleted
inserted
replaced
56:817a45e6d7e3 | 57:a51ea7874e43 |
---|---|
21 from sat.core import log as logging | 21 from sat.core import log as logging |
22 log = logging.getLogger(__name__) | 22 log = logging.getLogger(__name__) |
23 from sat.core.i18n import _ | 23 from sat.core.i18n import _ |
24 from cagou.core.constants import Const as C | 24 from cagou.core.constants import Const as C |
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.scrollview import ScrollView | 27 from kivy.uix.scrollview import ScrollView |
27 from kivy.uix.textinput import TextInput | 28 from kivy.uix.textinput import TextInput |
29 from kivy.uix.label import Label | |
28 from kivy.metrics import dp | 30 from kivy.metrics import dp |
31 from kivy.utils import escape_markup | |
29 from kivy import properties | 32 from kivy import properties |
30 from sat_frontends.quick_frontend import quick_widgets | 33 from sat_frontends.quick_frontend import quick_widgets |
31 from sat_frontends.quick_frontend import quick_chat | 34 from sat_frontends.quick_frontend import quick_chat |
32 from sat_frontends.tools import jid | 35 from sat_frontends.tools import jid, css_color |
33 from cagou.core import cagou_widget | 36 from cagou.core import cagou_widget |
34 from cagou.core.image import Image | 37 from cagou.core.image import Image |
35 from cagou import G | 38 from cagou import G |
39 from xml.etree import ElementTree as ET | |
36 | 40 |
37 | 41 |
38 PLUGIN_INFO = { | 42 PLUGIN_INFO = { |
39 "name": _(u"chat"), | 43 "name": _(u"chat"), |
40 "main": "Chat", | 44 "main": "Chat", |
46 | 50 |
47 class MessAvatar(Image): | 51 class MessAvatar(Image): |
48 pass | 52 pass |
49 | 53 |
50 | 54 |
55 class Escape(unicode): | |
56 """Class used to mark that a message need to be escaped""" | |
57 | |
58 def __init__(self, text): | |
59 super(Escape, self).__init__(text) | |
60 | |
61 | |
62 class SimpleXHTMLWidget(StackLayout): | |
63 """widget handling simple XHTML parsing""" | |
64 xhtml = properties.StringProperty() | |
65 color = properties.ListProperty([1, 1, 1, 1]) | |
66 # XXX: bold is only used for escaped text | |
67 bold = properties.BooleanProperty(False) | |
68 | |
69 def on_xhtml(self, instance, xhtml): | |
70 """parse xhtml and set content accordingly | |
71 | |
72 if xhtml is an instance of Escape, a Label with not markup | |
73 will be used | |
74 """ | |
75 self.clear_widgets() | |
76 if isinstance(xhtml, Escape): | |
77 label = Label(text=xhtml, color=self.color) | |
78 self.bind(color=label.setter('color')) | |
79 self.bind(bold=label.setter('bold')) | |
80 self.add_widget(label) | |
81 else: | |
82 xhtml = ET.fromstring(xhtml.encode('utf-8')) | |
83 self.current_wid = None | |
84 self.styles = [] | |
85 self._callParseMethod(xhtml) | |
86 | |
87 def escape(self, text): | |
88 """mark that a text need to be escaped (i.e. no markup)""" | |
89 return Escape(text) | |
90 | |
91 # XHTML parsing methods | |
92 | |
93 def _callParseMethod(self, e): | |
94 """call the suitable method to parse the element | |
95 | |
96 self.xhtml_[tag] will be called if it exists, else | |
97 self.xhtml_generic will be used | |
98 @param e(ET.Element): element to parse | |
99 """ | |
100 try: | |
101 method = getattr(self, "xhtml_{}".format(e.tag)) | |
102 except AttributeError: | |
103 log.warning(u"Unhandled XHTML tag: {}".format(e.tag)) | |
104 method = self.xhtml_generic | |
105 method(e) | |
106 | |
107 def _addStyle(self, tag, value=None, append_to_list=True): | |
108 """add a markup style to label | |
109 | |
110 @param tag(unicode): markup tag | |
111 @param value(unicode): markup value if suitable | |
112 @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 | |
114 should most probably be set to True | |
115 """ | |
116 if append_to_list: | |
117 self.styles.append((tag, value)) | |
118 label = self._getLabel() | |
119 label.text += u'[{tag}{value}]'.format( | |
120 tag = tag, | |
121 value = u'={}'.format(value) if value else '' | |
122 ) | |
123 | |
124 def _removeStyle(self, tag, remove_from_list=True): | |
125 """remove a markup style from the label | |
126 | |
127 @param tag(unicode): markup tag to remove | |
128 @param remove_from_list(bool): if True, remove from self.styles too | |
129 should most probably be set to True | |
130 """ | |
131 label = self._getLabel() | |
132 label.text += u'[/{tag}]'.format( | |
133 tag = tag | |
134 ) | |
135 if remove_from_list: | |
136 for rev_idx, style in enumerate(reversed(self.styles)): | |
137 if style[0] == tag: | |
138 tag_idx = len(self.styles) - 1 - rev_idx | |
139 del self.styles[tag_idx] | |
140 break | |
141 | |
142 def _getLabel(self): | |
143 """get current Label if it exists, or create a new one""" | |
144 if not isinstance(self.current_wid, Label): | |
145 self._addLabel() | |
146 return self.current_wid | |
147 | |
148 def _addLabel(self): | |
149 """add a new Label | |
150 | |
151 current styles will be closed and reopened if needed | |
152 """ | |
153 self._closeLabel() | |
154 label = Label(color=self.color, markup=True) | |
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: | |
159 self._addStyle(tag, value, append_to_list=False) | |
160 self.add_widget(self.current_wid) | |
161 | |
162 def _closeLabel(self): | |
163 """close current style tags in current label | |
164 | |
165 needed when you change label to keep style between | |
166 different widgets | |
167 """ | |
168 if isinstance(self.current_wid, Label): | |
169 for tag, value in reversed(self.styles): | |
170 self._removeStyle(tag, remove_from_list=False) | |
171 | |
172 def _parseCSS(self, e): | |
173 """parse CSS found in "style" attribute of element | |
174 | |
175 self._css_styles will be created and contained markup styles added by this method | |
176 @param e(ET.Element): element which may have a "style" attribute | |
177 """ | |
178 styles_limit = len(self.styles) | |
179 styles = e.attrib['style'].split(u';') | |
180 for style in styles: | |
181 try: | |
182 prop, value = style.split(u':') | |
183 except ValueError: | |
184 log.warning(u"can't parse style: {}".format(style)) | |
185 continue | |
186 prop = prop.strip().replace(u'-', '_') | |
187 value = value.strip() | |
188 try: | |
189 method = getattr(self, "css_{}".format(prop)) | |
190 except AttributeError: | |
191 log.warning(u"Unhandled CSS: {}".format(prop)) | |
192 else: | |
193 method(e, value) | |
194 self._css_styles = self.styles[styles_limit:] | |
195 | |
196 def _closeCSS(self): | |
197 """removed CSS styles | |
198 | |
199 styles in self._css_styles will be removed | |
200 and the attribute will be deleted | |
201 """ | |
202 for tag, dummy in reversed(self._css_styles): | |
203 self._removeStyle(tag) | |
204 del self._css_styles | |
205 | |
206 def xhtml_generic(self, elem, style=True, markup=None): | |
207 """generic method for adding HTML elements | |
208 | |
209 this method handle content, style and children parsing | |
210 @param elem(ET.Element): element to add | |
211 @param style(bool): if True handle style attribute (CSS) | |
212 @param markup(tuple[unicode, (unicode, None)], None): kivy markup to use | |
213 """ | |
214 # we first add markup and CSS style | |
215 if markup is not None: | |
216 if isinstance(markup, basestring): | |
217 tag, value = markup, None | |
218 else: | |
219 tag, value = markup | |
220 self._addStyle(tag, value) | |
221 style_ = 'style' in elem.attrib and style | |
222 if style_: | |
223 self._parseCSS(elem) | |
224 | |
225 # then content | |
226 if elem.text: | |
227 self._getLabel().text += escape_markup(elem.text) | |
228 | |
229 # we parse the children | |
230 for child in elem: | |
231 self._callParseMethod(child) | |
232 | |
233 # closing CSS style and markup | |
234 if style_: | |
235 self._closeCSS() | |
236 if markup is not None: | |
237 self._removeStyle(tag) | |
238 | |
239 # and the tail, which is regular text | |
240 if elem.tail: | |
241 self._getLabel().text += escape_markup(elem.tail) | |
242 | |
243 # method handling XHTML elements | |
244 | |
245 def xhtml_br(self, elem): | |
246 label = self._getLabel() | |
247 label.text+='\n' | |
248 self.xhtml_generic(style=False) | |
249 | |
250 def xhtml_em(self, elem): | |
251 self.xhtml_generic(elem, markup='i') | |
252 | |
253 def xhtml_p(self, elem): | |
254 self._addLabel() | |
255 self.xhtml_generic(elem) | |
256 | |
257 def xhtml_span(self, elem): | |
258 self.xhtml_generic(elem) | |
259 | |
260 def xhtml_strong(self, elem): | |
261 self.xhtml_generic(elem, markup='b') | |
262 | |
263 # methods handling CSS properties | |
264 | |
265 def css_color(self, elem, value): | |
266 self._addStyle(u"color", css_color.parse(value)) | |
267 | |
268 def css_text_decoration(self, elem, value): | |
269 if value == u'underline': | |
270 log.warning(u"{} not handled yet, it needs Kivy 1.9.2 to be released".format(value)) | |
271 # FIXME: activate when 1.9.2 is out | |
272 # self._addStyle('u') | |
273 elif value == u'line-through': | |
274 log.warning(u"{} not handled yet, it needs Kivy 1.9.2 to be released".format(value)) | |
275 # FIXME: activate when 1.9.2 is out | |
276 # self._addStyle('s') | |
277 else: | |
278 log.warning(u"unhandled text decoration: {}".format(value)) | |
279 | |
280 | |
51 class MessageWidget(GridLayout): | 281 class MessageWidget(GridLayout): |
52 mess_data = properties.ObjectProperty() | 282 mess_data = properties.ObjectProperty() |
53 mess_label = properties.ObjectProperty() | 283 mess_xhtml = properties.ObjectProperty() |
54 mess_padding = (dp(5), dp(5)) | 284 mess_padding = (dp(5), dp(5)) |
55 avatar = properties.ObjectProperty() | 285 avatar = properties.ObjectProperty() |
56 | 286 |
57 def __init__(self, **kwargs): | 287 def __init__(self, **kwargs): |
58 super(MessageWidget, self).__init__(**kwargs) | 288 super(MessageWidget, self).__init__(**kwargs) |
66 @property | 296 @property |
67 def message(self): | 297 def message(self): |
68 """Return currently displayed message""" | 298 """Return currently displayed message""" |
69 return self.mess_data.main_message | 299 return self.mess_data.main_message |
70 | 300 |
301 @property | |
302 def message_xhtml(self): | |
303 """Return currently displayed message""" | |
304 return self.mess_data.main_message_xhtml | |
305 | |
71 def widthAdjust(self): | 306 def widthAdjust(self): |
72 """this widget grows up with its children""" | 307 """this widget grows up with its children""" |
73 parent = self.mess_label.parent | 308 pass |
74 padding_x = self.mess_padding[0] | 309 # parent = self.mess_xhtml.parent |
75 text_width, text_height = self.mess_label.texture_size | 310 # padding_x = self.mess_padding[0] |
76 if text_width > parent.width: | 311 # text_width, text_height = self.mess_xhtml.texture_size |
77 self.mess_label.text_size = (parent.width - padding_x, None) | 312 # if text_width > parent.width: |
78 self.text_max = text_width | 313 # self.mess_xhtml.text_size = (parent.width - padding_x, None) |
79 elif self.mess_label.text_size[0] is not None and text_width < parent.width - padding_x: | 314 # self.text_max = text_width |
80 if text_width < self.text_max: | 315 # elif self.mess_xhtml.text_size[0] is not None and text_width < parent.width - padding_x: |
81 self.mess_label.text_size = (None, None) | 316 # if text_width < self.text_max: |
82 else: | 317 # self.mess_xhtml.text_size = (None, None) |
83 self.mess_label.text_size = (parent.width - padding_x, None) | 318 # else: |
319 # self.mess_xhtml.text_size = (parent.width - padding_x, None) | |
84 | 320 |
85 def update(self, update_dict): | 321 def update(self, update_dict): |
86 if 'avatar' in update_dict: | 322 if 'avatar' in update_dict: |
87 self.avatar.source = update_dict['avatar'] | 323 self.avatar.source = update_dict['avatar'] |
88 | 324 |