Mercurial > libervia-web
comparison src/browser/sat_browser/editor_widget.py @ 648:6d3142b782c3 frontends_multi_profiles
browser_side: classes reorganisation:
- moved widgets in dedicated modules (base, contact, editor, libervia) and a widget module for single classes
- same thing for panels (base, main, contact)
- libervia_widget mix main panels and widget and drag n drop for technical reasons (see comments)
- renamed WebPanel to WebWidget
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 26 Feb 2015 18:10:54 +0100 |
parents | src/browser/sat_browser/base_panels.py@e0021d571eef |
children | e876f493dccc |
comparison
equal
deleted
inserted
replaced
647:e0021d571eef | 648:6d3142b782c3 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.log import getLogger | |
21 log = getLogger(__name__) | |
22 from sat_frontends.tools import strings | |
23 | |
24 from pyjamas.ui.HTML import HTML | |
25 from pyjamas.ui.SimplePanel import SimplePanel | |
26 from pyjamas.ui.TextArea import TextArea | |
27 from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler | |
28 from pyjamas.ui.FocusListener import FocusHandler | |
29 from pyjamas.ui.ClickListener import ClickHandler | |
30 from pyjamas.ui.MouseListener import MouseHandler | |
31 from pyjamas.Timer import Timer | |
32 from pyjamas import DOM | |
33 | |
34 import html_tools | |
35 | |
36 | |
37 class MessageBox(TextArea): | |
38 """A basic text area for entering messages""" | |
39 | |
40 def __init__(self, host): | |
41 TextArea.__init__(self) | |
42 self.host = host | |
43 self.size = (0, 0) | |
44 self.setStyleName('messageBox') | |
45 self.addKeyboardListener(self) | |
46 MouseHandler.__init__(self) | |
47 self.addMouseListener(self) | |
48 | |
49 def onBrowserEvent(self, event): | |
50 # XXX: woraroung a pyjamas bug: self.currentEvent is not set | |
51 # so the TextBox's cancelKey doens't work. This is a workaround | |
52 # FIXME: fix the bug upstream | |
53 self.currentEvent = event | |
54 TextArea.onBrowserEvent(self, event) | |
55 | |
56 def onKeyPress(self, sender, keycode, modifiers): | |
57 _txt = self.getText() | |
58 | |
59 def history_cb(text): | |
60 self.setText(text) | |
61 Timer(5, lambda timer: self.setCursorPos(len(text))) | |
62 | |
63 if keycode == KEY_ENTER: | |
64 if _txt: | |
65 self.host.selected_widget.onTextEntered(_txt) | |
66 self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? | |
67 self.setText('') | |
68 sender.cancelKey() | |
69 elif keycode == KEY_UP: | |
70 self.host._updateInputHistory(_txt, -1, history_cb) | |
71 elif keycode == KEY_DOWN: | |
72 self.host._updateInputHistory(_txt, +1, history_cb) | |
73 else: | |
74 self._onComposing() | |
75 | |
76 def _onComposing(self): | |
77 """Callback when the user is composing a text.""" | |
78 self.host.selected_widget.state_machine._onEvent("composing") | |
79 | |
80 def onMouseUp(self, sender, x, y): | |
81 size = (self.getOffsetWidth(), self.getOffsetHeight()) | |
82 if size != self.size: | |
83 self.size = size | |
84 self.host.resize() | |
85 | |
86 def onSelectedChange(self, selected): | |
87 self._selected_cache = selected | |
88 | |
89 | |
90 class BaseTextEditor(object): | |
91 """Basic definition of a text editor. The method edit gets a boolean parameter which | |
92 should be set to True when you want to edit the text and False to only display it.""" | |
93 | |
94 def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): | |
95 """ | |
96 Remark when inheriting this class: since the setContent method could be | |
97 overwritten by the child class, you should consider calling this __init__ | |
98 after all the parameters affecting this setContent method have been set. | |
99 @param content: dict with at least a 'text' key | |
100 @param strproc: method to be applied on strings to clean the content | |
101 @param modifiedCb: method to be called when the text has been modified. | |
102 If this method returns: | |
103 - True: the modification will be saved and afterEditCb called; | |
104 - False: the modification won't be saved and afterEditCb called; | |
105 - None: the modification won't be saved and afterEditCb not called. | |
106 @param afterEditCb: method to be called when the edition is done | |
107 """ | |
108 if content is None: | |
109 content = {'text': ''} | |
110 assert('text' in content) | |
111 if strproc is None: | |
112 def strproc(text): | |
113 try: | |
114 return text.strip() | |
115 except (TypeError, AttributeError): | |
116 return text | |
117 self.strproc = strproc | |
118 self.__modifiedCb = modifiedCb | |
119 self._afterEditCb = afterEditCb | |
120 self.initialized = False | |
121 self.edit_listeners = [] | |
122 self.setContent(content) | |
123 | |
124 def setContent(self, content=None): | |
125 """Set the editable content. The displayed content, which is set from the child class, could differ. | |
126 @param content: dict with at least a 'text' key | |
127 """ | |
128 if content is None: | |
129 content = {'text': ''} | |
130 elif not isinstance(content, dict): | |
131 content = {'text': content} | |
132 assert('text' in content) | |
133 self._original_content = {} | |
134 for key in content: | |
135 self._original_content[key] = self.strproc(content[key]) | |
136 | |
137 def getContent(self): | |
138 """Get the current edited or editable content. | |
139 @return: dict with at least a 'text' key | |
140 """ | |
141 raise NotImplementedError | |
142 | |
143 def setOriginalContent(self, content): | |
144 """Use this method with care! Content initialization should normally be | |
145 done with self.setContent. This method exists to let you trick the editor, | |
146 e.g. for self.modified to return True also when nothing has been modified. | |
147 @param content: dict | |
148 """ | |
149 self._original_content = content | |
150 | |
151 def getOriginalContent(self): | |
152 """ | |
153 @return the original content before modification (dict) | |
154 """ | |
155 return self._original_content | |
156 | |
157 def modified(self, content=None): | |
158 """Check if the content has been modified. | |
159 Remark: we don't use the direct comparison because we want to ignore empty elements | |
160 @content: content to be check against the original content or None to use the current content | |
161 @return: True if the content has been modified. | |
162 """ | |
163 if content is None: | |
164 content = self.getContent() | |
165 # the following method returns True if one non empty element exists in a but not in b | |
166 diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] | |
167 # the following method returns True if the values for the common keys are not equals | |
168 diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] | |
169 # finally the combination of both to return True if a difference is found | |
170 diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) | |
171 | |
172 return diff(content, self._original_content) | |
173 | |
174 def edit(self, edit, abort=False, sync=False): | |
175 """ | |
176 Remark: the editor must be visible before you call this method. | |
177 @param edit: set to True to edit the content or False to only display it | |
178 @param abort: set to True to cancel the edition and loose the changes. | |
179 If edit and abort are both True, self.abortEdition can be used to ask for a | |
180 confirmation. When edit is False and abort is True, abortion is actually done. | |
181 @param sync: set to True to cancel the edition after the content has been saved somewhere else | |
182 """ | |
183 if edit: | |
184 if not self.initialized: | |
185 self.syncToEditor() # e.g.: use the selected target and unibox content | |
186 self.setFocus(True) | |
187 if abort: | |
188 content = self.getContent() | |
189 if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation | |
190 self.edit(False, True, sync) | |
191 return | |
192 if sync: | |
193 self.syncFromEditor(content) # e.g.: save the content to unibox | |
194 return | |
195 else: | |
196 if not self.initialized: | |
197 return | |
198 content = self.getContent() | |
199 if abort: | |
200 self._afterEditCb(content) | |
201 return | |
202 if self.__modifiedCb and self.modified(content): | |
203 result = self.__modifiedCb(content) # e.g.: send a message or update something | |
204 if result is not None: | |
205 if self._afterEditCb: | |
206 self._afterEditCb(content) # e.g.: restore the display mode | |
207 if result is True: | |
208 self.setContent(content) | |
209 elif self._afterEditCb: | |
210 self._afterEditCb(content) | |
211 | |
212 self.initialized = True | |
213 | |
214 def setFocus(self, focus): | |
215 """ | |
216 @param focus: set to True to focus the editor | |
217 """ | |
218 raise NotImplementedError | |
219 | |
220 def syncToEditor(self): | |
221 pass | |
222 | |
223 def syncFromEditor(self, content): | |
224 pass | |
225 | |
226 def abortEdition(self, content): | |
227 return True | |
228 | |
229 def addEditListener(self, listener): | |
230 """Add a method to be called whenever the text is edited. | |
231 @param listener: method taking two arguments: sender, keycode""" | |
232 self.edit_listeners.append(listener) | |
233 | |
234 | |
235 class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): | |
236 """Base class for manage a simple text editor.""" | |
237 | |
238 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): | |
239 """ | |
240 @param content | |
241 @param modifiedCb | |
242 @param afterEditCb | |
243 @param options: dict with the following value: | |
244 - no_xhtml: set to True to clean any xhtml content. | |
245 - enhance_display: if True, the display text will be enhanced with strings.addURLToText | |
246 - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. | |
247 - listen_focus: set to True to terminate the edition when the focus is lost. | |
248 - listen_click: set to True to start the edition when you click on the widget. | |
249 """ | |
250 self.options = {'no_xhtml': False, | |
251 'enhance_display': True, | |
252 'listen_keyboard': True, | |
253 'listen_focus': False, | |
254 'listen_click': False | |
255 } | |
256 if options: | |
257 self.options.update(options) | |
258 self.__shift_down = False | |
259 if self.options['listen_focus']: | |
260 FocusHandler.__init__(self) | |
261 if self.options['listen_click']: | |
262 ClickHandler.__init__(self) | |
263 KeyboardHandler.__init__(self) | |
264 strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) | |
265 BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) | |
266 self.textarea = self.display = None | |
267 | |
268 def setContent(self, content=None): | |
269 BaseTextEditor.setContent(self, content) | |
270 | |
271 def getContent(self): | |
272 raise NotImplementedError | |
273 | |
274 def edit(self, edit, abort=False, sync=False): | |
275 BaseTextEditor.edit(self, edit) | |
276 if edit: | |
277 if self.options['listen_focus'] and self not in self.textarea._focusListeners: | |
278 self.textarea.addFocusListener(self) | |
279 if self.options['listen_click']: | |
280 self.display.clearClickListener() | |
281 if self not in self.textarea._keyboardListeners: | |
282 self.textarea.addKeyboardListener(self) | |
283 else: | |
284 self.setDisplayContent() | |
285 if self.options['listen_focus']: | |
286 try: | |
287 self.textarea.removeFocusListener(self) | |
288 except ValueError: | |
289 pass | |
290 if self.options['listen_click'] and self not in self.display._clickListeners: | |
291 self.display.addClickListener(self) | |
292 try: | |
293 self.textarea.removeKeyboardListener(self) | |
294 except ValueError: | |
295 pass | |
296 | |
297 def setDisplayContent(self): | |
298 text = self._original_content['text'] | |
299 if not self.options['no_xhtml']: | |
300 text = strings.addURLToImage(text) | |
301 if self.options['enhance_display']: | |
302 text = strings.addURLToText(text) | |
303 self.display.setHTML(html_tools.convertNewLinesToXHTML(text)) | |
304 | |
305 def setFocus(self, focus): | |
306 raise NotImplementedError | |
307 | |
308 def onKeyDown(self, sender, keycode, modifiers): | |
309 for listener in self.edit_listeners: | |
310 listener(self.textarea, keycode) | |
311 if not self.options['listen_keyboard']: | |
312 return | |
313 if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> | |
314 self.__shift_down = True | |
315 return | |
316 if keycode == KEY_ENTER: # finish the edition | |
317 self.textarea.setFocus(False) | |
318 if not self.options['listen_focus']: | |
319 self.edit(False) | |
320 | |
321 def onKeyUp(self, sender, keycode, modifiers): | |
322 if keycode == KEY_SHIFT: | |
323 self.__shift_down = False | |
324 | |
325 def onLostFocus(self, sender): | |
326 """Finish the edition when focus is lost""" | |
327 if self.options['listen_focus']: | |
328 self.edit(False) | |
329 | |
330 def onClick(self, sender=None): | |
331 """Start the edition when the widget is clicked""" | |
332 if self.options['listen_click']: | |
333 self.edit(True) | |
334 | |
335 def onBrowserEvent(self, event): | |
336 if self.options['listen_focus']: | |
337 FocusHandler.onBrowserEvent(self, event) | |
338 if self.options['listen_click']: | |
339 ClickHandler.onBrowserEvent(self, event) | |
340 KeyboardHandler.onBrowserEvent(self, event) | |
341 | |
342 | |
343 class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): | |
344 """Manage a simple text editor with the HTML 5 "contenteditable" property.""" | |
345 | |
346 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): | |
347 HTML.__init__(self) | |
348 SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) | |
349 self.textarea = self.display = self | |
350 | |
351 def getContent(self): | |
352 text = DOM.getInnerHTML(self.getElement()) | |
353 return {'text': self.strproc(text) if text else ''} | |
354 | |
355 def edit(self, edit, abort=False, sync=False): | |
356 if edit: | |
357 self.textarea.setHTML(self._original_content['text']) | |
358 self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') | |
359 SimpleTextEditor.edit(self, edit, abort, sync) | |
360 | |
361 def setFocus(self, focus): | |
362 if focus: | |
363 self.getElement().focus() | |
364 else: | |
365 self.getElement().blur() | |
366 | |
367 | |
368 class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): | |
369 """Manage a simple text editor with a TextArea for editing, HTML for display.""" | |
370 | |
371 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): | |
372 SimplePanel.__init__(self) | |
373 SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) | |
374 self.textarea = TextArea() | |
375 self.display = HTML() | |
376 | |
377 def getContent(self): | |
378 text = self.textarea.getText() | |
379 return {'text': self.strproc(text) if text else ''} | |
380 | |
381 def edit(self, edit, abort=False, sync=False): | |
382 if edit: | |
383 self.textarea.setText(self._original_content['text']) | |
384 self.setWidget(self.textarea if edit else self.display) | |
385 SimpleTextEditor.edit(self, edit, abort, sync) | |
386 | |
387 def setFocus(self, focus): | |
388 if focus and self.isAttached(): | |
389 self.textarea.setCursorPos(len(self.textarea.getText())) | |
390 self.textarea.setFocus(focus) |