comparison browser_side/panels.py @ 351:c943fd54c90e

browser_side: heavy refactorisation for microblogs: - RichTextEditor inheritates from BaseTextEditor - stuff related to display/edition have been moved from MicroblogEntry to LightTextEditor and RichTextEditor. Now the editors has two modes for display/edition and the microblog bubble is actually the editor itself. - RichTextEditor's display mode uses a LightTextEditor (this will be used for WYSIWYG edition) - addressing stuff of RichTextEditor have been moved to a child class RichMessageEditor (used for the rich text editor when clicking on the button left to the unibox) - handle blog titles TODO: - fix encode/decode errors when sending special chars - fix images maximal width in the bubble - rich content WYSIWYG edition
author souliane <souliane@mailoo.org>
date Wed, 12 Feb 2014 15:17:04 +0100
parents f1b9ec412769
children f4efffb9627c
comparison
equal deleted inserted replaced
350:f1b9ec412769 351:c943fd54c90e
70 def refresh(self): 70 def refresh(self):
71 """Enable or disable this panel. Contained widgets are created when necessary.""" 71 """Enable or disable this panel. Contained widgets are created when necessary."""
72 enable = self.host.params_ui['unibox']['value'] 72 enable = self.host.params_ui['unibox']['value']
73 self.setVisible(enable) 73 self.setVisible(enable)
74 if enable and not self.unibox: 74 if enable and not self.unibox:
75 self.button = Button ('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>') 75 self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>')
76 self.button.setTitle('Open the rich text editor') 76 self.button.setTitle('Open the rich text editor')
77 self.button.addStyleName('uniBoxButton') 77 self.button.addStyleName('uniBoxButton')
78 self.add(self.button) 78 self.add(self.button)
79 self.unibox = UniBox(self.host) 79 self.unibox = UniBox(self.host)
80 self.add(self.unibox) 80 self.add(self.unibox)
81 self.setCellWidth(self.unibox, '100%') 81 self.setCellWidth(self.unibox, '100%')
82 self.button.addClickListener(self.openRichTextEditor) 82 self.button.addClickListener(self.openRichMessageEditor)
83 self.unibox.addKey("@@: ") 83 self.unibox.addKey("@@: ")
84 84
85 def openRichTextEditor(self): 85 def openRichMessageEditor(self):
86 """Open the rich text editor.""" 86 """Open the rich text editor."""
87 self.button.setVisible(False) 87 self.button.setVisible(False)
88 self.unibox.setVisible(False) 88 self.unibox.setVisible(False)
89 self.setCellWidth(self.unibox, '0px') 89 self.setCellWidth(self.unibox, '0px')
90 self.host.panel._contactsMove(self) 90 self.host.panel._contactsMove(self)
91 91
92 def onCloseCallback(): 92 def afterEditCb():
93 Window.removeWindowResizeListener(self) 93 Window.removeWindowResizeListener(self)
94 self.host.panel._contactsMove(self.host.panel._hpanel) 94 self.host.panel._contactsMove(self.host.panel._hpanel)
95 self.setCellWidth(self.unibox, '100%') 95 self.setCellWidth(self.unibox, '100%')
96 self.button.setVisible(True) 96 self.button.setVisible(True)
97 self.unibox.setVisible(True) 97 self.unibox.setVisible(True)
98 self.host.resize() 98 self.host.resize()
99 99
100 richtext.RichTextEditor.getOrCreate(self.host, self, onCloseCallback) 100 richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb)
101 Window.addWindowResizeListener(self) 101 Window.addWindowResizeListener(self)
102 self.host.resize() 102 self.host.resize()
103 103
104 def onWindowResized(self, width, height): 104 def onWindowResized(self, width, height):
105 right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() 105 right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth()
334 334
335 def __init__(self, data): 335 def __init__(self, data):
336 self.id = data['id'] 336 self.id = data['id']
337 self.type = data.get('type', 'main_item') 337 self.type = data.get('type', 'main_item')
338 self.empty = data.get('new', False) 338 self.empty = data.get('new', False)
339 self.title = data.get('title', '')
340 self.title_xhtml = data.get('title_xhtml', '')
339 self.content = data.get('content', '') 341 self.content = data.get('content', '')
340 self.xhtml = data.get('xhtml', '') 342 self.content_xhtml = data.get('content_xhtml', '')
341 self.author = data['author'] 343 self.author = data['author']
342 self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here 344 self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here
343 try: 345 try:
344 self.published = float(data['published']) # XXX: int doesn't work here 346 self.published = float(data['published']) # XXX: int doesn't work here
345 except KeyError: 347 except KeyError:
397 self.add(self.panel) 399 self.add(self.panel)
398 ClickHandler.__init__(self) 400 ClickHandler.__init__(self)
399 self.addClickListener(self) 401 self.addClickListener(self)
400 402
401 self.pub_data = (self.hash[0], self.hash[1], self.id) 403 self.pub_data = (self.hash[0], self.hash[1], self.id)
402 self._setContent() 404 self.__setContent()
403 405
404 def _setContent(self): 406 def __setContent(self):
405 """Actually set the entry content (header, icons, bubble...)""" 407 """Actually set the entry content (header, icons, bubble...)"""
406 self.delete_label = self.update_label = self.comment_label = None 408 self.delete_label = self.update_label = self.comment_label = None
407 self.bubble = self.editbox = self._current_comment = None 409 self.bubble = self._current_comment = None
408 self._setHeader() 410 self.__setHeader()
409 if self.empty: 411 self.__setBubble()
410 self.editable_content = ['', Const.SYNTAX_XHTML] 412 self.__setIcons()
411 else: 413
412 self.editable_content = [self.xhtml, Const.SYNTAX_XHTML] if self.xhtml else [self.content, None] 414 def __setHeader(self):
413 self.setEntryDialog()
414 self._setIcons()
415
416 def _setHeader(self):
417 """Set the entry header""" 415 """Set the entry header"""
418 if self.empty: 416 if self.empty:
419 return 417 return
420 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) 418 update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated)
421 self.header.setHTML("""<div class='mb_entry_header'> 419 self.header.setHTML("""<div class='mb_entry_header'>
425 'published': datetime.fromtimestamp(self.published), 423 'published': datetime.fromtimestamp(self.published),
426 'updated': update_text if self.published != self.updated else '' 424 'updated': update_text if self.published != self.updated else ''
427 } 425 }
428 ) 426 )
429 427
430 def _setIcons(self): 428 def __setIcons(self):
431 """Set the entry icons (delete, update, comment)""" 429 """Set the entry icons (delete, update, comment)"""
432 if self.empty: 430 if self.empty:
433 return 431 return
434 432
435 def addIcon(label, title): 433 def addIcon(label, title):
454 if sender == self: 452 if sender == self:
455 self._blog_panel.setSelectedEntry(self) 453 self._blog_panel.setSelectedEntry(self)
456 elif sender == self.delete_label: 454 elif sender == self.delete_label:
457 self._delete() 455 self._delete()
458 elif sender == self.update_label: 456 elif sender == self.update_label:
459 self.setEntryDialog(edit=True) 457 self.bubble.edit(True)
460 elif sender == self.comment_label: 458 elif sender == self.comment_label:
461 self._comment() 459 self._comment()
462 460
463 def onKeyUp(self, sender, keycode, modifiers): 461 def __modifiedCb(self, content):
464 """Update is done when ENTER key is pressed within the raw editbox""" 462 """Send the new content to the backend
465 if sender != self.editbox or not self.editbox.getVisible(): 463 @return: False to restore the original content if a deletion has been cancelled
466 return 464 """
467 if keycode == KEY_ENTER: 465 if not content['text']: # previous content has been emptied
468 self._updateContent() 466 self._delete(True)
469 467 return False
470 def onLostFocus(self, sender):
471 """Update is done when the focus leaves the raw editbox"""
472 if sender != self.editbox or not self.editbox.getVisible():
473 return
474 self._updateContent()
475
476 def _updateContent(self, cancel=False):
477 """Send the new content to the backend, remove the entry if it was
478 an empty one (used for creating a new blog post)"""
479 if not self.editbox or not self.editbox.getVisible():
480 return
481 self.entry_dialog.setWidth("auto")
482 self.entry_dialog.remove(self.edit_panel)
483 self.entry_dialog.add(self.bubble)
484 new_text = self.editbox.getText().strip()
485 self.edit_panel = self.editbox = None
486
487 def removeNewEntry():
488 if self.empty:
489 self._blog_panel.removeEntry(self.type, self.id)
490 if self.type == 'main_item': # restore the "New message" button
491 self._blog_panel.setUniBox(enable=False)
492 else: # allow to create a new comment
493 self._parent_entry._current_comment = None
494
495 if cancel or new_text == self.editable_content[0] or new_text == "":
496 removeNewEntry()
497 return
498 self.editable_content[0] = new_text
499 extra = {'published': str(self.published)} 468 extra = {'published': str(self.published)}
500 if self.empty or self.xhtml: 469 if self.empty or self.content_xhtml:
501 extra.update({'rich': new_text}) 470 # TODO: if the user change his parameters after the message edition started,
471 # the message syntax could be different then the current syntax: pass the
472 # message syntax in extra for the frontend to use it instead of current syntax.
473 extra.update({'content_rich': content['text'], 'title': content['title']})
502 if self.empty: 474 if self.empty:
503 if self.type == 'main_item': 475 if self.type == 'main_item':
504 self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, new_text, extra) 476 self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra)
505 else: 477 else:
506 self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, new_text, extra) 478 self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra)
507 else: 479 else:
508 self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, new_text, extra) 480 self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, content['text'], extra)
509 removeNewEntry() 481 return True
510 482
511 def setEntryDialog(self, edit=False): 483 def __afterEditCb(self, content):
512 """Set the bubble or the editor 484 """Remove the entry if it was an empty one (used for creating a new blog post).
513 @param edit: set to True to display the editor""" 485 Data for the actual new blog post will be received from the bridge"""
514 if edit: 486 if self.empty:
515 if self.editbox and self.editbox.getVisible(): 487 self._blog_panel.removeEntry(self.type, self.id)
516 self.editbox.setFocus(True) 488 if self.type == 'main_item': # restore the "New message" button
517 return 489 self._blog_panel.refresh()
518 if self.empty or self.xhtml: 490 else: # allow to create a new comment
519 def cb(result): 491 self._parent_entry._current_comment = None
520 self._updateContent(result == richtext.CANCEL) 492
521 493 def __setBubble(self):
522 options = ['no_recipient', 'no_sync_unibox', 'no_style', 'no_close'] 494 """Set the bubble displaying the initial content."""
523 if not self.empty: 495 content = {'text': self.content_xhtml if self.content_xhtml else self.content,
524 options.append('update_msg') 496 'title': self.title_xhtml if self.title_xhtml else self.title}
525 editor = richtext.RichTextEditor(self._blog_panel.host, self.panel, cb, options=options) 497 if self.empty or self.content_xhtml: # new message and rich text message
526 editor.setWidth('100%') 498 content.update({'syntax': Const.SYNTAX_XHTML})
527 self.editbox = editor.textarea 499 if self.author != self._blog_panel.host.whoami.bare:
528 editor.setVisible(True) # needed to build the toolbar 500 options = ['read_only']
529 if self.editable_content[0]:
530 self._blog_panel.host.bridge.call('syntaxConvert', lambda d: self._setOriginalText(d, editor),
531 self.editable_content[0], self.editable_content[1])
532 else:
533 self._setOriginalText("", editor)
534 else: 501 else:
535 if not self.editbox: 502 options = [] if self.empty else ['update_msg']
536 self.editbox = TextArea() 503 self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options)
537 self.editbox.addFocusListener(self) 504 else: # assume raw text message have no title
538 self.editbox.addKeyboardListener(self) 505 self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, True)
539 self._setOriginalText(self.editable_content[0], self.editbox) 506 self.bubble.setStyleName("bubble")
540 else: 507 self.entry_dialog.add(self.bubble)
541 if not self.bubble: 508 self.bubble.edit(False)
542 self.bubble = HTML() 509
543 self.bubble.setStyleName("bubble") 510 def _delete(self, empty=False):
544 511 """Ask confirmation for deletion.
545 self.bubble.setHTML(addURLToText(html_sanitize(self.content)) if not self.xhtml else self.xhtml) 512 @return: False if the deletion has been cancelled."""
546 self.entry_dialog.add(self.bubble)
547
548 def _setOriginalText(self, text, container):
549 """Set the original text to be modified in the editor"""
550 text = text.strip()
551 container.original_text = text
552 self.editbox.setWidth('100%')
553 self.editbox.setText(text)
554 panel = SimplePanel()
555 panel.add(container)
556 panel.setStyleName("bubble")
557 panel.addStyleName('bubble-editbox')
558 if self.bubble:
559 self.entry_dialog.remove(self.bubble)
560 self.entry_dialog.add(panel)
561 self.editbox.setFocus(True)
562 if text:
563 self.editbox.setSelectionRange(len(text), 0)
564 self.edit_panel = panel
565 self.editable_content = [text, container.format if isinstance(container, richtext.RichTextEditor) else None]
566
567 def _delete(self):
568 """Ask confirmation for deletion"""
569 def confirm_cb(answer): 513 def confirm_cb(answer):
570 if answer: 514 if answer:
571 self._blog_panel.host.bridge.call('deleteMblog', None, self.pub_data, self.comments) 515 self._blog_panel.host.bridge.call('deleteMblog', None, self.pub_data, self.comments)
572 516 else: # restore the text if it has been emptied during the edition
573 target = 'message and all its comments' if self.comments else 'comment' 517 self.bubble.setContent(self.bubble._original_content)
574 _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete this %s?" % target) 518
575 _dialog.show() 519 if self.empty:
520 text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.")
521 dialog.InfoDialog(_("Information"), text).show()
522 return
523 text = ""
524 if empty:
525 text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>")
526 target = _('message and all its comments') if self.comments else _('comment')
527 text += _("Do you really want to delete this %s?") % target
528 dialog.ConfirmDialog(confirm_cb, text=text).show()
576 529
577 def _comment(self): 530 def _comment(self):
578 """Add an empty entry for a new comment""" 531 """Add an empty entry for a new comment"""
579 if self._current_comment: 532 if self._current_comment:
580 self._current_comment.editbox.setFocus(True) 533 self._current_comment.bubble.setFocus(True)
581 return 534 return
582 data = {'id': str(time()), 535 data = {'id': str(time()),
583 'new': True, 536 'new': True,
584 'type': 'comment', 537 'type': 'comment',
585 'author': self._blog_panel.host.whoami.bare, 538 'author': self._blog_panel.host.whoami.bare,
587 'node': self.node 540 'node': self.node
588 } 541 }
589 entry = self._blog_panel.addEntry(data) 542 entry = self._blog_panel.addEntry(data)
590 entry._parent_entry = self 543 entry._parent_entry = self
591 self._current_comment = entry 544 self._current_comment = entry
592 entry.setEntryDialog(edit=True) 545 entry.bubble.edit(True)
593 546
594 547
595 class MicroblogPanel(base_widget.LiberviaWidget): 548 class MicroblogPanel(base_widget.LiberviaWidget):
596 warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" 549 warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know"
597 warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" 550 warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>"
623 data = {'id': str(time()), 576 data = {'id': str(time()),
624 'new': True, 577 'new': True,
625 'author': self.host.whoami.bare, 578 'author': self.host.whoami.bare,
626 } 579 }
627 entry = self.addEntry(data) 580 entry = self.addEntry(data)
628 entry.setEntryDialog(edit=True) 581 entry.bubble.edit(True)
629 self.new_button = Button("New message", listener=addBox) 582 self.new_button = Button("New message", listener=addBox)
630 self.new_button.setStyleName("microblogNewButton") 583 self.new_button.setStyleName("microblogNewButton")
631 self.vpanel.insert(self.new_button, 0) 584 self.vpanel.insert(self.new_button, 0)
632 585
633 @classmethod 586 @classmethod