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