comparison browser_side/base_panels.py @ 349:f488692c4903

browser_side: LightTextEditor inheritates from BaseTextEditor + display URL in the status
author souliane <souliane@mailoo.org>
date Wed, 12 Feb 2014 14:58:11 +0100
parents f1ba38043d78
children b95099a1d11b
comparison
equal deleted inserted replaced
348:83454ba70a9c 349:f488692c4903
33 from pyjamas import DOM 33 from pyjamas import DOM
34 34
35 from datetime import datetime 35 from datetime import datetime
36 from time import time 36 from time import time
37 37
38 from tools import html_sanitize, html_clean, inlineRoot 38 from tools import html_sanitize, html_strip, inlineRoot
39 39
40 from sat_frontends.tools.strings import addURLToText 40 from sat_frontends.tools.strings import addURLToText
41 from sat.core.i18n import _ 41 from sat.core.i18n import _
42 42
43 43
135 """This implementation of a popup menu (context menu) allow you to assign 135 """This implementation of a popup menu (context menu) allow you to assign
136 two special methods which are common to all the items, in order to hide 136 two special methods which are common to all the items, in order to hide
137 certain items and also easily define their callbacks. The menu can be 137 certain items and also easily define their callbacks. The menu can be
138 bound to any of the mouse button (left, middle, right). 138 bound to any of the mouse button (left, middle, right).
139 """ 139 """
140 def __init__(self, entries, hide=None, callback=None, vertical=True, style={}, **kwargs): 140 def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs):
141 """ 141 """
142 @param entries: a dict of dicts, where each sub-dict is representing 142 @param entries: a dict of dicts, where each sub-dict is representing
143 one menu item: the sub-dict key can be used as the item text and 143 one menu item: the sub-dict key can be used as the item text and
144 description, but optional "title" and "desc" entries would be used 144 description, but optional "title" and "desc" entries would be used
145 if they exists. The sub-dicts may be extended later to do 145 if they exists. The sub-dicts may be extended later to do
155 self._entries = entries 155 self._entries = entries
156 self._hide = hide 156 self._hide = hide
157 self._callback = callback 157 self._callback = callback
158 self.vertical = vertical 158 self.vertical = vertical
159 self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"} 159 self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"}
160 self.style.update(style) 160 if isinstance(style, dict):
161 self.style.update(style)
161 self._senders = {} 162 self._senders = {}
162 163
163 def _show(self, sender): 164 def _show(self, sender):
164 """Popup the menu relative to this sender's position. 165 """Popup the menu relative to this sender's position.
165 @param sender: the widget that has been clicked 166 @param sender: the widget that has been clicked
287 288
288 def setText(self, text): 289 def setText(self, text):
289 self.text_area.setText(text) 290 self.text_area.setText(text)
290 291
291 292
292 class LightTextEditor(HTML, FocusHandler, KeyboardHandler): 293 class BaseTextEditor(object):
293 294 """Basic definition of a text editor. The method edit gets a boolean parameter which
294 def __init__(self, content='', single_line=False, callback=None): 295 should be set to True when you want to edit the text and False to only display it."""
296
297 def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None):
298 """
299 Remark when inheriting this class: since the setContent method could be
300 overwritten by the child class, you should consider calling this __init__
301 after all the parameters affecting this setContent method have been set.
302 @param content: dict with at least a 'text' key
303 @param strproc: method to be applied on strings to clean the content
304 @param modifiedCb: method to be called when the text has been modified.
305 If this method returns:
306 - True: the modification will be saved and afterEditCb called;
307 - False: the modification won't be saved and afterEditCb called;
308 - None: the modification won't be saved and afterEditCb not called.
309 @param afterEditCb: method to be called when the edition is done
310 """
311 if content is None:
312 content = {'text': ''}
313 assert('text' in content)
314 if strproc is None:
315 def strproc(text):
316 try:
317 return text.strip()
318 except (TypeError, AttributeError):
319 return text
320 self.strproc = strproc
321 self.__modifiedCb = modifiedCb
322 self._afterEditCb = afterEditCb
323 self.initialized = False
324 self.setContent(content)
325
326 def setContent(self, content=None):
327 """Set the editable content. The displayed content, which is set from the child class, could differ.
328 @param content: dict with at least a 'text' key
329 """
330 if content is None:
331 content = {'text': ''}
332 elif not isinstance(content, dict):
333 content = {'text': content}
334 assert('text' in content)
335 self._original_content = {}
336 for key in content:
337 self._original_content[key] = self.strproc(content[key])
338
339 def getContent(self):
340 """Get the current edited or editable content.
341 @return: dict with at least a 'text' key
342 """
343 raise NotImplementedError
344
345 def modified(self, content=None):
346 """Check if the content has been modified.
347 Remark: we don't use the direct comparison because we want to ignore empty elements
348 @content: content to be check against the original content or None to use the current content
349 @return: True if the content has been modified.
350 """
351 if content is None:
352 content = self.getContent()
353 # the following method returns True if one non empty element exists in a but not in b
354 diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != []
355 # the following method returns True if the values for the common keys are not equals
356 diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != []
357 # finally the combination of both to return True if a difference is found
358 diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b)
359
360 return diff(content, self._original_content)
361
362 def edit(self, edit, abort=False, sync=False):
363 """
364 Remark: the editor must be visible before you call this method.
365 @param edit: set to True to edit the content or False to only display it
366 @param abort: set to True to cancel the edition and loose the changes.
367 If edit and abort are both True, self.abortEdition can be used to ask for a
368 confirmation. When edit is False and abort is True, abortion is actually done.
369 @param sync: set to True to cancel the edition after the content has been saved somewhere else
370 """
371 if edit:
372 if not self.initialized:
373 self.syncToEditor() # e.g.: use the selected target and unibox content
374 self.setFocus(True)
375 if abort:
376 content = self.getContent()
377 if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation
378 self.edit(False, True, sync)
379 return
380 if sync:
381 self.syncFromEditor(content) # e.g.: save the content to unibox
382 return
383 else:
384 if not self.initialized:
385 return
386 content = self.getContent()
387 if abort:
388 self._afterEditCb(content)
389 return
390 if self.__modifiedCb and self.modified(content):
391 result = self.__modifiedCb(content) # e.g.: send a message or update something
392 if result is not None:
393 if self._afterEditCb:
394 self._afterEditCb(content) # e.g.: restore the display mode
395 if result is True:
396 self.setContent(content)
397 elif self._afterEditCb:
398 self._afterEditCb(content)
399
400 self.initialized = True
401
402 def setFocus(self, focus):
403 """
404 @param focus: set to True to focus the editor
405 """
406 raise NotImplementedError
407
408 def syncToEditor(self):
409 pass
410
411 def syncFromEditor(self, content):
412 pass
413
414 def abortEdition(self, content):
415 return True
416
417
418 class LightTextEditor(BaseTextEditor, HTML, FocusHandler, KeyboardHandler):
419 """Manage a simple text editor whith the HTML 5 "contenteditable" property."""
420
421 def __init__(self, content=None, modifiedCb=None, afterEditCb=None, single_line=False):
422 """
423 @param content
424 @param modifiedCb
425 @param afterEditCb
426 @param single_line: set to True to manage a single line editor. In that case
427 the edition would be terminated when the focus is lost or <enter> key is pressed.
428 """
295 HTML.__init__(self) 429 HTML.__init__(self)
296 if single_line: 430 if single_line:
297 FocusHandler.__init__(self) 431 FocusHandler.__init__(self)
298 KeyboardHandler.__init__(self) 432 KeyboardHandler.__init__(self)
299 self.__single_line = single_line 433 self.__single_line = single_line
300 self.__callback = callback 434 strproc = lambda text: html_sanitize(html_strip(text)) if self.__single_line else html_strip(text)
301 self.setContent(content) 435 BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb)
302 436
303 def setContent(self, content=''): 437 def setContent(self, content=None):
304 self.__original_content = html_clean(content) if self.__single_line else content.strip() 438 BaseTextEditor.setContent(self, content)
305 self.setHTML('<div>%s</div>' % self.__original_content)
306 439
307 def getContent(self): 440 def getContent(self):
308 content = DOM.getInnerHTML(self.getElement().firstChild) 441 text = DOM.getInnerHTML(self.getElement())
309 return html_clean(content) if self.__single_line else content.strip() 442 return {'text': self.strproc(text) if text else ''}
310 443
311 def modified(self, content=None): 444 def edit(self, edit, abort=False, sync=False):
312 if content is None: 445 if edit:
313 content = self.getContent() 446 self.setHTML(self._original_content['text'])
314 return content != self.__original_content
315
316 def edit(self, edit):
317 self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') 447 self.getElement().setAttribute('contenteditable', 'true' if edit else 'false')
448 BaseTextEditor.edit(self, edit)
318 if edit: 449 if edit:
319 self.setFocus(True)
320 if self.__single_line: 450 if self.__single_line:
321 self.addFocusListener(self) 451 self.addFocusListener(self)
322 self.addKeyboardListener(self) 452 self.addKeyboardListener(self)
323 else: 453 else:
454 self.setDisplayContent()
324 if self.__single_line: 455 if self.__single_line:
325 self.removeFocusListener(self) 456 if self in self._focusListeners:
326 self.removeKeyboardListener(self) 457 self.removeFocusListener(self)
327 content = self.getContent() 458 if self in self._keyboardListeners:
328 if self.modified(content) and self.__callback: 459 self.removeKeyboardListener(self)
329 self.__callback(content) 460
461 def setDisplayContent(self):
462 self.setHTML(addURLToText(self._original_content['text']))
330 463
331 def setFocus(self, focus): 464 def setFocus(self, focus):
332 if focus: 465 if focus:
333 self.getElement().focus() 466 self.getElement().focus()
334 else: 467 else:
335 self.getElement().blur() 468 self.getElement().blur()
336 469
337 def onKeyDown(self, sender, keycode, modifiers): 470 def onKeyPress(self, sender, keycode, modifiers):
338 if keycode == KEY_ENTER: 471 if keycode == KEY_ENTER:
339 self.setFocus(False) 472 self.setFocus(False) # finish the edition
340 473
341 def onLostFocus(self, sender): 474 def onLostFocus(self, sender):
475 """Finish the edition when focus is lost"""
342 self.edit(False) 476 self.edit(False)