Mercurial > libervia-web
view browser_side/panels.py @ 319:462a0a8894e3
browser_side: display the good text for microblog button regarding the context: "Send message" or "Update"
author | souliane <souliane@mailoo.org> |
---|---|
date | Fri, 03 Jan 2014 16:06:00 +0100 |
parents | 09205b410a53 |
children | 971e3812903a |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- """ Libervia: a Salut à Toi frontend Copyright (C) 2011, 2012, 2013 Jérôme Poisson <goffi@goffi.org> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ import pyjd # this is dummy in pyjs from pyjamas.ui.SimplePanel import SimplePanel from pyjamas.ui.AbsolutePanel import AbsolutePanel from pyjamas.ui.VerticalPanel import VerticalPanel from pyjamas.ui.HorizontalPanel import HorizontalPanel from pyjamas.ui.HTMLPanel import HTMLPanel from pyjamas.ui.Frame import Frame from pyjamas.ui.TextArea import TextArea from pyjamas.ui.Label import Label from pyjamas.ui.Button import Button from pyjamas.ui.HTML import HTML from pyjamas.ui.Image import Image from pyjamas.ui.PopupPanel import PopupPanel from pyjamas.ui.StackPanel import StackPanel from pyjamas.ui.ClickListener import ClickHandler from pyjamas.ui.FlowPanel import FlowPanel from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT from pyjamas.ui.MouseListener import MouseHandler from pyjamas.Timer import Timer from pyjamas import DOM from card_game import CardPanel from radiocol import RadioColPanel from menu import Menu from jid import JID from tools import html_sanitize, inlineRoot, setPresenceStyle from sat_frontends.tools.strings import addURLToText from datetime import datetime from time import time import dialog import base_widget from dialog import ConfirmDialog import richtext from plugin_xep_0085 import ChatStateMachine from pyjamas import Window from __pyjamas__ import doc from sat_frontends.tools.games import SYMBOLS from sat_frontends import constants from pyjamas.ui.FocusListener import FocusHandler import logging const = constants.Const # to directly import 'const' doesn't work class UniBoxPanel(HorizontalPanel): """Panel containing the UniBox""" def __init__(self, host): HorizontalPanel.__init__(self) self.host = host self.setStyleName('uniBoxPanel') self.unibox = None def setUniBox(self, enable): """Enable or disable the unibox widget. @param enable: boolean @return: UniBox instance or None if disabled """ if enable: self.button = Button ('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>') self.button.setTitle('Open the rich text editor') self.button.addStyleName('uniBoxButton') self.add(self.button) self.unibox = UniBox(self.host) self.add(self.unibox) self.setCellWidth(self.unibox, '100%') self.button.addClickListener(self.openRichTextEditor) self.unibox.addKey("@@: ") return self.unibox else: if self.unibox: self.remove(self.unibox) self.unibox = None return None def openRichTextEditor(self): """Open the rich text editor.""" self.button.setVisible(False) self.unibox.setVisible(False) self.setCellWidth(self.unibox, '0px') self.host.panel._contactsMove(self) def onCloseCallback(): Window.removeWindowResizeListener(self) self.host.panel._contactsMove(self.host.panel._hpanel) self.setCellWidth(self.unibox, '100%') self.button.setVisible(True) self.unibox.setVisible(True) self.host.resize() richtext.RichTextEditor.getOrCreate(self.host, self, onCloseCallback) Window.addWindowResizeListener(self) self.host.resize() def onWindowResized(self, width, height): right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() ideal_width = right - left - 40 self.host.richtext.setWidth("%spx" % ideal_width) class MessageBox(TextArea): """A basic text area for entering messages""" def __init__(self, host): TextArea.__init__(self) self.host = host self.__size = (0, 0) self.setStyleName('messageBox') self.addKeyboardListener(self) MouseHandler.__init__(self) self.addMouseListener(self) self._selected_cache = None def onBrowserEvent(self, event): # XXX: woraroung a pyjamas bug: self.currentEvent is not set # so the TextBox's cancelKey doens't work. This is a workaround # FIXME: fix the bug upstream self.currentEvent = event TextArea.onBrowserEvent(self, event) def onKeyPress(self, sender, keycode, modifiers): _txt = self.getText() def history_cb(text): self.setText(text) Timer(5, lambda: self.setCursorPos(len(text))) if keycode == KEY_ENTER: if _txt: self._selected_cache.onTextEntered(_txt) self.host._updateInputHistory(_txt) self.setText('') sender.cancelKey() elif keycode == KEY_UP: self.host._updateInputHistory(_txt, -1, history_cb) elif keycode == KEY_DOWN: self.host._updateInputHistory(_txt, +1, history_cb) else: self.__onComposing() def __onComposing(self): """Callback when the user is composing a text.""" if hasattr(self._selected_cache, "target"): self._selected_cache.state_machine._onEvent("composing") def onMouseUp(self, sender, x, y): size = (self.getOffsetWidth(), self.getOffsetHeight()) if size != self.__size: self.__size = size self.host.resize() def onSelectedChange(self, selected): self._selected_cache = selected class UniBox(MessageBox, MouseHandler): #AutoCompleteTextBox): """This text box is used as a main typing point, for message, microblog, etc""" def __init__(self, host): MessageBox.__init__(self, host) #AutoCompleteTextBox.__init__(self) self.setStyleName('uniBox') self._popup = None self._timer = Timer(notify=self._timeCb) host.addSelectedListener(self.onSelectedChange) def addKey(self, key): return #self.getCompletionItems().completions.append(key) def removeKey(self, key): return # TODO: investigate why AutoCompleteTextBox doesn't work here, # maybe it can work on a TextBox but no TextArea. Remove addKey # and removeKey methods if they don't serve anymore. try: self.getCompletionItems().completions.remove(key) except KeyError: print "WARNING: trying to remove an unknown key" def showWarning(self, target_data): target_hook, _type, msg = target_data if _type == "NONE": return if not msg: print "WARNING: no msg set uniBox warning" return if _type == "PUBLIC": style = "targetPublic" elif _type == "GROUP": style = "targetGroup" elif _type == "STATUS": msg = "This will be your new status message" style = "targetStatus" elif _type == "ONE2ONE": style = "targetOne2One" else: print "ERROR: unknown message type" return contents = HTML(msg) self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) self._popup.target_data = target_data self._popup.add(contents) self._popup.setStyleName("warningPopup") if style: self._popup.addStyleName(style) left = 0 top = 0 # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2) self._popup.setPopupPosition(left, top) self._popup.show() def _timeCb(self, timer): if self._popup: self._popup.hide() del self._popup self._popup = None def _getTarget(self, txt): """ Say who will receive the messsage @return: a tuple (selected, target_type, target info) with: - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None)) - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC - msg: HTML message which will appear in the privacy warning banner """ target = self._selected_cache def getSelectedOrStatus(): if target and target.isSelectable(): _type, msg = target.getWarningData() target_hook = None # we use the selected widget, not a hook else: _type, msg = "STATUS", "This will be your new status message" target_hook = (txt, None) return (target_hook, _type, msg) if not txt.startswith('@'): target_hook, _type, msg = getSelectedOrStatus() elif txt.startswith('@@: '): _type = "PUBLIC" msg = MicroblogPanel.warning_msg_public target_hook = (txt[4:], None) elif txt.startswith('@'): _end = txt.find(': ') if _end == -1: target_hook, _type, msg = getSelectedOrStatus() else: group = txt[1:_end] # only one target group is managed for the moment if not group or not group in self.host.contact_panel.getGroups(): # the group doesn't exists, we ignore the key group = None target_hook, _type, msg = getSelectedOrStatus() else: _type = "GROUP" msg = MicroblogPanel.warning_msg_group % group target_hook = (txt[_end + 2:], group) else: print "ERROR: Unknown target" target_hook, _type, msg = getSelectedOrStatus() return (target_hook, _type, msg) def onKeyPress(self, sender, keycode, modifiers): _txt = self.getText() target = self._getTarget(_txt) if not self._popup: self.showWarning(target) elif target != self._popup.target_data: self._timeCb(None) # we remove the popup self.showWarning(target) self._timer.schedule(2000) if keycode == KEY_ENTER: if _txt: target_hook, type_, msg = target if target_hook: parsed_txt, data = target_hook self.host.send([(type_, data)], parsed_txt) self.host._updateInputHistory(_txt) self.setText('') self._timeCb(None) # we remove the popup MessageBox.onKeyPress(self, sender, keycode, modifiers) def getTargetAndData(self): """For external use, to get information about the (hypothetical) message that would be sent if we press Enter right now in the unibox. @return a tuple (target, data) with: - data: what would be the content of the message (body) - target: JID, group with the prefix "@" or the public entity "@@" """ _txt = self.getText() target_hook, _type, _msg = self._getTarget(_txt) if target_hook: data, target = target_hook if target is None: return target_hook return (data, "@%s" % (target if target != "" else "@")) if isinstance(self._selected_cache, MicroblogPanel): groups = self._selected_cache.accepted_groups target = "@%s" % (groups[0] if len(groups) > 0 else "@") if len(groups) > 1: Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0]) elif isinstance(self._selected_cache, ChatPanel): target = self._selected_cache.target else: target = None return (_txt, target) def onWidgetClosed(self, lib_wid): """Called when a libervia widget is closed""" if self._selected_cache == lib_wid: self.onSelectedChange(None) """def complete(self): #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this return AutoCompleteTextBox.complete(self)""" class MicroblogItem(): # XXX: should be moved in a separated module def __init__(self, data): self.id = data['id'] self.type = data.get('type', 'main_item') self.empty = data.get('new', False) self.content = data.get('content', '') self.xhtml = data.get('xhtml', '') self.author = data['author'] self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here try: self.published = float(data['published']) # XXX: int doesn't work here except KeyError: self.published = self.updated self.comments = data.get('comments', False) if self.empty and self.type == 'main_item': self.service = self.node = None self.hash = (self.service, self.node) else: try: self.service = data['comments_service'] if self.comments else data['service'] self.node = data['comments_node'] if self.comments else data['node'] self.hash = (self.service, self.node) except KeyError: logging.error("Warning: can't manage item [%s] some keys are missing in microblog data (%s)" % (self.id, data.keys())) self.comments = False class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): def __init__(self, blog_panel, data={}): """ @param blog_panel: the parent panel @param data: dict containing the blog item data, or a MicroblogItem instance. """ if isinstance(data, MicroblogItem): self._base_item = data else: self._base_item = MicroblogItem(data) SimplePanel.__init__(self) self._blog_panel = blog_panel self.panel = FlowPanel() self.panel.setStyleName('mb_entry') self.header = HTMLPanel('') self.panel.add(self.header) self.entry_actions = VerticalPanel() self.entry_actions.setStyleName('mb_entry_actions') self.panel.add(self.entry_actions) entry_avatar = SimplePanel() entry_avatar.setStyleName('mb_entry_avatar') self.avatar = Image(self._blog_panel.host.getAvatar(self.author)) entry_avatar.add(self.avatar) self.panel.add(entry_avatar) self.entry_dialog = SimplePanel() self.entry_dialog.setStyleName('mb_entry_dialog') self.panel.add(self.entry_dialog) self.add(self.panel) ClickHandler.__init__(self) self.addClickListener(self) self.pub_data = (self.hash[0], self.hash[1], self.id) self._setContent() def __getattr__(self, name): """This allows to directly use the attributes of MicroblogItem""" if hasattr(self, name): return self.__dict__[name] else: return getattr(self._base_item, name) def _setContent(self): """Actually set the entry content (header, icons, bubble...)""" self.delete_label = self.update_label = self.comment_label = None self.bubble = self.editbox = self._current_comment = None self._setHeader() if self.empty: self.editable_content = ['', const.SYNTAX_XHTML] else: self.editable_content = [self.xhtml, const.SYNTAX_XHTML] if self.xhtml else [self.content, None] self.setEntryDialog() self._setIcons() def _setHeader(self): """Set the entry header""" if self.empty: return update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) self.header.setHTML("""<div class='mb_entry_header'> <span class='mb_entry_author'>%(author)s</span> on <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s </div>""" % {'author': html_sanitize(self.author), 'published': datetime.fromtimestamp(self.published), 'updated': update_text if self.published != self.updated else '' } ) def _setIcons(self): """Set the entry icons (delete, update, comment)""" if self.empty: return def addIcon(label, title): label = Label(label) label.setTitle(title) label.addClickListener(self) self.entry_actions.add(label) return label if self.author == self._blog_panel.host.whoami.bare: self.delete_label = addIcon(u"✗", "Delete this message") self.update_label = addIcon(u"✍", "Edit this message") if self.comments: self.comment_label = addIcon(u"↶", "Comment this message") def updateAvatar(self, new_avatar): """Change the avatar of the entry @param new_avatar: path to the new image""" self.avatar.setUrl(new_avatar) def onClick(self, sender): if sender == self: self._blog_panel.setSelectedEntry(self if self.comments else None) elif sender == self.delete_label: self._delete() elif sender == self.update_label: self.setEntryDialog(edit=True) elif sender == self.comment_label: self._comment() def onKeyUp(self, sender, keycode, modifiers): """Update is done when ENTER key is pressed within the raw editbox""" if sender != self.editbox or not self.editbox.getVisible(): return if keycode == KEY_ENTER: self._updateContent() def onLostFocus(self, sender): """Update is done when the focus leaves the raw editbox""" if sender != self.editbox or not self.editbox.getVisible(): return self._updateContent() def _updateContent(self, cancel=False): """Send the new content to the backend, remove the entry if it was an empty one (used for creating a new blog post)""" if not self.editbox or not self.editbox.getVisible(): return self.entry_dialog.setWidth("auto") self.entry_dialog.remove(self.edit_panel) self.entry_dialog.add(self.bubble) new_text = self.editbox.getText().strip() self.edit_panel = self.editbox = None def removeNewEntry(): if self.empty: self._blog_panel.removeEntry(self.type, self.id) if self.type == 'main_item': # restore the "New message" button self._blog_panel.setUniBox(enable=False) else: # allow to create a new comment self._parent_entry._current_comment = None if cancel or new_text == self.editable_content[0] or new_text == "": removeNewEntry() return self.editable_content[0] = new_text extra = {'published': str(self.published)} if self.empty or self.xhtml: extra.update({'rich': new_text}) if self.empty: if self.type == 'main_item': self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, new_text, extra) else: self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, new_text, extra) else: self._blog_panel.host.bridge.call('updateMblog', None, self.pub_data, self.comments, new_text, extra) removeNewEntry() def setEntryDialog(self, edit=False): """Set the bubble or the editor @param edit: set to True to display the editor""" if edit: if self.editbox and self.editbox.getVisible(): self.editbox.setFocus(True) return if self.empty or self.xhtml: def cb(result): self._updateContent(result == richtext.CANCEL) options = ['no_recipient', 'no_sync_unibox', 'no_style', 'no_close'] if not self.empty: options.append('update_msg') editor = richtext.RichTextEditor(self._blog_panel.host, self.panel, cb, options=options) editor.setWidth('100%') self.editbox = editor.textarea editor.setVisible(True) # needed to build the toolbar if self.editable_content[0]: self._blog_panel.host.bridge.call('syntaxConvert', lambda d: self._setOriginalText(d, editor), self.editable_content[0], self.editable_content[1]) else: self._setOriginalText("", editor) else: if not self.editbox: self.editbox = TextArea() self.editbox.addFocusListener(self) self.editbox.addKeyboardListener(self) self._setOriginalText(self.editable_content[0], self.editbox) else: if not self.bubble: self.bubble = HTML() self.bubble.setStyleName("bubble") self.bubble.setHTML(addURLToText(html_sanitize(self.content)) if not self.xhtml else self.xhtml) self.entry_dialog.add(self.bubble) def _setOriginalText(self, text, container): """Set the original text to be modified in the editor""" text = text.strip() container.original_text = text self.editbox.setWidth('100%') self.editbox.setText(text) panel = SimplePanel() panel.add(container) panel.setStyleName("bubble") panel.addStyleName('bubble-editbox') if self.bubble: self.entry_dialog.remove(self.bubble) self.entry_dialog.add(panel) self.editbox.setFocus(True) if text: self.editbox.setSelectionRange(len(text), 0) self.edit_panel = panel self.editable_content = [text, container.format if isinstance(container, richtext.RichTextEditor) else None] def _delete(self): """Ask confirmation for deletion""" def confirm_cb(answer): if answer: self._blog_panel.host.bridge.call('deleteMblog', None, self.pub_data, self.comments) target = 'message and all its comments' if self.comments else 'comment' _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete this %s?" % target) _dialog.show() def _comment(self): """Add an empty entry for a new comment""" if self._current_comment: self._current_comment.editbox.setFocus(True) return data = {'id': str(time()), 'new': True, 'type': 'comment', 'author': self._blog_panel.host.whoami.bare, 'service': self.service, 'node': self.node } entry = self._blog_panel.addEntry(data) entry._parent_entry = self self._current_comment = entry entry.setEntryDialog(edit=True) class MicroblogPanel(base_widget.LiberviaWidget): warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" def __init__(self, host, accepted_groups): """Panel used to show microblog @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts """ base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) self.setAcceptedGroup(accepted_groups) self.host = host self.entries = {} self.comments = {} self.selected_entry = None self.vpanel = VerticalPanel() self.vpanel.setStyleName('microblogPanel') self.setWidget(self.vpanel) self.setUniBox(self.host.uni_box) def setUniBox(self, enable=False): """Enable or disable the unibox. If it is disabled, display the 'New message' button on top of the panel""" if enable: return if hasattr(self, 'new_button'): self.new_button.setVisible(True) else: def addBox(): self.new_button.setVisible(False) data = {'id': str(time()), 'new': True, 'author': self.host.whoami.bare, } entry = self.addEntry(data) entry.setEntryDialog(edit=True) self.new_button = Button("New message", listener=addBox) self.new_button.setStyleName("microblogNewButton") self.vpanel.insert(self.new_button, 0) @classmethod def registerClass(cls): base_widget.LiberviaWidget.addDropKey("GROUP", cls.createPanel) base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMetaPanel) @classmethod def createPanel(cls, host, item): """Generic panel creation for one, several or all groups (meta). @parem host: the SatWebFrontend instance @param item: single group as a string, list of groups (as an array) or None (for the meta group = "all groups") @return: the created MicroblogPanel """ _items = item if isinstance(item, list) else ([] if item is None else [item]) _type = 'ALL' if _items == [] else 'GROUP' # XXX: pyjamas doesn't support use of cls directly _new_panel = MicroblogPanel(host, _items) host.FillMicroblogPanel(_new_panel) host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10) host.setSelected(_new_panel) return _new_panel @classmethod def createMetaPanel(cls, host, item): """Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group""" return MicroblogPanel.createPanel(host, None) @property def accepted_groups(self): return self._accepted_groups def matchEntity(self, entity): """ @param entity: single group as a string, list of groups (as an array) or None (for the meta group = "all groups") @return: True if self matches the given entity """ entity = entity if isinstance(entity, list) else ([] if entity is None else [entity]) entity.sort() # sort() do not return the sorted list: do it here, not on the "return" line return self.accepted_groups == entity def getWarningData(self): if self.selected_entry: if not self.selected_entry.comments: print ("ERROR: an item without comment is selected") return ("NONE", None) return ("PUBLIC", "This is a <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") elif not self._accepted_groups: # we have a meta MicroblogPanel, we publish publicly return ("PUBLIC", self.warning_msg_public) else: # we only accept one group at the moment # FIXME: manage several groups return ("GROUP", self.warning_msg_group % self._accepted_groups[0]) def onTextEntered(self, text): if self.selected_entry: # we are entering a comment comments_node = self.selected_entry.comments if not comments_node: raise Exception("ERROR: comments node is empty") target = ("COMMENT", comments_node) elif not self._accepted_groups: # we are entering a public microblog target = ("PUBLIC", None) else: # we are entering a microblog restricted to a group # FIXME: manage several groups target = ("GROUP", self._accepted_groups[0]) self.host.send([target], text) def accept_all(self): return not self._accepted_groups # we accept every microblog only if we are not filtering by groups def getEntries(self): """Ask all the entries for the currenly accepted groups, and fill the panel""" def massiveInsert(self, mblogs): """Insert several microblogs at once @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs """ print "Massive insertion of %d microblogs" % len(mblogs) for publisher in mblogs: print "adding blogs for [%s]" % publisher for mblog in mblogs[publisher]: if not "content" in mblog: print ("WARNING: No content found in microblog [%s]", mblog) continue self.addEntry(mblog) def mblogsInsert(self, mblogs): """ Insert several microblogs at once @param mblogs: list of microblogs """ for mblog in mblogs: if not "content" in mblog: print ("WARNING: No content found in microblog [%s]", mblog) continue self.addEntry(mblog) def _chronoInsert(self, vpanel, entry, reverse=True): """ Insert an entry in chronological order @param vpanel: VerticalPanel instance @param entry: MicroblogEntry @param reverse: more recent entry on top if True, chronological order else""" if entry.empty: entry.published = time() # we look for the right index to insert our entry: # if reversed, we insert the entry above the first entry # in the past idx = 0 for child in vpanel.children: if not isinstance(child, MicroblogEntry): idx += 1 continue if reverse: if child.published < entry.published: break else: if child.published > entry.published: break idx += 1 vpanel.insert(entry, idx) def addEntry(self, data): """Add an entry to the panel @param data: dict containing the item data @return: the added entry, or None """ _entry = MicroblogEntry(self, data) if _entry.type == "comment": if not _entry.hash in self.comments: # The comments node is not known in this panel return None parent = self.comments[_entry.hash] parent_idx = self.vpanel.getWidgetIndex(parent) # we find or create the panel where the comment must be inserted try: sub_panel = self.vpanel.getWidget(parent_idx + 1) except IndexError: sub_panel = None if not sub_panel or not isinstance(sub_panel, VerticalPanel): sub_panel = VerticalPanel() sub_panel.setStyleName('microblogPanel') sub_panel.addStyleName('subPanel') self.vpanel.insert(sub_panel, parent_idx + 1) for idx in xrange(0, len(sub_panel.getChildren())): comment = sub_panel.getIndexedChild(idx) if comment.pub_data[2] == _entry.id: # update an existing comment sub_panel.remove(comment) sub_panel.insert(_entry, idx) return _entry # we want comments to be inserted in chronological order self._chronoInsert(sub_panel, _entry, reverse=False) return _entry if _entry.id in self.entries: # update idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) self.vpanel.remove(self.entries[_entry.id]) self.vpanel.insert(_entry, idx) else: # new entry self._chronoInsert(self.vpanel, _entry) self.entries[_entry.id] = _entry if _entry.comments: # entry has comments, we keep the comment node as a reference self.comments[_entry.hash] = _entry self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.service, _entry.node) return _entry def removeEntry(self, type_, id_): """Remove an entry from the panel @param type_: entry type ('main_item' or 'comment') @param id_: entry id """ for child in self.vpanel.getChildren(): if isinstance(child, MicroblogEntry) and type_ == 'main_item': if child.pub_data[2] == id_: main_idx = self.vpanel.getWidgetIndex(child) try: sub_panel = self.vpanel.getWidget(main_idx + 1) if isinstance(sub_panel, VerticalPanel): sub_panel.removeFromParent() except IndexError: pass child.removeFromParent() self.selected_entry = None break elif isinstance(child, VerticalPanel) and type_ == 'comment': for comment in child.getChildren(): if comment.pub_data[2] == id_: comment.removeFromParent() self.selected_entry = None break def setSelectedEntry(self, entry): if self.selected_entry == entry: entry = None if self.selected_entry: self.selected_entry.removeStyleName('selected_entry') if entry: print "microblog entry selected (author=%s)" % entry.author entry.addStyleName('selected_entry') self.selected_entry = entry def updateValue(self, type_, jid, value): """Update a jid value in entries @param type_: one of 'avatar', 'nick' @param jid: jid concerned @param value: new value""" def updateVPanel(vpanel): for child in vpanel.children: if isinstance(child, MicroblogEntry) and child.author == jid: child.updateAvatar(value) elif isinstance(child, VerticalPanel): updateVPanel(child) if type_ == 'avatar': updateVPanel(self.vpanel) def setAcceptedGroup(self, group): """Add one or more group(s) which can be displayed in this panel. Prevent from duplicate values and keep the list sorted. @param group: string of the group, or list of string """ if not hasattr(self, "_accepted_groups"): self._accepted_groups = [] groups = group if isinstance(group, list) else [group] for _group in groups: if _group not in self._accepted_groups: self._accepted_groups.append(_group) self._accepted_groups.sort() def isJidAccepted(self, jid): """Tell if a jid is actepted and shown in this panel @param jid: jid @return: True if the jid is accepted""" if self.accept_all(): return True for group in self._accepted_groups: if self.host.contact_panel.isContactInGroup(group, jid): return True return False class StatusPanel(HTMLPanel, ClickHandler, FocusHandler, KeyboardHandler): EMPTY_STATUS = '<click to set a status>' def __init__(self, host, status=''): self.host = host self.status = status HTMLPanel.__init__(self, self.__getContent()) self.setStyleName('statusPanel') ClickHandler.__init__(self) FocusHandler.__init__(self) KeyboardHandler.__init__(self) self.addClickListener(self) def __getContent(self): return "<span class='status'>%(status)s</span>" % {'status': html_sanitize(self.status or self.EMPTY_STATUS)} def changeStatus(self, new_status): self.status = new_status self.setHTML(self.__getContent()) def onClick(self, sender): self.textarea = TextArea() self.textarea.setStyleName('status-edit') self.textarea.setText(self.status) self.textarea.addKeyboardListener(self) self.setVisible(False) self.parent.insert(self.textarea, self.parent.getWidgetIndex(self)) self.textarea.setFocus(True) self.textarea.setSelectionRange(0, len(self.status)) self.textarea.addFocusListener(self) self.textarea.addKeyboardListener(self) def onKeyPress(self, sender, keycode, modifiers): text = self.textarea.getText() if keycode == KEY_ENTER: if text != self.status: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, text) self.parent.remove(self.textarea) self.setVisible(True) def onLostFocus(self, sender): FocusHandler.onLostFocus(self, sender) if sender == self.textarea: self.onKeyPress(self, KEY_ENTER, None) class PresenceStatusPanel(HorizontalPanel, ClickHandler): def __init__(self, host, presence="", status=""): self.host = host HorizontalPanel.__init__(self, Width='100%') self.presence_button = Label(u"◉") self.presence_button.setStyleName("presence-button") self.status_panel = StatusPanel(host, status=status) self.setPresence(presence) entries = {} for value in const.PRESENCE.keys(): entries.update({const.PRESENCE[value]: {"value": value}}) def callback(sender, key): self.setPresence(entries[key]["value"]) # order matters self.host.send([("STATUS", None)], self.status_panel.status) self.presence_list = PopupMenuPanel(entries, callback=callback, style={"menu": "gwt-ListBox"}) self.presence_list.registerClickSender(self.presence_button) panel = HorizontalPanel() panel.add(self.presence_button) panel.add(self.status_panel) panel.setCellVerticalAlignment(self.presence_button, 'baseline') panel.setCellVerticalAlignment(self.status_panel, 'baseline') panel.setStyleName("marginAuto") self.add(panel) ClickHandler.__init__(self) self.addClickListener(self) def getPresence(self): return self.presence def setPresence(self, presence): status = self.status_panel.status if not status.strip() or status == " " or (self.presence in const.PRESENCE and status == const.PRESENCE[self.presence]): self.changeStatus(const.PRESENCE[presence]) self.presence = presence setPresenceStyle(self.presence_button, self.presence) def changeStatus(self, new_status): self.status_panel.changeStatus(new_status) def onClick(self, sender): # As status is the default target of uniBar, we don't want to select anything if click on it self.host.setSelected(None) class ChatText(HTMLPanel): def __init__(self, timestamp, nick, mymess, msg, xhtml = None): _date = datetime.fromtimestamp(float(timestamp or time())) _msg_class = ["chat_text_msg"] if mymess: _msg_class.append("chat_text_mymess") 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>" % {"timestamp": _date.strftime("%H:%M"), "nick": "[%s]" % html_sanitize(nick), "msg_class": ' '.join(_msg_class), "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)} #FIXME: images and external links must be removed according to preferences ) self.setStyleName('chatText') class Occupant(HTML): """Occupant of a MUC room""" def __init__(self, nick, special=""): HTML.__init__(self) self.nick = nick self.special = special self._refresh() def __str__(self): return self.nick def addSpecial(self, special=""): if special not in self.special: self.special += special self._refresh() def _refresh(self): special = "" if len(self.special) == 0 else " %s" % self.special self.setHTML("<div class='occupant'>%s%s</div>" % (html_sanitize(self.nick), special)) class OccupantsList(AbsolutePanel): """Panel user to show occupants of a room""" def __init__(self): AbsolutePanel.__init__(self) self.occupants_list = {} self.setStyleName('occupantsList') def addOccupant(self, nick): _occupant = Occupant(nick) self.occupants_list[nick] = _occupant self.add(_occupant) def removeOccupant(self, nick): try: self.remove(self.occupants_list[nick]) except KeyError: print "ERROR: trying to remove an unexisting nick" def clear(self): self.occupants_list.clear() AbsolutePanel.clear(self) def addSpecials(self, occupants=[], html=""): index = 0 special = html for occupant in occupants: if occupant in self.occupants_list.keys(): if isinstance(html, list): special = html[index] index = (index + 1) % len(html) self.occupants_list[occupant].addSpecial(special) class ChatPanel(base_widget.LiberviaWidget): def __init__(self, host, target, type_='one2one'): """Panel used for conversation (one 2 one or group chat) @param host: SatWebFrontend instance @param target: entity (JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room) @param type: one2one for simple conversation, group for MUC""" base_widget.LiberviaWidget.__init__(self, host, target.bare, selectable=True) self.vpanel = VerticalPanel() self.vpanel.setSize('100%', '100%') self.type = type_ self.nick = None if not target: print "ERROR: Empty target !" return self.target = target self.__body = AbsolutePanel() self.__body.setStyleName('chatPanel_body') chat_area = HorizontalPanel() chat_area.setStyleName('chatArea') if type_ == 'group': self.occupants_list = OccupantsList() chat_area.add(self.occupants_list) self.__body.add(chat_area) self.content = AbsolutePanel() self.content.setStyleName('chatContent') self.content_scroll = base_widget.ScrollPanelWrapper(self.content) chat_area.add(self.content_scroll) chat_area.setCellWidth(self.content_scroll, '100%') self.vpanel.add(self.__body) self.vpanel.setCellHeight(self.__body, '100%') self.addStyleName('chatPanel') self.setWidget(self.vpanel) self.state_machine = ChatStateMachine(self.host, str(self.target)) self.setUniBox(self.host.uni_box) def setUniBox(self, enable): """Enable or disable the unibox. If it is disabled, display a message box below the panel.""" if enable: return message_box = MessageBox(self.host) message_box.onSelectedChange(self) self.vpanel.add(message_box) """def doDetachChildren(self): #We need to force the use of a panel subclass method here, #for the same reason as doAttachChildren base_widget.ScrollPanelWrapper.doDetachChildren(self) def doAttachChildren(self): #We need to force the use of a panel subclass method here, else #the event will not propagate to children base_widget.ScrollPanelWrapper.doAttachChildren(self)""" @classmethod def registerClass(cls): base_widget.LiberviaWidget.addDropKey("CONTACT", cls.createPanel) @classmethod def createPanel(cls, host, item): _contact = item if isinstance(item, JID) else JID(item) host.contact_panel.setContactMessageWaiting(_contact.bare, False) _new_panel = ChatPanel(host, _contact) # XXX: pyjamas doesn't seems to support creating with cls directly _new_panel.historyPrint() host.setSelected(_new_panel) return _new_panel def refresh(self): """Refresh the display of this widget.""" self.host.contact_panel.setContactMessageWaiting(self.target.bare, False) self.content_scroll.scrollToBottom() def matchEntity(self, entity): """ @param entity: target jid as a string or JID instance. Could also be a couple with a type in the second element. @return: True if self matches the given entity """ if isinstance(entity, tuple): entity, type_ = entity if len(entity) > 1 else (entity[0], self.type) else: type_ = self.type entity = entity if isinstance(entity, JID) else JID(entity) try: return self.target.bare == entity.bare and self.type == type_ except AttributeError as e: e.include_traceback() return False def getWarningData(self): if self.type not in ["one2one", "group"]: raise Exception("Unmanaged type !") if self.type == "one2one": msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % self.target elif self.type == "group": msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg) def onTextEntered(self, text): self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text) self.state_machine._onEvent("active") def onQuit(self): base_widget.LiberviaWidget.onQuit(self) if self.type == 'group': self.host.bridge.call('mucLeave', None, self.target.bare) def setUserNick(self, nick): """Set the nick of the user, usefull for e.g. change the color of the user""" self.nick = nick def setPresents(self, nicks): """Set the users presents in this room @param occupants: list of nicks (string)""" self.occupants_list.clear() for nick in nicks: self.occupants_list.addOccupant(nick) def userJoined(self, nick, data): self.occupants_list.addOccupant(nick) self.printInfo("=> %s has joined the room" % nick) def userLeft(self, nick, data): self.occupants_list.removeOccupant(nick) self.printInfo("<= %s has left the room" % nick) def historyPrint(self, size=20): """Print the initial history""" def getHistoryCB(history): # display day change day_format = "%A, %d %b %Y" previous_day = datetime.now().strftime(day_format) for line in history: timestamp, from_jid, to_jid, message, mess_type, extra = line message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) if previous_day != message_day: self.printInfo("* " + message_day) previous_day = message_day self.printMessage(from_jid, message, extra, timestamp) self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True) def printInfo(self, msg, type='normal'): """Print general info @param msg: message to print @type: one of: normal: general info like "toto has joined the room" me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" """ _wid = Label(msg) if type == 'normal': _wid.setStyleName('chatTextInfo') elif type == 'me': _wid.setStyleName('chatTextMe') else: _wid.setStyleName('chatTextInfo') self.content.add(_wid) def printMessage(self, from_jid, msg, extra, timestamp=None): """Print message in chat window. Must be implemented by child class""" _jid = JID(from_jid) nick = _jid.node if self.type == 'one2one' else _jid.resource mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare # mymess = True if message comes from local user if msg.startswith('/me '): self.printInfo('* %s %s' % (nick, msg[4:]), type='me') return self.content.add(ChatText(timestamp, nick, mymess, msg, extra.get('xhtml'))) self.content_scroll.scrollToBottom() def startGame(self, game_type, waiting, referee, players, *args): """Configure the chat window to start a game""" classes = {"Tarot": CardPanel, "RadioCol": RadioColPanel} if game_type not in classes.keys(): return # unknown game attr = game_type.lower() self.occupants_list.addSpecials(players, SYMBOLS[attr]) if waiting or not self.nick in players: return # waiting for player or not playing attr = "%s_panel" % attr if hasattr(self, attr): return print ("%s Game Started \o/" % game_type) panel = classes[game_type](self, referee, self.nick, players, *args) setattr(self, attr, panel) self.vpanel.insert(panel, 0) self.vpanel.setCellHeight(panel, panel.getHeight()) def getGame(self, game_type): """Return class managing the game type""" # TODO: check that the game is launched, and manage errors if game_type == "Tarot": return self.tarot_panel elif game_type == "RadioCol": return self.radiocol_panel class WebPanel(base_widget.LiberviaWidget): """ (mini)browser like widget """ def __init__(self, host, url=None): """ @param host: SatWebFrontend instance """ base_widget.LiberviaWidget.__init__(self, host) self._vpanel = VerticalPanel() self._vpanel.setSize('100%', '100%') self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) self._url.setText(url or "") self._url.setWidth('100%') hpanel = HorizontalPanel() hpanel.add(self._url) btn = Button("Go", self.onUrlClick) hpanel.setCellWidth(self._url, "100%") #self.setCellWidth(btn, "10%") hpanel.add(self._url) hpanel.add(btn) self._vpanel.add(hpanel) self._vpanel.setCellHeight(hpanel, '20px') self._frame = Frame(url or "") self._frame.setSize('100%', '100%') DOM.setStyleAttribute(self._frame.getElement(), "position", "relative") self._vpanel.add(self._frame) self.setWidget(self._vpanel) def onUrlClick(self, sender): self._frame.setUrl(self._url.getText()) class MainPanel(AbsolutePanel): def __init__(self, host): self.host = host AbsolutePanel.__init__(self) # menu self.menu = Menu(host) # unibox self.unibox_panel = UniBoxPanel(host) self.unibox_panel.setVisible(False) # status bar status = host.status_panel # contacts self._contacts = HorizontalPanel() self._contacts.addStyleName('globalLeftArea') self.contacts_switch = Button(u'«', self._contactsSwitch) self.contacts_switch.addStyleName('contactsSwitch') self._contacts.add(self.contacts_switch) self._contacts.add(self.host.contact_panel) # tabs self.tab_panel = base_widget.MainTabPanel(host) self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True) self.tab_panel.add(self.discuss_panel, "Discussions") self.tab_panel.selectTab(0) header = AbsolutePanel() header.add(self.menu) header.add(self.unibox_panel) header.add(status) header.setStyleName('header') self.add(header) self._hpanel = HorizontalPanel() self._hpanel.add(self._contacts) self._hpanel.add(self.tab_panel) self.add(self._hpanel) self.setWidth("100%") Window.addWindowResizeListener(self) def _contactsSwitch(self, btn=None): """ (Un)hide contacts panel """ if btn is None: btn = self.contacts_switch cpanel = self.host.contact_panel cpanel.setVisible(not cpanel.getVisible()) btn.setText(u"«" if cpanel.getVisible() else u"»") self.host.resize() def _contactsMove(self, parent): """Move the contacts container (containing the contact list and the "hide/show" button) to another parent, but always as the first child position (insert at index 0). """ if self._contacts.getParent(): if self._contacts.getParent() == parent: return self._contacts.removeFromParent() parent.insert(self._contacts, 0) def onWindowResized(self, width, height): _elts = doc().getElementsByClassName('gwt-TabBar') if not _elts.length: tab_bar_h = 0 else: tab_bar_h = _elts.item(0).offsetHeight ideal_height = Window.getClientHeight() - tab_bar_h self.setHeight("%s%s" % (ideal_height, "px")) def setUniBoxPanel(self, enable): """Enable or disable the unibox @param enable: boolean @return: UniBox instance or None if disabled """ self.unibox_panel.setVisible(enable) return self.unibox_panel.setUniBox(enable) class PopupMenuPanel(PopupPanel): """This implementation of a popup menu (context menu) allow you to assign two special methods which are common to all the items, in order to hide certain items and also easily define their callbacks. The menu can be bound to any of the mouse button (left, middle, right). """ def __init__(self, entries, hide=None, callback=None, vertical=True, style={}, **kwargs): """ @param entries: a dict of dicts, where each sub-dict is representing one menu item: the sub-dict key can be used as the item text and description, but optional "title" and "desc" entries would be used if they exists. The sub-dicts may be extended later to do more complicated stuff or overwrite the common methods. @param hide: function with 2 args: widget, key as string and returns True if that item should be hidden from the context menu. @param callback: function with 2 args: sender, key as string @param vertical: True or False, to set the direction @param item_style: alternative CSS class for the menu items @param menu_style: supplementary CSS class for the sender widget """ PopupPanel.__init__(self, autoHide=True, **kwargs) self._entries = entries self._hide = hide self._callback = callback self.vertical = vertical self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"} self.style.update(style) self._senders = {} def _show(self, sender): """Popup the menu relative to this sender's position. @param sender: the widget that has been clicked """ menu = VerticalPanel() if self.vertical is True else HorizontalPanel() menu.setStyleName(self.style["menu"]) def button_cb(item): """You can not put that method in the loop and rely on _key, because it is overwritten by each step. You can rely on item.key instead, which is copied from _key after the item creation. @param item: the menu item that has been clicked """ if self._callback is not None: self._callback(sender=sender, key=item.key) self.hide(autoClosed=True) for _key in self._entries.keys(): entry = self._entries[_key] if self._hide is not None and self._hide(sender=sender, key=_key) is True: continue title = entry["title"] if "title" in entry.keys() else _key item = Button(title, button_cb) item.key = _key item.setStyleName(self.style["item"]) item.setTitle(entry["desc"] if "desc" in entry.keys() else title) menu.add(item) if len(menu.getChildren()) == 0: return self.add(menu) if self.vertical is True: x = sender.getAbsoluteLeft() + sender.getOffsetWidth() y = sender.getAbsoluteTop() else: x = sender.getAbsoluteLeft() y = sender.getAbsoluteTop() + sender.getOffsetHeight() self.setPopupPosition(x, y) self.show() if self.style["selected"]: sender.addStyleDependentName(self.style["selected"]) def _onHide(popup): if self.style["selected"]: sender.removeStyleDependentName(self.style["selected"]) return PopupPanel.onHideImpl(self, popup) self.onHideImpl = _onHide def registerClickSender(self, sender, button=BUTTON_LEFT): """Bind the menu to the specified sender. @param sender: the widget to which the menu should be bound @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT """ self._senders.setdefault(sender, []) self._senders[sender].append(button) if button == BUTTON_RIGHT: # WARNING: to disable the context menu is a bit tricky... # The following seems to work on Firefox 24.0, but: # TODO: find a cleaner way to disable the context menu sender.getElement().setAttribute("oncontextmenu", "return false") def _onBrowserEvent(event): button = DOM.eventGetButton(event) if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: self._show(sender) return sender.__class__.onBrowserEvent(sender, event) sender.onBrowserEvent = _onBrowserEvent def registerMiddleClickSender(self, sender): self.registerClickSender(sender, BUTTON_MIDDLE) def registerRightClickSender(self, sender): self.registerClickSender(sender, BUTTON_RIGHT) class ToggleStackPanel(StackPanel): """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be visible at the same time, clicking a sub-panel header will not display it and hide the others but only toggle its own visibility. The argument 'visibleStack' is ignored. Note that the argument 'visible' has been added to listener's 'onStackChanged' method. """ def __init__(self, **kwargs): StackPanel.__init__(self, **kwargs) def onBrowserEvent(self, event): if DOM.eventGetType(event) == "click": index = self.getDividerIndex(DOM.eventGetTarget(event)) if index != -1: self.toggleStack(index) def add(self, widget, stackText="", asHTML=False, visible=False): StackPanel.add(self, widget, stackText, asHTML) self.setStackVisible(self.getWidgetCount() - 1, visible) def toggleStack(self, index): if index >= self.getWidgetCount(): return visible = not self.getWidget(index).getVisible() self.setStackVisible(index, visible) for listener in self.stackListeners: listener.onStackChanged(self, index, visible)