Mercurial > libervia-web
view browser_side/panels.py @ 34:ed935f763cc8
browser side: misc css/layout fixes
- new ScrollPanelWrapper class, to try to handle correctly ScrollPanels in tables
- removed from EmptyPanel, because all td must appear empty, to use the absolute trick in ScrollPanelWrapper
- when an empty panel is changed to a normal panel, cells widths are recalculated
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 16 May 2011 03:10:11 +0200 |
parents | e70521e6d803 |
children | d43d6e4b9dc8 |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- """ Libervia: a Salut à Toi frontend Copyright (C) 2011 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.FlowPanel import FlowPanel from pyjamas.ui.AbsolutePanel import AbsolutePanel from pyjamas.ui.VerticalPanel import VerticalPanel from pyjamas.ui.DockPanel import DockPanel from pyjamas.ui.HorizontalPanel import HorizontalPanel from pyjamas.ui.ScrollPanel import ScrollPanel from pyjamas.ui.TabPanel import TabPanel from pyjamas.ui.HTMLPanel import HTMLPanel from pyjamas.ui.PopupPanel import PopupPanel from pyjamas.ui.Grid import Grid from pyjamas.ui.AutoComplete import AutoCompleteTextBox from pyjamas.ui.MenuBar import MenuBar from pyjamas.ui.MenuItem import MenuItem from pyjamas.ui.Label import Label from pyjamas.ui.HTML import HTML from pyjamas.ui.DropWidget import DropWidget from pyjamas.ui.ClickListener import ClickHandler from pyjamas.ui import HasAlignment from pyjamas.ui.KeyboardListener import KEY_ENTER from pyjamas.Timer import Timer from pyjamas import Window from pyjamas import DOM from __pyjamas__ import JS, doc from pyjamas.dnd import makeDraggable from pyjamas.ui.DragWidget import DragWidget, DragContainer from jid import JID from tools import html_sanitize from datetime import datetime from time import time from dialog import ContactsChooser class MenuCmd: def __init__(self, object, handler): self._object = object self._handler = handler def execute(self): handler = getattr(self._object, self._handler) handler() class Menu(SimplePanel): def __init__(self, host): self.host = host SimplePanel.__init__(self) menu_general = MenuBar(vertical=True) menu_general.addItem("Properties", MenuCmd(self, "onProperties")) menu_games = MenuBar(vertical=True) menu_games.addItem("Tarot", MenuCmd(self, "onTarotGame")) menu_games.addItem("Xiangqi", MenuCmd(self, "onXiangqiGame")) menubar = MenuBar(vertical=False) menubar.addItem(MenuItem("General", menu_general)) menubar.addItem(MenuItem("Games", True, menu_games)) self.add(menubar) def onProperties(self): Window.alert("Properties selected") def onTarotGame(self): #Window.alert("Tarot selected") #self.host.tab_panel.add(EmptyPanel(self.host), "Tarot") def onPlayersSelected(other_players): self.host.bridge.call('launchTarotGame', None, other_players) ContactsChooser(self.host, onPlayersSelected, 3, text="Please select 3 other players").getContacts() def onXiangqiGame(self): Window.alert("Xiangqi selected") class DropCell(DropWidget): """Cell in the middle grid which replace itself with the dropped widget on DnD""" def __init__(self): DropWidget.__init__(self) def onDragEnter(self, event): print "drag enter" self.addStyleName('dragover') DOM.eventPreventDefault(event) def onDragLeave(self, event): print "\ndrag leave" if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\ event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth()-1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight()-1: #We check that we are inside widget's box, and we don't remove the style in this case because #if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we #don't want that self.removeStyleName('dragover') def onDragOver(self, event): DOM.eventPreventDefault(event) def _getCellAndRow(self, grid, event): """Return cell and row index where the event is occuring""" cell = grid.getEventTargetCell(event) row = DOM.getParent(cell) return (row.rowIndex, cell.cellIndex) def onDrop(self, event): print "Empty Panel: onDrop" dt = event.dataTransfer #'text', 'text/plain', and 'Text' are equivalent. try: item = dt.getData("text/plain") item_type = dt.getData("type") print "message: %s" % item print "type: %s" % item_type except: print "no message found" item=' ' item_type = None DOM.eventPreventDefault(event) if item_type=="GROUP": _new_panel = MicroblogPanel(self.host, item) _new_panel.setAcceptedGroup(item) elif item_type=="CONTACT": _contact = JID(item) _new_panel = ChatPanel(self.host, _contact) _new_panel.historyPrint() elif item_type=="CONTACT_TITLE": _new_panel = MicroblogPanel(self.host, accept_all=True) self.host.mpanels.remove(self) self.host.mpanels.append(_new_panel) grid = self.getParent() row_idx, cell_idx = self._getCellAndRow(grid, event) if self.host.selected == self: self.host.select(None) self.removeFromParent() grid.setWidget(row_idx, cell_idx, _new_panel) _panels = list(grid) #this suppose that we only use 1 row, need to be changed in the futur _unempty_panels = filter(lambda wid:not isinstance(wid,EmptyPanel),list(grid)) _width = 90/float(len(_unempty_panels) or 1) #now we resize all the cell of the column for panel in _unempty_panels: td_elt = panel.getElement().parentNode DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width) #FIXME: delete object ? Check the right way with pyjamas class ScrollPanelWrapper(SimplePanel): """Scroll Panel like component, wich use the full available space to work around percent size issue, it use some of the ideas found here: http://code.google.com/p/google-web-toolkit/issues/detail?id=316 specially in code given at comment #46, thanks to Stefan Bachert""" def __init__(self, *args, **kwargs): SimplePanel.__init__(self) self.spanel = ScrollPanel(*args, **kwargs) SimplePanel.setWidget(self, self.spanel) DOM.setStyleAttribute(self.getElement(), "position", "relative") DOM.setStyleAttribute(self.getElement(), "top", "0px") DOM.setStyleAttribute(self.getElement(), "left", "0px") DOM.setStyleAttribute(self.getElement(), "width", "100%") DOM.setStyleAttribute(self.getElement(), "height", "100%") DOM.setStyleAttribute(self.spanel.getElement(), "position", "absolute") DOM.setStyleAttribute(self.spanel.getElement(), "width", "100%") DOM.setStyleAttribute(self.spanel.getElement(), "height", "100%") def setWidget(self, widget): self.spanel.setWidget(widget) def setScrollPosition(self, position): self.spanel.setScrollPosition(position) def scrollToBottom(self): self.setScrollPosition(self.spanel.getElement().scrollHeight) class EmptyPanel(DropCell, SimplePanel): """Empty dropable panel""" def __init__(self, host): SimplePanel.__init__(self) self.host = host _panel = HTMLPanel("") self.add(_panel) self.setHeight('100%') DropCell.__init__(self) 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(AutoCompleteTextBox): """This text box is used as a main typing point, for message, microblog, etc""" def __init__(self, host): AutoCompleteTextBox.__init__(self) self._popup = None self._timer = Timer(notify=self._timeCb) self.host = host def addKey(self, key): self.getCompletionItems().completions.append(key) def showWarning(self, target_data): type, target = target_data if type == "PUBLIC": msg = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" style = "targetPublic" elif type == "GROUP": msg = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" % (target or '') style = "targetGroup" elif type == "STATUS": msg = "This will be your new status message" style = "targetStatus" elif type == "ONE2ONE": msg = "This message will be sent to your contact <span class='warningTarget'>%s</span>" % target style = "targetOne2One" else: print "WARNING: undetermined target for this message" return contents = HTML(msg) self._popup = PopupPanel(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.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 (target_type, target info)""" type = None target = None if txt.startswith('@@: '): type = "PUBLIC" elif txt.startswith('@'): type = "GROUP" _end = txt.find(': ') if _end == -1: type = "STATUS" else: target = txt[1:_end] #only one target group is managed for the moment if not target in self.host.contact_panel.getGroups(): target = None elif self.host.selected == None: type = "STATUS" elif isinstance(self.host.selected, ChatPanel): type = "ONE2ONE" target = str(self.host.selected.target) else: print self.host.selected type = "UNKNOWN" return (type, target) def onKeyPress(self, sender, keycode, modifiers): _txt = self.getText() if not self._popup: self.showWarning(self._getTarget(_txt)) else: _target = self._getTarget(_txt) if _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 _txt: if _txt.startswith('@'): self.host.bridge.call('sendMblog', None, self.getText()) elif self.host.selected == None: self.host.bridge.call('setStatus', None, _txt) elif isinstance(self.host.selected, ChatPanel): _chat = self.host.selected mess_type = "groupchat" if _chat.type=='group' else "chat" self.host.bridge.call('sendMessage', None, str(_chat.target), _txt, '', mess_type) self.setText('') self._timeCb(None) #we remove the popup 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 MicroblogEntry(SimplePanel): def __init__(self, body, author, timestamp): SimplePanel.__init__(self) _datetime = datetime.fromtimestamp(timestamp) 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_body'>%(body)s</div>" % {"author": author, "timestamp": _datetime, "body": body} ) panel.setStyleName('microblogEntry') self.add(panel) class MicroblogPanel(DropCell, ScrollPanelWrapper): def __init__(self,host, title=' ', accept_all=False): """Panel used to show microblog @param title: title of the panel @param accept_all: if true, show every message, without filtering jids""" ScrollPanelWrapper.__init__(self) DropCell.__init__(self) self.host = host self.accept_all = accept_all title=title.replace('<','<').replace('>','>') self.accepted_groups = [] _class = ['mb_panel_header'] if title == ' ': _class.append('empty_header') self.vpanel = VerticalPanel() self.vpanel.add(HTMLPanel("<div class='%s'>%s</div>" % (','.join(_class),title))) self.vpanel.setWidth('100%') self.setStyleName('microblogPanel') self.setWidget(self.vpanel) def addEntry(self, text, author=None, timestamp=None): """Add an entry to the panel @param text: main text of the entry @param author: who wrote the entry @param date: when the entry was written""" _entry = MicroblogEntry(text, author, timestamp) self.vpanel.insert(_entry,1) 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 show 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':self.status} def changeStatus(self, new_status): self.status = new_status or ' ' self.setHTML(self.__getContent()) def onClick(self, sender, event): #As status is the default target of uniBar, we don't want to select anything if click on it self.host.select(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]" % nick, "msg_class": ' '.join(_msg_class), "msg": 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)) class OccupantsList(AbsolutePanel): """Panel user to show occupants of a room""" def __init__(self): AbsolutePanel.__init__(self) self.setStyleName('occupantsList') self.occupants = set() #self.setHeight('100%') def addOccupant(self, nick): _occupant = Occupant(nick) print "addOccupant: nick", nick print _occupant self.occupants.add(_occupant) self.add(_occupant) class ChatPanel(DropCell, ClickHandler, SimplePanel): 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""" SimplePanel.__init__(self) DropCell.__init__(self) ClickHandler.__init__(self) self.vpanel = VerticalPanel() self.vpanel.setSize('100%','100%') self.host = host self.type = type self.nick = None if not target: print "ERROR: Empty target !" return self.target = target title="%s" % target.bare title.replace('<','<').replace('>','>') _class = ['mb_panel_header'] self.header = HTMLPanel("<div class='%s'>%s</div>" % (','.join(_class),title)) self.header.setStyleName('chatHeader') 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 = ScrollPanelWrapper(self.content) chat_area.add(self.content_scroll) chat_area.setCellWidth(self.content_scroll, '100%') self.vpanel.add(self.header) self.vpanel.setCellHeight(self.header, '1%') self.vpanel.add(self.body) self.setWidget(self.vpanel) self.setStyleName('chatPanel') self.addClickListener(self) def onClick(self, sender, event): self.host.select(self) 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)""" for nick in nicks: self.occupants_list.addOccupant(nick) def historyPrint(self, size=20): """Print the initial history""" def getHistoryCB(history): stamps=history.keys() stamps.sort() for stamp in stamps: self.printMessage(history[stamp][0], history[stamp][1], stamp) self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, str(self.target), 20) 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() class MainDiscussionPanel(HorizontalPanel): def __init__(self, host): self.host=host HorizontalPanel.__init__(self) self._left = self.host.contact_panel self._right = Grid(1,3) self._right.setWidth('100%') self._right.setHeight('100%') self.add(self._left) self.setCellWidth(self._left, "15%") self.add(self._right) self.setCellWidth(self._right, "85%") def changePanel(self, idx, panel): self._right.setWidget(0,idx,panel) self._right.getCellFormatter().setWidth(0, idx, '5%' if isinstance(panel, EmptyPanel) else '90%') class MainTabPanel(TabPanel): def __init__(self, host): TabPanel.__init__(self) self.host=host TabPanel() self.tabBar.setVisible(False) self.addStyleName('mainTabPanel') Window.addWindowResizeListener(self) def onWindowResized(self, width, height): print "onWindowResized" tab_panel_elt = self.getElement() _elts = doc().getElementsByClassName('gwt-TabBar') if not _elts.length: print ("ERROR: not TabBar found, it should exist !") tab_bar_h = 0 else: tab_bar_h = _elts.item(0).offsetHeight ideal_height = Window.getClientHeight() - tab_panel_elt.offsetTop - tab_bar_h - 5 self.setWidth("%s%s" % (width-5, "px")); self.setHeight("%s%s" % (ideal_height, "px")); def add(self, widget, tabText=None, asHTML=False): TabPanel.add(self, widget, tabText, asHTML) if self.getWidgetCount()>1: self.tabBar.setVisible(True) self.host.resize() class MainPanel(AbsolutePanel): def __init__(self, host): self.host=host AbsolutePanel.__init__(self) #self.setHorizontalAlignment(HasAlignment.ALIGN_LEFT) #self.setVerticalAlignment(HasAlignment.ALIGN_TOP) menu = Menu(host) unibox_panel = UniBoxPanel(host) self.host.setUniBox(unibox_panel.unibox) status = host.status_panel self.tab_panel = MainTabPanel(host) self.discuss_panel = MainDiscussionPanel(self.host) self.tab_panel.add(self.discuss_panel, "Discussions") self.tab_panel.selectTab(0) self.add(menu) self.add(unibox_panel) self.add(status) self.add(self.tab_panel) self.setWidth("100%") #self.setHeight("99%") Window.addWindowResizeListener(self) def onWindowResized(self, width, height): print "resizing: %s %s" % (width, height) #self.setWidth("%s%s" % (width, "px")); _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"));