Mercurial > libervia-web
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 |