Mercurial > libervia-web
comparison src/browser/sat_browser/base_panels.py @ 467:97c72fe4a5f2
browser_side: import fixes:
- moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly
- refactored bad import practices: classes are most of time not imported directly, module is imported instead.
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 09 Jun 2014 22:15:26 +0200 |
parents | src/browser/base_panels.py@981ed669d3b3 |
children | 830b50593597 |
comparison
equal
deleted
inserted
replaced
466:01880aa8ea2d | 467:97c72fe4a5f2 |
---|---|
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 import pyjd # this is dummy in pyjs | |
21 from sat.core.log import getLogger | |
22 log = getLogger(__name__) | |
23 from sat.core.i18n import _ | |
24 from sat_frontends.tools import strings | |
25 | |
26 from pyjamas.ui.AbsolutePanel import AbsolutePanel | |
27 from pyjamas.ui.VerticalPanel import VerticalPanel | |
28 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
29 from pyjamas.ui.HTMLPanel import HTMLPanel | |
30 from pyjamas.ui.Button import Button | |
31 from pyjamas.ui.HTML import HTML | |
32 from pyjamas.ui.SimplePanel import SimplePanel | |
33 from pyjamas.ui.PopupPanel import PopupPanel | |
34 from pyjamas.ui.StackPanel import StackPanel | |
35 from pyjamas.ui.TextArea import TextArea | |
36 from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT | |
37 from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler | |
38 from pyjamas.ui.FocusListener import FocusHandler | |
39 from pyjamas.ui.ClickListener import ClickHandler | |
40 from pyjamas import DOM | |
41 | |
42 from datetime import datetime | |
43 from time import time | |
44 | |
45 import html_tools | |
46 from constants import Const as C | |
47 | |
48 | |
49 class ChatText(HTMLPanel): | |
50 | |
51 def __init__(self, timestamp, nick, mymess, msg, xhtml=None): | |
52 _date = datetime.fromtimestamp(float(timestamp or time())) | |
53 _msg_class = ["chat_text_msg"] | |
54 if mymess: | |
55 _msg_class.append("chat_text_mymess") | |
56 HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" % | |
57 {"timestamp": _date.strftime("%H:%M"), | |
58 "nick": "[%s]" % html_tools.sanitize(nick), | |
59 "msg_class": ' '.join(_msg_class), | |
60 "msg": strings.addURLToText(html_tools.sanitize(msg)) if not xhtml else html_tools.inlineRoot(xhtml)} # FIXME: images and external links must be removed according to preferences | |
61 ) | |
62 self.setStyleName('chatText') | |
63 | |
64 | |
65 class Occupant(HTML): | |
66 """Occupant of a MUC room""" | |
67 | |
68 def __init__(self, nick, state=None, special=""): | |
69 """ | |
70 @param nick: the user nickname | |
71 @param state: the user chate state (XEP-0085) | |
72 @param special: a string of symbols (e.g: for activities) | |
73 """ | |
74 HTML.__init__(self) | |
75 self.nick = nick | |
76 self._state = state | |
77 self.special = special | |
78 self._refresh() | |
79 | |
80 def __str__(self): | |
81 return self.nick | |
82 | |
83 def setState(self, state): | |
84 self._state = state | |
85 self._refresh() | |
86 | |
87 def addSpecial(self, special): | |
88 """@param special: unicode""" | |
89 if special not in self.special: | |
90 self.special += special | |
91 self._refresh() | |
92 | |
93 def removeSpecials(self, special): | |
94 """@param special: unicode or list""" | |
95 if not isinstance(special, list): | |
96 special = [special] | |
97 for symbol in special: | |
98 self.special = self.special.replace(symbol, "") | |
99 self._refresh() | |
100 | |
101 def _refresh(self): | |
102 state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' | |
103 special = "" if len(self.special) == 0 else " %s" % self.special | |
104 self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_tools.sanitize(self.nick), special, state)) | |
105 | |
106 | |
107 class OccupantsList(AbsolutePanel): | |
108 """Panel user to show occupants of a room""" | |
109 | |
110 def __init__(self): | |
111 AbsolutePanel.__init__(self) | |
112 self.occupants_list = {} | |
113 self.setStyleName('occupantsList') | |
114 | |
115 def addOccupant(self, nick): | |
116 _occupant = Occupant(nick) | |
117 self.occupants_list[nick] = _occupant | |
118 self.add(_occupant) | |
119 | |
120 def removeOccupant(self, nick): | |
121 try: | |
122 self.remove(self.occupants_list[nick]) | |
123 except KeyError: | |
124 log.error("trying to remove an unexisting nick") | |
125 | |
126 def clear(self): | |
127 self.occupants_list.clear() | |
128 AbsolutePanel.clear(self) | |
129 | |
130 def updateSpecials(self, occupants=[], html=""): | |
131 """Set the specified html "symbol" to the listed occupants, | |
132 and eventually remove it from the others (if they got it). | |
133 This is used for example to visualize who is playing a game. | |
134 @param occupants: list of the occupants that need the symbol | |
135 @param html: unicode symbol (actually one character or more) | |
136 or a list to assign different symbols of the same family. | |
137 """ | |
138 index = 0 | |
139 special = html | |
140 for occupant in self.occupants_list.keys(): | |
141 if occupant in occupants: | |
142 if isinstance(html, list): | |
143 special = html[index] | |
144 index = (index + 1) % len(html) | |
145 self.occupants_list[occupant].addSpecial(special) | |
146 else: | |
147 self.occupants_list[occupant].removeSpecials(html) | |
148 | |
149 | |
150 class PopupMenuPanel(PopupPanel): | |
151 """This implementation of a popup menu (context menu) allow you to assign | |
152 two special methods which are common to all the items, in order to hide | |
153 certain items and also easily define their callbacks. The menu can be | |
154 bound to any of the mouse button (left, middle, right). | |
155 """ | |
156 def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs): | |
157 """ | |
158 @param entries: a dict of dicts, where each sub-dict is representing | |
159 one menu item: the sub-dict key can be used as the item text and | |
160 description, but optional "title" and "desc" entries would be used | |
161 if they exists. The sub-dicts may be extended later to do | |
162 more complicated stuff or overwrite the common methods. | |
163 @param hide: function with 2 args: widget, key as string and | |
164 returns True if that item should be hidden from the context menu. | |
165 @param callback: function with 2 args: sender, key as string | |
166 @param vertical: True or False, to set the direction | |
167 @param item_style: alternative CSS class for the menu items | |
168 @param menu_style: supplementary CSS class for the sender widget | |
169 """ | |
170 PopupPanel.__init__(self, autoHide=True, **kwargs) | |
171 self._entries = entries | |
172 self._hide = hide | |
173 self._callback = callback | |
174 self.vertical = vertical | |
175 self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"} | |
176 if isinstance(style, dict): | |
177 self.style.update(style) | |
178 self._senders = {} | |
179 | |
180 def _show(self, sender): | |
181 """Popup the menu relative to this sender's position. | |
182 @param sender: the widget that has been clicked | |
183 """ | |
184 menu = VerticalPanel() if self.vertical is True else HorizontalPanel() | |
185 menu.setStyleName(self.style["menu"]) | |
186 | |
187 def button_cb(item): | |
188 """You can not put that method in the loop and rely | |
189 on _key, because it is overwritten by each step. | |
190 You can rely on item.key instead, which is copied | |
191 from _key after the item creation. | |
192 @param item: the menu item that has been clicked | |
193 """ | |
194 if self._callback is not None: | |
195 self._callback(sender=sender, key=item.key) | |
196 self.hide(autoClosed=True) | |
197 | |
198 for _key in self._entries.keys(): | |
199 entry = self._entries[_key] | |
200 if self._hide is not None and self._hide(sender=sender, key=_key) is True: | |
201 continue | |
202 title = entry["title"] if "title" in entry.keys() else _key | |
203 item = Button(title, button_cb) | |
204 item.key = _key | |
205 item.setStyleName(self.style["item"]) | |
206 item.setTitle(entry["desc"] if "desc" in entry.keys() else title) | |
207 menu.add(item) | |
208 if len(menu.getChildren()) == 0: | |
209 return | |
210 self.add(menu) | |
211 if self.vertical is True: | |
212 x = sender.getAbsoluteLeft() + sender.getOffsetWidth() | |
213 y = sender.getAbsoluteTop() | |
214 else: | |
215 x = sender.getAbsoluteLeft() | |
216 y = sender.getAbsoluteTop() + sender.getOffsetHeight() | |
217 self.setPopupPosition(x, y) | |
218 self.show() | |
219 if self.style["selected"]: | |
220 sender.addStyleDependentName(self.style["selected"]) | |
221 | |
222 def _onHide(popup): | |
223 if self.style["selected"]: | |
224 sender.removeStyleDependentName(self.style["selected"]) | |
225 return PopupPanel.onHideImpl(self, popup) | |
226 | |
227 self.onHideImpl = _onHide | |
228 | |
229 def registerClickSender(self, sender, button=BUTTON_LEFT): | |
230 """Bind the menu to the specified sender. | |
231 @param sender: the widget to which the menu should be bound | |
232 @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT | |
233 """ | |
234 self._senders.setdefault(sender, []) | |
235 self._senders[sender].append(button) | |
236 | |
237 if button == BUTTON_RIGHT: | |
238 # WARNING: to disable the context menu is a bit tricky... | |
239 # The following seems to work on Firefox 24.0, but: | |
240 # TODO: find a cleaner way to disable the context menu | |
241 sender.getElement().setAttribute("oncontextmenu", "return false") | |
242 | |
243 def _onBrowserEvent(event): | |
244 button = DOM.eventGetButton(event) | |
245 if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: | |
246 self._show(sender) | |
247 return sender.__class__.onBrowserEvent(sender, event) | |
248 | |
249 sender.onBrowserEvent = _onBrowserEvent | |
250 | |
251 def registerMiddleClickSender(self, sender): | |
252 self.registerClickSender(sender, BUTTON_MIDDLE) | |
253 | |
254 def registerRightClickSender(self, sender): | |
255 self.registerClickSender(sender, BUTTON_RIGHT) | |
256 | |
257 | |
258 class ToggleStackPanel(StackPanel): | |
259 """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be | |
260 visible at the same time, clicking a sub-panel header will not display it and hide | |
261 the others but only toggle its own visibility. The argument 'visibleStack' is ignored. | |
262 Note that the argument 'visible' has been added to listener's 'onStackChanged' method. | |
263 """ | |
264 | |
265 def __init__(self, **kwargs): | |
266 StackPanel.__init__(self, **kwargs) | |
267 | |
268 def onBrowserEvent(self, event): | |
269 if DOM.eventGetType(event) == "click": | |
270 index = self.getDividerIndex(DOM.eventGetTarget(event)) | |
271 if index != -1: | |
272 self.toggleStack(index) | |
273 | |
274 def add(self, widget, stackText="", asHTML=False, visible=False): | |
275 StackPanel.add(self, widget, stackText, asHTML) | |
276 self.setStackVisible(self.getWidgetCount() - 1, visible) | |
277 | |
278 def toggleStack(self, index): | |
279 if index >= self.getWidgetCount(): | |
280 return | |
281 visible = not self.getWidget(index).getVisible() | |
282 self.setStackVisible(index, visible) | |
283 for listener in self.stackListeners: | |
284 listener.onStackChanged(self, index, visible) | |
285 | |
286 | |
287 class TitlePanel(ToggleStackPanel): | |
288 """A toggle panel to set the message title""" | |
289 def __init__(self): | |
290 ToggleStackPanel.__init__(self, Width="100%") | |
291 self.text_area = TextArea() | |
292 self.add(self.text_area, _("Title")) | |
293 self.addStackChangeListener(self) | |
294 | |
295 def onStackChanged(self, sender, index, visible=None): | |
296 if visible is None: | |
297 visible = sender.getWidget(index).getVisible() | |
298 text = self.text_area.getText() | |
299 suffix = "" if (visible or not text) else (": %s" % text) | |
300 sender.setStackText(index, _("Title") + suffix) | |
301 | |
302 def getText(self): | |
303 return self.text_area.getText() | |
304 | |
305 def setText(self, text): | |
306 self.text_area.setText(text) | |
307 | |
308 | |
309 class BaseTextEditor(object): | |
310 """Basic definition of a text editor. The method edit gets a boolean parameter which | |
311 should be set to True when you want to edit the text and False to only display it.""" | |
312 | |
313 def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): | |
314 """ | |
315 Remark when inheriting this class: since the setContent method could be | |
316 overwritten by the child class, you should consider calling this __init__ | |
317 after all the parameters affecting this setContent method have been set. | |
318 @param content: dict with at least a 'text' key | |
319 @param strproc: method to be applied on strings to clean the content | |
320 @param modifiedCb: method to be called when the text has been modified. | |
321 If this method returns: | |
322 - True: the modification will be saved and afterEditCb called; | |
323 - False: the modification won't be saved and afterEditCb called; | |
324 - None: the modification won't be saved and afterEditCb not called. | |
325 @param afterEditCb: method to be called when the edition is done | |
326 """ | |
327 if content is None: | |
328 content = {'text': ''} | |
329 assert('text' in content) | |
330 if strproc is None: | |
331 def strproc(text): | |
332 try: | |
333 return text.strip() | |
334 except (TypeError, AttributeError): | |
335 return text | |
336 self.strproc = strproc | |
337 self.__modifiedCb = modifiedCb | |
338 self._afterEditCb = afterEditCb | |
339 self.initialized = False | |
340 self.edit_listeners = [] | |
341 self.setContent(content) | |
342 | |
343 def setContent(self, content=None): | |
344 """Set the editable content. The displayed content, which is set from the child class, could differ. | |
345 @param content: dict with at least a 'text' key | |
346 """ | |
347 if content is None: | |
348 content = {'text': ''} | |
349 elif not isinstance(content, dict): | |
350 content = {'text': content} | |
351 assert('text' in content) | |
352 self._original_content = {} | |
353 for key in content: | |
354 self._original_content[key] = self.strproc(content[key]) | |
355 | |
356 def getContent(self): | |
357 """Get the current edited or editable content. | |
358 @return: dict with at least a 'text' key | |
359 """ | |
360 raise NotImplementedError | |
361 | |
362 def setOriginalContent(self, content): | |
363 """Use this method with care! Content initialization should normally be | |
364 done with self.setContent. This method exists to let you trick the editor, | |
365 e.g. for self.modified to return True also when nothing has been modified. | |
366 @param content: dict | |
367 """ | |
368 self._original_content = content | |
369 | |
370 def getOriginalContent(self): | |
371 """ | |
372 @return the original content before modification (dict) | |
373 """ | |
374 return self._original_content | |
375 | |
376 def modified(self, content=None): | |
377 """Check if the content has been modified. | |
378 Remark: we don't use the direct comparison because we want to ignore empty elements | |
379 @content: content to be check against the original content or None to use the current content | |
380 @return: True if the content has been modified. | |
381 """ | |
382 if content is None: | |
383 content = self.getContent() | |
384 # the following method returns True if one non empty element exists in a but not in b | |
385 diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] | |
386 # the following method returns True if the values for the common keys are not equals | |
387 diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] | |
388 # finally the combination of both to return True if a difference is found | |
389 diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) | |
390 | |
391 return diff(content, self._original_content) | |
392 | |
393 def edit(self, edit, abort=False, sync=False): | |
394 """ | |
395 Remark: the editor must be visible before you call this method. | |
396 @param edit: set to True to edit the content or False to only display it | |
397 @param abort: set to True to cancel the edition and loose the changes. | |
398 If edit and abort are both True, self.abortEdition can be used to ask for a | |
399 confirmation. When edit is False and abort is True, abortion is actually done. | |
400 @param sync: set to True to cancel the edition after the content has been saved somewhere else | |
401 """ | |
402 if edit: | |
403 if not self.initialized: | |
404 self.syncToEditor() # e.g.: use the selected target and unibox content | |
405 self.setFocus(True) | |
406 if abort: | |
407 content = self.getContent() | |
408 if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation | |
409 self.edit(False, True, sync) | |
410 return | |
411 if sync: | |
412 self.syncFromEditor(content) # e.g.: save the content to unibox | |
413 return | |
414 else: | |
415 if not self.initialized: | |
416 return | |
417 content = self.getContent() | |
418 if abort: | |
419 self._afterEditCb(content) | |
420 return | |
421 if self.__modifiedCb and self.modified(content): | |
422 result = self.__modifiedCb(content) # e.g.: send a message or update something | |
423 if result is not None: | |
424 if self._afterEditCb: | |
425 self._afterEditCb(content) # e.g.: restore the display mode | |
426 if result is True: | |
427 self.setContent(content) | |
428 elif self._afterEditCb: | |
429 self._afterEditCb(content) | |
430 | |
431 self.initialized = True | |
432 | |
433 def setFocus(self, focus): | |
434 """ | |
435 @param focus: set to True to focus the editor | |
436 """ | |
437 raise NotImplementedError | |
438 | |
439 def syncToEditor(self): | |
440 pass | |
441 | |
442 def syncFromEditor(self, content): | |
443 pass | |
444 | |
445 def abortEdition(self, content): | |
446 return True | |
447 | |
448 def addEditListener(self, listener): | |
449 """Add a method to be called whenever the text is edited. | |
450 @param listener: method taking two arguments: sender, keycode""" | |
451 self.edit_listeners.append(listener) | |
452 | |
453 | |
454 class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): | |
455 """Base class for manage a simple text editor.""" | |
456 | |
457 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): | |
458 """ | |
459 @param content | |
460 @param modifiedCb | |
461 @param afterEditCb | |
462 @param options: dict with the following value: | |
463 - no_xhtml: set to True to clean any xhtml content. | |
464 - enhance_display: if True, the display text will be enhanced with strings.addURLToText | |
465 - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. | |
466 - listen_focus: set to True to terminate the edition when the focus is lost. | |
467 - listen_click: set to True to start the edition when you click on the widget. | |
468 """ | |
469 self.options = {'no_xhtml': False, | |
470 'enhance_display': True, | |
471 'listen_keyboard': True, | |
472 'listen_focus': False, | |
473 'listen_click': False | |
474 } | |
475 if options: | |
476 self.options.update(options) | |
477 self.__shift_down = False | |
478 if self.options['listen_focus']: | |
479 FocusHandler.__init__(self) | |
480 if self.options['listen_click']: | |
481 ClickHandler.__init__(self) | |
482 KeyboardHandler.__init__(self) | |
483 strproc = lambda text: html_tools.sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) | |
484 BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) | |
485 self.textarea = self.display = None | |
486 | |
487 def setContent(self, content=None): | |
488 BaseTextEditor.setContent(self, content) | |
489 | |
490 def getContent(self): | |
491 raise NotImplementedError | |
492 | |
493 def edit(self, edit, abort=False, sync=False): | |
494 BaseTextEditor.edit(self, edit) | |
495 if edit: | |
496 if self.options['listen_focus'] and self not in self.textarea._focusListeners: | |
497 self.textarea.addFocusListener(self) | |
498 if self.options['listen_click']: | |
499 self.display.clearClickListener() | |
500 if self not in self.textarea._keyboardListeners: | |
501 self.textarea.addKeyboardListener(self) | |
502 else: | |
503 self.setDisplayContent() | |
504 if self.options['listen_focus']: | |
505 try: | |
506 self.textarea.removeFocusListener(self) | |
507 except ValueError: | |
508 pass | |
509 if self.options['listen_click'] and self not in self.display._clickListeners: | |
510 self.display.addClickListener(self) | |
511 try: | |
512 self.textarea.removeKeyboardListener(self) | |
513 except ValueError: | |
514 pass | |
515 | |
516 def setDisplayContent(self): | |
517 text = self._original_content['text'] | |
518 if not self.options['no_xhtml']: | |
519 text = strings.addURLToImage(text) | |
520 if self.options['enhance_display']: | |
521 text = strings.addURLToText(text) | |
522 self.display.setHTML(html_tools.convertNewLinesToXHTML(text)) | |
523 | |
524 def setFocus(self, focus): | |
525 raise NotImplementedError | |
526 | |
527 def onKeyDown(self, sender, keycode, modifiers): | |
528 for listener in self.edit_listeners: | |
529 listener(self.textarea, keycode) | |
530 if not self.options['listen_keyboard']: | |
531 return | |
532 if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> | |
533 self.__shift_down = True | |
534 return | |
535 if keycode == KEY_ENTER: # finish the edition | |
536 self.textarea.setFocus(False) | |
537 if not self.options['listen_focus']: | |
538 self.edit(False) | |
539 | |
540 def onKeyUp(self, sender, keycode, modifiers): | |
541 if keycode == KEY_SHIFT: | |
542 self.__shift_down = False | |
543 | |
544 def onLostFocus(self, sender): | |
545 """Finish the edition when focus is lost""" | |
546 if self.options['listen_focus']: | |
547 self.edit(False) | |
548 | |
549 def onClick(self, sender=None): | |
550 """Start the edition when the widget is clicked""" | |
551 if self.options['listen_click']: | |
552 self.edit(True) | |
553 | |
554 def onBrowserEvent(self, event): | |
555 if self.options['listen_focus']: | |
556 FocusHandler.onBrowserEvent(self, event) | |
557 if self.options['listen_click']: | |
558 ClickHandler.onBrowserEvent(self, event) | |
559 KeyboardHandler.onBrowserEvent(self, event) | |
560 | |
561 | |
562 class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): | |
563 """Manage a simple text editor with the HTML 5 "contenteditable" property.""" | |
564 | |
565 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): | |
566 HTML.__init__(self) | |
567 SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) | |
568 self.textarea = self.display = self | |
569 | |
570 def getContent(self): | |
571 text = DOM.getInnerHTML(self.getElement()) | |
572 return {'text': self.strproc(text) if text else ''} | |
573 | |
574 def edit(self, edit, abort=False, sync=False): | |
575 if edit: | |
576 self.textarea.setHTML(self._original_content['text']) | |
577 self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') | |
578 SimpleTextEditor.edit(self, edit, abort, sync) | |
579 | |
580 def setFocus(self, focus): | |
581 if focus: | |
582 self.getElement().focus() | |
583 else: | |
584 self.getElement().blur() | |
585 | |
586 | |
587 class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): | |
588 """Manage a simple text editor with a TextArea for editing, HTML for display.""" | |
589 | |
590 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): | |
591 SimplePanel.__init__(self) | |
592 SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) | |
593 self.textarea = TextArea() | |
594 self.display = HTML() | |
595 | |
596 def getContent(self): | |
597 text = self.textarea.getText() | |
598 return {'text': self.strproc(text) if text else ''} | |
599 | |
600 def edit(self, edit, abort=False, sync=False): | |
601 if edit: | |
602 self.textarea.setText(self._original_content['text']) | |
603 self.setWidget(self.textarea if edit else self.display) | |
604 SimpleTextEditor.edit(self, edit, abort, sync) | |
605 | |
606 def setFocus(self, focus): | |
607 if focus: | |
608 self.textarea.setCursorPos(len(self.textarea.getText())) | |
609 self.textarea.setFocus(focus) |