Mercurial > libervia-web
view browser_side/panels.py @ 217:f7ec248192de
browser_side: display clickable URLs in chat text
author | souliane <souliane@mailoo.org> |
---|---|
date | Sun, 08 Sep 2013 12:34:00 +0200 |
parents | 7b26be266ab1 |
children | 4e6467efd6bf |
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.ClickListener import ClickHandler from pyjamas.ui.KeyboardListener import KEY_ENTER 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, addURLToText from datetime import datetime from time import time import dialog import base_widget from pyjamas import Window from __pyjamas__ import doc class UniBoxPanel(SimplePanel): """Panel containing the UniBox""" def __init__(self, host): SimplePanel.__init__(self) self.setStyleName('uniBoxPanel') self.unibox = UniBox(host) self.unibox.setWidth('100%') self.add(self.unibox) class UniBox(TextArea, MouseHandler): #AutoCompleteTextBox): """This text box is used as a main typing point, for message, microblog, etc""" def __init__(self, host): TextArea.__init__(self) #AutoCompleteTextBox.__init__(self) self.__size = (0, 0) self._popup = None self._timer = Timer(notify=self._timeCb) self.host = host self.setStyleName('uniBox') self.addKeyboardListener(self) MouseHandler.__init__(self) self.addMouseListener(self) self._selected_cache = None host.addSelectedListener(self.onSelectedChange) def addKey(self, key): return #self.getCompletionItems().completions.append(key) def removeKey(self, key): 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: _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 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() 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 and not self.visible: if keycode == KEY_ENTER: if _txt: target_hook, _type, msg = target if target_hook: parsed_txt, data = target_hook if _type == "PUBLIC": self.host.bridge.call("sendMblog", None, "PUBLIC", None, parsed_txt) elif _type == "GROUP": self.host.bridge.call("sendMblog", None, "GROUP", data, parsed_txt) elif _type == "STATUS": self.host.bridge.call('setStatus', None, parsed_txt) else: print "ERROR: Unknown target hook type" else: #we send the message to the selected target self._selected_cache.onTextEntered(_txt) self.setText('') self._timeCb(None) #we remove the popup sender.cancelKey() else: self.__onComposing() def __onComposing(self): """Callback when the user is composing a text.""" if hasattr(self._selected_cache, "target"): target_s = str(self._selected_cache.target) self.host.bridge.call('chatStateComposing', None, target_s) 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 """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.content = data['content'] self.author = data['author'] self.timestamp = float(data.get('timestamp', 0)) #XXX: int doesn't work here self.comments = data.get('comments', False) if self.comments: try: self.comments_hash = (data['comments_service'], data['comments_node']) self.comments_service = data['comments_service'] self.comments_node = data['comments_node'] except KeyError: print "Warning: can't manage comment [%s], some keys are missing in microblog data (%s)" % (data["comments"], data.keys()) self.comments = False if set(("service","node")).issubset(data.keys()): self.service = data["service"] self.node = data["node"] self.hash = (self.service, self.node) class MicroblogEntry(SimplePanel, ClickHandler): def __init__(self, blog_panel, mblog_entry): SimplePanel.__init__(self) self._blog_panel = blog_panel self.author = mblog_entry.author self.timestamp = mblog_entry.timestamp _datetime = datetime.fromtimestamp(mblog_entry.timestamp) self.comments = mblog_entry.comments self.panel = HTMLPanel(""" <div class='mb_entry_header'><span class='mb_entry_author'>%(author)s</span> on <span class='mb_entry_timestamp'>%(timestamp)s</span></div> <div class="mb_entry_avatar" id='id_avatar'></div> <div class="mb_entry_dialog"> <p class="bubble">%(body)s</p> </div> """ % {"author": html_sanitize(self.author), "timestamp": _datetime, "body": addURLToText(html_sanitize(mblog_entry.content)) }) self.avatar = Image(blog_panel.host.getAvatar(self.author)) self.panel.add(self.avatar, "id_avatar") self.panel.setStyleName('mb_entry') self.add(self.panel) ClickHandler.__init__(self) self.addClickListener(self) 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): print "microblog entry selected (author=%s)" % self.author self._blog_panel.setSelectedEntry(self if self.comments else None) 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) #base_widget.ScrollPanelWrapper.__init__(self) #DropCell.__init__(self) self.accepted_groups = accepted_groups self.entries = {} self.comments = {} self.selected_entry = None self.vpanel = VerticalPanel() self.vpanel.setStyleName('microblogPanel') self.setWidget(self.vpanel) @classmethod def registerClass(cls): base_widget.LiberviaWidget.addDropKey("GROUP", cls.createGroup) base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", cls.createMeta) @classmethod def createGroup(cls, host, item): _new_panel = MicroblogPanel(host, [item]) #XXX: pyjamas doesn't support use of cls directly _new_panel.setAcceptedGroup(item) host.FillMicroblogPanel(_new_panel) host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, 'GROUP', [item], 10) return _new_panel @classmethod def createMeta(cls, host, item): _new_panel = MicroblogPanel(host, []) #XXX: pyjamas doesn't support use of cls directly host.FillMicroblogPanel(_new_panel) host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, 'ALL', [], 10) return _new_panel 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") self.host.bridge.call("sendMblogComment", None, comments_node, text) elif not self.accepted_groups: # we are entering a public microblog self.host.bridge.call("sendMblog", None, "PUBLIC", None, text) else: # we are entering a microblog restricted to a group # FIXME: manage several groups self.host.bridge.call("sendMblog", None, "GROUP", self.accepted_groups[0], 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 microblogs" 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 mblog_item = MicroblogItem(mblog) self.addEntry(mblog_item) 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 mblog_item = MicroblogItem(mblog) self.addEntry(mblog_item) 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""" # 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.timestamp < entry.timestamp: break else: if child.timestamp > entry.timestamp: break idx += 1 vpanel.insert(entry, idx) def addEntry(self, mblog_item): """Add an entry to the panel @param mblog_item: MicroblogItem instance """ if mblog_item.type == "comment": if not mblog_item.hash in self.comments: # The comments node is not known in this panel return _entry = MicroblogEntry(self, mblog_item) parent = self.comments[mblog_item.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) # we want comments to be inserted in chronological order self._chronoInsert(sub_panel, _entry, reverse=False) return if mblog_item.id in self.entries: return _entry = MicroblogEntry(self, mblog_item) self.entries[mblog_item.id] = _entry self._chronoInsert(self.vpanel, _entry) if mblog_item.comments: # entry has comments, we keep the comment node as a reference self.comments[mblog_item.comments_hash] = _entry self.host.bridge.call('getMblogComments', self.mblogsInsert, mblog_item.comments_service, mblog_item.comments_node) def setSelectedEntry(self, entry): if self.selected_entry == entry: entry = None if self.selected_entry: self.selected_entry.removeStyleName('selected_entry') if entry: 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): """Set the group which can be displayed in this panel @param group: string of the group, or list of string """ if isinstance(group, list): self.accepted_groups.extend(group) else: self.accepted_groups.append(group) 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): def __init__(self, host, status=''): self.host = host self.status = status or ' ' HTMLPanel.__init__(self, self.__getContent()) self.setStyleName('statusPanel') ClickHandler.__init__(self) self.addClickListener(self) def __getContent(self): return "<span class='status'>%(status)s</span>" % {'status': html_sanitize(self.status)} def changeStatus(self, new_status): self.status = new_status or ' ' self.setHTML(self.__getContent()) 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): _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))} ) self.setStyleName('chatText') class Occupant(HTML): """Occupant of a MUC room""" def __init__(self, nick): self.nick = nick HTML.__init__(self, "<div class='occupant'>%s</div>" % html_sanitize(nick)) def __str__(self): return self.nick 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) 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.addStyleName('chatPanel') self.setWidget(self.vpanel) """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.createChat) @classmethod def createChat(cls, host, item): _contact = 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() return _new_panel 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): mess_type = "groupchat" if self.type == 'group' else "chat" self.host.bridge.call('sendMessage', None, str(self.target), text, '', mess_type) 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 = 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, 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, 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)) self.content_scroll.scrollToBottom() def startGame(self, game_type, referee, players): """Configure the chat window to start a game""" if game_type == "Tarot": if hasattr(self, "tarot_panel"): return self.tarot_panel = CardPanel(self, referee, players, self.nick) self.vpanel.insert(self.tarot_panel, 0) self.vpanel.setCellHeight(self.tarot_panel, self.tarot_panel.getHeight()) elif game_type == "RadioCol": #XXX: We can have double panel if we join quickly enough to have the group chat start signal # on invitation + the one triggered on room join if hasattr(self, "radiocol_panel"): return self.radiocol_panel = RadioColPanel(self, referee, self.nick) self.vpanel.insert(self.radiocol_panel, 0) self.vpanel.setCellHeight(self.radiocol_panel, self.radiocol_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 menu = Menu(host) #unibox unibox_panel = UniBoxPanel(host) self.host.setUniBox(unibox_panel.unibox) #status bar status = host.status_panel #contacts _contacts = HorizontalPanel() _contacts.addStyleName('globalLeftArea') contacts_switch = Button(u'«', self._contactsSwitch) contacts_switch.addStyleName('contactsSwitch') _contacts.add(contacts_switch) _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(menu) header.add(unibox_panel) header.add(status) header.setStyleName('header') self.add(header) _hpanel = HorizontalPanel() _hpanel.add(_contacts) _hpanel.add(self.tab_panel) self.add(_hpanel) self.setWidth("100%") Window.addWindowResizeListener(self) def _contactsSwitch(self, btn): """ (Un)hide contacts panel """ cpanel = self.host.contact_panel cpanel.setVisible(not cpanel.getVisible()) btn.setText(u"«" if cpanel.getVisible() else u"»") self.host.resize() 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"))