Mercurial > libervia-web
changeset 449:981ed669d3b3
/!\ reorganize all the file hierarchy, move the code and launching script to src:
- browser_side --> src/browser
- public --> src/browser_side/public
- libervia.py --> src/browser/libervia_main.py
- libervia_server --> src/server
- libervia_server/libervia.sh --> src/libervia.sh
- twisted --> src/twisted
- new module src/common
- split constants.py in 3 files:
- src/common/constants.py
- src/browser/constants.py
- src/server/constants.py
- output --> html (generated by pyjsbuild during the installation)
- new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css
- setup.py installs libervia to the following paths:
- src/common --> <LIB>/libervia/common
- src/server --> <LIB>/libervia/server
- src/twisted --> <LIB>/twisted
- html --> <SHARE>/libervia/html
- server_side --> <SHARE>libervia/server_side
- LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation:
- clean: remove previous installation directories
- purge: remove building and previous installation directories
You may need to update your sat.conf and/or launching script to update the following options/parameters:
- ssl_certificate
- data_dir
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 20 May 2014 06:41:16 +0200 |
parents | 14c35f7f1ef5 |
children | 41aae13cab2b |
files | browser_side/__init__.py browser_side/base_panels.py browser_side/base_widget.py browser_side/card_game.py browser_side/contact.py browser_side/contact_group.py browser_side/dialog.py browser_side/file_tools.py browser_side/html_tools.py browser_side/jid.py browser_side/list_manager.py browser_side/logging.py browser_side/menu.py browser_side/nativedom.py browser_side/notification.py browser_side/panels.py browser_side/plugin_xep_0085.py browser_side/radiocol.py browser_side/register.py browser_side/richtext.py browser_side/xmlui.py constants.py libervia.py libervia_server/__init__.py libervia_server/blog.py libervia_server/html_tools.py libervia_server/libervia.sh public/contrat_social.html public/libervia.css public/libervia.html public/sat_logo_16.png setup.py src/__init__.py src/browser/__init__.py src/browser/base_panels.py src/browser/base_widget.py src/browser/card_game.py src/browser/constants.py src/browser/contact.py src/browser/contact_group.py src/browser/dialog.py src/browser/file_tools.py src/browser/html_tools.py src/browser/jid.py src/browser/libervia_main.py src/browser/list_manager.py src/browser/logging.py src/browser/menu.py src/browser/nativedom.py src/browser/notification.py src/browser/panels.py src/browser/plugin_xep_0085.py src/browser/public/contrat_social.html src/browser/public/libervia.css src/browser/public/libervia.html src/browser/public/sat_logo_16.png src/browser/radiocol.py src/browser/register.py src/browser/richtext.py src/browser/xmlui.py src/common/__init__.py src/common/constants.py src/libervia.sh src/server/__init__.py src/server/blog.py src/server/constants.py src/server/html_tools.py src/server/server.py src/twisted/plugins/libervia_server.py tools/__init__.py twisted/plugins/libervia.py |
diffstat | 65 files changed, 11796 insertions(+), 11601 deletions(-) [+] |
line wrap: on
line diff
--- a/browser_side/base_panels.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,609 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -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.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.StackPanel import StackPanel -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas import DOM - -from datetime import datetime -from time import time - -from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML - -from constants import Const as C -from sat_frontends.tools.strings import addURLToText, addURLToImage -from sat.core.i18n import _ - - -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, state=None, special=""): - """ - @param nick: the user nickname - @param state: the user chate state (XEP-0085) - @param special: a string of symbols (e.g: for activities) - """ - HTML.__init__(self) - self.nick = nick - self._state = state - self.special = special - self._refresh() - - def __str__(self): - return self.nick - - def setState(self, state): - self._state = state - self._refresh() - - def addSpecial(self, special): - """@param special: unicode""" - if special not in self.special: - self.special += special - self._refresh() - - def removeSpecials(self, special): - """@param special: unicode or list""" - if not isinstance(special, list): - special = [special] - for symbol in special: - self.special = self.special.replace(symbol, "") - self._refresh() - - def _refresh(self): - state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' - special = "" if len(self.special) == 0 else " %s" % self.special - self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state)) - - -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: - log.error("trying to remove an unexisting nick") - - def clear(self): - self.occupants_list.clear() - AbsolutePanel.clear(self) - - def updateSpecials(self, occupants=[], html=""): - """Set the specified html "symbol" to the listed occupants, - and eventually remove it from the others (if they got it). - This is used for example to visualize who is playing a game. - @param occupants: list of the occupants that need the symbol - @param html: unicode symbol (actually one character or more) - or a list to assign different symbols of the same family. - """ - index = 0 - special = html - for occupant in self.occupants_list.keys(): - if occupant in occupants: - if isinstance(html, list): - special = html[index] - index = (index + 1) % len(html) - self.occupants_list[occupant].addSpecial(special) - else: - self.occupants_list[occupant].removeSpecials(html) - - -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=None, **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"} - if isinstance(style, dict): - 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) - - -class TitlePanel(ToggleStackPanel): - """A toggle panel to set the message title""" - def __init__(self): - ToggleStackPanel.__init__(self, Width="100%") - self.text_area = TextArea() - self.add(self.text_area, _("Title")) - self.addStackChangeListener(self) - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - text = self.text_area.getText() - suffix = "" if (visible or not text) else (": %s" % text) - sender.setStackText(index, _("Title") + suffix) - - def getText(self): - return self.text_area.getText() - - def setText(self, text): - self.text_area.setText(text) - - -class BaseTextEditor(object): - """Basic definition of a text editor. The method edit gets a boolean parameter which - should be set to True when you want to edit the text and False to only display it.""" - - def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): - """ - Remark when inheriting this class: since the setContent method could be - overwritten by the child class, you should consider calling this __init__ - after all the parameters affecting this setContent method have been set. - @param content: dict with at least a 'text' key - @param strproc: method to be applied on strings to clean the content - @param modifiedCb: method to be called when the text has been modified. - If this method returns: - - True: the modification will be saved and afterEditCb called; - - False: the modification won't be saved and afterEditCb called; - - None: the modification won't be saved and afterEditCb not called. - @param afterEditCb: method to be called when the edition is done - """ - if content is None: - content = {'text': ''} - assert('text' in content) - if strproc is None: - def strproc(text): - try: - return text.strip() - except (TypeError, AttributeError): - return text - self.strproc = strproc - self.__modifiedCb = modifiedCb - self._afterEditCb = afterEditCb - self.initialized = False - self.edit_listeners = [] - self.setContent(content) - - def setContent(self, content=None): - """Set the editable content. The displayed content, which is set from the child class, could differ. - @param content: dict with at least a 'text' key - """ - if content is None: - content = {'text': ''} - elif not isinstance(content, dict): - content = {'text': content} - assert('text' in content) - self._original_content = {} - for key in content: - self._original_content[key] = self.strproc(content[key]) - - def getContent(self): - """Get the current edited or editable content. - @return: dict with at least a 'text' key - """ - raise NotImplementedError - - def setOriginalContent(self, content): - """Use this method with care! Content initialization should normally be - done with self.setContent. This method exists to let you trick the editor, - e.g. for self.modified to return True also when nothing has been modified. - @param content: dict - """ - self._original_content = content - - def getOriginalContent(self): - """ - @return the original content before modification (dict) - """ - return self._original_content - - def modified(self, content=None): - """Check if the content has been modified. - Remark: we don't use the direct comparison because we want to ignore empty elements - @content: content to be check against the original content or None to use the current content - @return: True if the content has been modified. - """ - if content is None: - content = self.getContent() - # the following method returns True if one non empty element exists in a but not in b - diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] - # the following method returns True if the values for the common keys are not equals - diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] - # finally the combination of both to return True if a difference is found - diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) - - return diff(content, self._original_content) - - def edit(self, edit, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if edit: - if not self.initialized: - self.syncToEditor() # e.g.: use the selected target and unibox content - self.setFocus(True) - if abort: - content = self.getContent() - if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation - self.edit(False, True, sync) - return - if sync: - self.syncFromEditor(content) # e.g.: save the content to unibox - return - else: - if not self.initialized: - return - content = self.getContent() - if abort: - self._afterEditCb(content) - return - if self.__modifiedCb and self.modified(content): - result = self.__modifiedCb(content) # e.g.: send a message or update something - if result is not None: - if self._afterEditCb: - self._afterEditCb(content) # e.g.: restore the display mode - if result is True: - self.setContent(content) - elif self._afterEditCb: - self._afterEditCb(content) - - self.initialized = True - - def setFocus(self, focus): - """ - @param focus: set to True to focus the editor - """ - raise NotImplementedError - - def syncToEditor(self): - pass - - def syncFromEditor(self, content): - pass - - def abortEdition(self, content): - return True - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - self.edit_listeners.append(listener) - - -class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): - """Base class for manage a simple text editor.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - """ - @param content - @param modifiedCb - @param afterEditCb - @param options: dict with the following value: - - no_xhtml: set to True to clean any xhtml content. - - enhance_display: if True, the display text will be enhanced with addURLToText - - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. - - listen_focus: set to True to terminate the edition when the focus is lost. - - listen_click: set to True to start the edition when you click on the widget. - """ - self.options = {'no_xhtml': False, - 'enhance_display': True, - 'listen_keyboard': True, - 'listen_focus': False, - 'listen_click': False - } - if options: - self.options.update(options) - self.__shift_down = False - if self.options['listen_focus']: - FocusHandler.__init__(self) - if self.options['listen_click']: - ClickHandler.__init__(self) - KeyboardHandler.__init__(self) - strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text) - BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) - self.textarea = self.display = None - - def setContent(self, content=None): - BaseTextEditor.setContent(self, content) - - def getContent(self): - raise NotImplementedError - - def edit(self, edit, abort=False, sync=False): - BaseTextEditor.edit(self, edit) - if edit: - if self.options['listen_focus'] and self not in self.textarea._focusListeners: - self.textarea.addFocusListener(self) - if self.options['listen_click']: - self.display.clearClickListener() - if self not in self.textarea._keyboardListeners: - self.textarea.addKeyboardListener(self) - else: - self.setDisplayContent() - if self.options['listen_focus']: - try: - self.textarea.removeFocusListener(self) - except ValueError: - pass - if self.options['listen_click'] and self not in self.display._clickListeners: - self.display.addClickListener(self) - try: - self.textarea.removeKeyboardListener(self) - except ValueError: - pass - - def setDisplayContent(self): - text = self._original_content['text'] - if not self.options['no_xhtml']: - text = addURLToImage(text) - if self.options['enhance_display']: - text = addURLToText(text) - self.display.setHTML(convertNewLinesToXHTML(text)) - - def setFocus(self, focus): - raise NotImplementedError - - def onKeyDown(self, sender, keycode, modifiers): - for listener in self.edit_listeners: - listener(self.textarea, keycode) - if not self.options['listen_keyboard']: - return - if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> - self.__shift_down = True - return - if keycode == KEY_ENTER: # finish the edition - self.textarea.setFocus(False) - if not self.options['listen_focus']: - self.edit(False) - - def onKeyUp(self, sender, keycode, modifiers): - if keycode == KEY_SHIFT: - self.__shift_down = False - - def onLostFocus(self, sender): - """Finish the edition when focus is lost""" - if self.options['listen_focus']: - self.edit(False) - - def onClick(self, sender=None): - """Start the edition when the widget is clicked""" - if self.options['listen_click']: - self.edit(True) - - def onBrowserEvent(self, event): - if self.options['listen_focus']: - FocusHandler.onBrowserEvent(self, event) - if self.options['listen_click']: - ClickHandler.onBrowserEvent(self, event) - KeyboardHandler.onBrowserEvent(self, event) - - -class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): - """Manage a simple text editor with the HTML 5 "contenteditable" property.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - HTML.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = self.display = self - - def getContent(self): - text = DOM.getInnerHTML(self.getElement()) - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setHTML(self._original_content['text']) - self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.getElement().focus() - else: - self.getElement().blur() - - -class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): - """Manage a simple text editor with a TextArea for editing, HTML for display.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - SimplePanel.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = TextArea() - self.display = HTML() - - def getContent(self): - text = self.textarea.getText() - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setText(self._original_content['text']) - self.setWidget(self.textarea if edit else self.display) - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.textarea.setCursorPos(len(self.textarea.getText())) - self.textarea.setFocus(focus)
--- a/browser_side/base_widget.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,730 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -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.ScrollPanel import ScrollPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.Image import Image -from pyjamas.ui.Widget import Widget -from pyjamas.ui.DragWidget import DragWidget -from pyjamas.ui.DropWidget import DropWidget -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui import HasAlignment -from pyjamas import DOM -from pyjamas import Window -from __pyjamas__ import doc - -from browser_side import dialog - - -class DragLabel(DragWidget): - - def __init__(self, text, _type): - DragWidget.__init__(self) - self._text = text - self._type = _type - - def onDragStart(self, event): - dt = event.dataTransfer - dt.setData('text/plain', "%s\n%s" % (self._text, self._type)) - dt.setDragImage(self.getElement(), 15, 15) - - -class LiberviaDragWidget(DragLabel): - """ A DragLabel which keep the widget being dragged as class value """ - current = None # widget currently dragged - - def __init__(self, text, _type, widget): - DragLabel.__init__(self, text, _type) - self.widget = widget - - def onDragStart(self, event): - LiberviaDragWidget.current = self.widget - DragLabel.onDragStart(self, event) - - def onDragEnd(self, event): - LiberviaDragWidget.current = None - - -class DropCell(DropWidget): - """Cell in the middle grid which replace itself with the dropped widget on DnD""" - drop_keys = {} - - def __init__(self, host): - DropWidget.__init__(self) - self.host = host - self.setStyleName('dropCell') - - @classmethod - def addDropKey(cls, key, callback): - DropCell.drop_keys[key] = callback - - def onDragEnter(self, event): - if self == LiberviaDragWidget.current: - return - self.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - 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): - self.removeStyleName('dragover') - DOM.eventPreventDefault(event) - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - try: - item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed - if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and - item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report - # item_type = dt.getData("type") - log.debug("message: %s" % item) - log.debug("type: %s" % item_type) - except: - log.debug("no message found") - item = ' ' - item_type = None - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - if self == _new_panel: # We can't drop on ourself - return - # we need to remove the widget from the panel as it will be inserted elsewhere - widgets_panel = _new_panel.getWidgetsPanel() - wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] - row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) - if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: - # the dropped widget is the only one in the same row - # as the target widget (self), we don't do anything - return - widgets_panel.removeWidget(_new_panel) - elif item_type in self.drop_keys: - _new_panel = self.drop_keys[item_type](self.host, item) - else: - log.warning("unmanaged item type") - return - if isinstance(self, LiberviaWidget): - self.host.unregisterWidget(self) - self.onQuit() - if not isinstance(_new_panel, LiberviaWidget): - log.warning("droping an object which is not a class of LiberviaWidget") - _flextable = self.getParent() - _widgetspanel = _flextable.getParent().getParent() - row_idx, cell_idx = self._getCellAndRow(_flextable, event) - if self.host.getSelected == self: - self.host.setSelected(None) - _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) - """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) - _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 WidgetHeader(AbsolutePanel, LiberviaDragWidget): - - def __init__(self, parent, title): - AbsolutePanel.__init__(self) - self.add(title) - button_group_wrapper = SimplePanel() - button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') - button_group = HorizontalPanel() - button_group.setStyleName('widgetHeader_buttonGroup') - setting_button = Image("media/icons/misc/settings.png") - setting_button.setStyleName('widgetHeader_settingButton') - setting_button.addClickListener(parent.onSetting) - close_button = Image("media/icons/misc/close.png") - close_button.setStyleName('widgetHeader_closeButton') - close_button.addClickListener(parent.onClose) - button_group.add(setting_button) - button_group.add(close_button) - button_group_wrapper.setWidget(button_group) - self.add(button_group_wrapper) - self.addStyleName('widgetHeader') - LiberviaDragWidget.__init__(self, "", "WIDGET", parent) - - -class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): - """Libervia's widget which can replace itself with a dropped widget on DnD""" - - def __init__(self, host, title='', selectable=False): - """Init the widget - @param host: SatWebFrontend object - @param title: title show in the header of the widget - @param selectable: True is widget can be selected by user""" - VerticalPanel.__init__(self) - DropCell.__init__(self, host) - ClickHandler.__init__(self) - self.__selectable = selectable - self.__title_id = HTMLPanel.createUniqueId() - self.__setting_button_id = HTMLPanel.createUniqueId() - self.__close_button_id = HTMLPanel.createUniqueId() - self.__title = Label(title) - self.__title.setStyleName('widgetHeader_title') - self._close_listeners = [] - header = WidgetHeader(self, self.__title) - self.add(header) - self.setSize('100%', '100%') - self.addStyleName('widget') - if self.__selectable: - self.addClickListener(self) - - def onClose(sender): - """Check dynamically if the unibox is enable or not""" - if self.host.uni_box: - self.host.uni_box.onWidgetClosed(sender) - - self.addCloseListener(onClose) - self.host.registerWidget(self) - - def getDebugName(self): - return "%s (%s)" % (self, self.__title.getText()) - - def getWidgetsPanel(self, verbose=True): - return self.getParent(WidgetsPanel, verbose) - - def getParent(self, class_=None, verbose=True): - """ - Note: this method overrides pyjamas.ui.Widget.getParent - @param class_: class of the ancestor to look for or None to return the first parent - @param verbose: set to True to log error messages # FIXME: must be removed - @return: the parent/ancestor or None if it has not been found - """ - current = Widget.getParent(self) - if class_ is None: - return current # this is the default behavior - while current is not None and not isinstance(current, class_): - current = Widget.getParent(current) - if current is None and verbose: - log.debug("Can't find parent %s for %s" % (class_, self)) - return current - - def onClick(self, sender): - self.host.setSelected(self) - - def onClose(self, sender): - """ Called when the close button is pushed """ - _widgetspanel = self.getWidgetsPanel() - _widgetspanel.removeWidget(self) - for callback in self._close_listeners: - callback(self) - self.onQuit() - - def onQuit(self): - """ Called when the widget is actually ending """ - pass - - def addCloseListener(self, callback): - """Add a close listener to this widget - @param callback: function to be called from self.onClose""" - self._close_listeners.append(callback) - - def refresh(self): - """This can be overwritten by a child class to refresh the display when, - instead of creating a new one, an existing widget is found and reused. - """ - pass - - def onSetting(self, sender): - widpanel = self.getWidgetsPanel() - row, col = widpanel.getIndex(self) - body = VerticalPanel() - - # colspan & rowspan - colspan = widpanel.getColSpan(row, col) - rowspan = widpanel.getRowSpan(row, col) - - def onColSpanChange(value): - widpanel.setColSpan(row, col, value) - - def onRowSpanChange(value): - widpanel.setRowSpan(row, col, value) - colspan_setter = dialog.IntSetter("Columns span", colspan) - colspan_setter.addValueChangeListener(onColSpanChange) - colspan_setter.setWidth('100%') - rowspan_setter = dialog.IntSetter("Rows span", rowspan) - rowspan_setter.addValueChangeListener(onRowSpanChange) - rowspan_setter.setWidth('100%') - body.add(colspan_setter) - body.add(rowspan_setter) - - # size - width_str = self.getWidth() - if width_str.endswith('px'): - width = int(width_str[:-2]) - else: - width = 0 - height_str = self.getHeight() - if height_str.endswith('px'): - height = int(height_str[:-2]) - else: - height = 0 - - def onWidthChange(value): - if not value: - self.setWidth('100%') - else: - self.setWidth('%dpx' % value) - - def onHeightChange(value): - if not value: - self.setHeight('100%') - else: - self.setHeight('%dpx' % value) - width_setter = dialog.IntSetter("width (0=auto)", width) - width_setter.addValueChangeListener(onWidthChange) - width_setter.setWidth('100%') - height_setter = dialog.IntSetter("height (0=auto)", height) - height_setter.addValueChangeListener(onHeightChange) - height_setter.setHeight('100%') - body.add(width_setter) - body.add(height_setter) - - # reset - def onReset(sender): - colspan_setter.setValue(1) - rowspan_setter.setValue(1) - width_setter.setValue(0) - height_setter.setValue(0) - - reset_bt = Button("Reset", onReset) - body.add(reset_bt) - body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER) - - _dialog = dialog.GenericDialog("Widget setting", body) - _dialog.show() - - def setTitle(self, text): - """change the title in the header of the widget - @param text: text of the new title""" - self.__title.setText(text) - - def isSelectable(self): - return self.__selectable - - def setSelectable(self, selectable): - if not self.__selectable: - try: - self.removeClickListener(self) - except ValueError: - pass - if self.selectable and not self in self._clickListeners: - self.addClickListener(self) - self.__selectable = selectable - - def getWarningData(self): - """ Return exposition warning level when this widget is selected and something is sent to it - This method should be overriden by children - @return: tuple (warning level type/HTML msg). Type can be one of: - - PUBLIC - - GROUP - - ONE2ONE - - MISC - - NONE - """ - if not self.__selectable: - log.error("getWarningLevel must not be called for an unselectable widget") - raise Exception - # TODO: cleaner warning types (more general constants) - return ("NONE", None) - - def setWidget(self, widget, scrollable=True): - """Set the widget that will be in the body of the LiberviaWidget - @param widget: widget to put in the body - @param scrollable: if true, the widget will be in a ScrollPanelWrapper""" - if scrollable: - _scrollpanelwrapper = ScrollPanelWrapper() - _scrollpanelwrapper.setStyleName('widgetBody') - _scrollpanelwrapper.setWidget(widget) - body_wid = _scrollpanelwrapper - else: - body_wid = widget - self.add(body_wid) - self.setCellHeight(body_wid, '100%') - - def doDetachChildren(self): - # We need to force the use of a panel subclass method here, - # for the same reason as doAttachChildren - VerticalPanel.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 - VerticalPanel.doAttachChildren(self) - - def matchEntity(self, entity): - """This method should be overwritten by child classes.""" - raise NotImplementedError - - -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 EmptyWidget(DropCell, SimplePanel): - """Empty dropable panel""" - - def __init__(self, host): - SimplePanel.__init__(self) - DropCell.__init__(self, host) - #self.setWidget(HTML('')) - self.setSize('100%', '100%') - - -class BorderWidget(EmptyWidget): - def __init__(self, host): - EmptyWidget.__init__(self, host) - self.addStyleName('borderPanel') - - -class LeftBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('leftBorderWidget') - - -class RightBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('rightBorderWidget') - - -class BottomBorderWidget(BorderWidget): - def __init__(self, host): - BorderWidget.__init__(self, host) - self.addStyleName('bottomBorderWidget') - - -class WidgetsPanel(ScrollPanelWrapper): - - def __init__(self, host, locked=False): - ScrollPanelWrapper.__init__(self) - self.setSize('100%', '100%') - self.host = host - self.locked = locked # if True: tab will not be removed when there are no more widgets inside - self.selected = None - self.flextable = FlexTable() - self.flextable.setSize('100%', '100%') - self.setWidget(self.flextable) - self.setStyleName('widgetsPanel') - _bottom = BottomBorderWidget(self.host) - self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row, - # dropping a widget there will add a new row - td_elt = _bottom.getElement().parentNode - DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit) - self._max_cols = 1 # give the maximum number of columns i a raw - - def isLocked(self): - return self.locked - - def changeWidget(self, row, col, wid): - """Change the widget in the given location, add row or columns when necessary""" - log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col)) - last_row = max(0, self.flextable.getRowCount() - 1) - try: - prev_wid = self.flextable.getWidget(row, col) - except: - log.error("Trying to change an unexisting widget !") - return - - cellFormatter = self.flextable.getFlexCellFormatter() - - if isinstance(prev_wid, BorderWidget): - # We are on a border, we must create a row and/or columns - log.debug("BORDER WIDGET") - prev_wid.removeStyleName('dragover') - - if isinstance(prev_wid, BottomBorderWidget): - # We are on the bottom border, we create a new row - self.flextable.insertRow(last_row) - self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host)) - self.flextable.setWidget(last_row, 1, wid) - self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host)) - cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT) - row = last_row - - elif isinstance(prev_wid, LeftBorderWidget): - if col != 0: - log.error("LeftBorderWidget must be on the first column !") - return - self.flextable.insertCell(row, col + 1) - self.flextable.setWidget(row, 1, wid) - - elif isinstance(prev_wid, RightBorderWidget): - if col != self.flextable.getCellCount(row) - 1: - log.error("RightBorderWidget must be on the last column !") - return - self.flextable.insertCell(row, col) - self.flextable.setWidget(row, col, wid) - - else: - prev_wid.removeFromParent() - self.flextable.setWidget(row, col, wid) - - _max_cols = max(self._max_cols, self.flextable.getCellCount(row)) - if _max_cols != self._max_cols: - self._max_cols = _max_cols - self._sizesAdjust() - - def _sizesAdjust(self): - cellFormatter = self.flextable.getFlexCellFormatter() - width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders - - for row_idx in xrange(self.flextable.getRowCount()): - for col_idx in xrange(self.flextable.getCellCount(row_idx)): - _widget = self.flextable.getWidget(row_idx, col_idx) - if not isinstance(_widget, BorderWidget): - td_elt = _widget.getElement().parentNode - DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width) - - last_row = max(0, self.flextable.getRowCount() - 1) - cellFormatter.setColSpan(last_row, 0, self._max_cols) - - def addWidget(self, wid): - """Add a widget to a new cell on the next to last row""" - last_row = max(0, self.flextable.getRowCount() - 1) - log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0)) - self.changeWidget(last_row, 0, wid) - - def removeWidget(self, wid): - """Remove a widget and the cell where it is""" - _row, _col = self.flextable.getIndex(wid) - self.flextable.remove(wid) - self.flextable.removeCell(_row, _col) - if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row - self.flextable.removeRow(_row) - _max_cols = 1 - for row_idx in xrange(self.flextable.getRowCount()): - _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx)) - if _max_cols != self._max_cols: - self._max_cols = _max_cols - self._sizesAdjust() - current = self - - blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ? - - if blank_page and not self.isLocked(): - # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed - while current is not None: - if isinstance(current, MainTabPanel): - current.onWidgetPanelRemove(self) - return - current = current.getParent() - log.error("no MainTabPanel found !") - - def getWidgetCoords(self, wid): - return self.flextable.getIndex(wid) - - def getLiberviaRowWidgets(self, row): - """ Return all the LiberviaWidget in the row """ - return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)] - - def getRowWidgets(self, row): - """ Return all the widgets in the row """ - widgets = [] - cols = self.flextable.getCellCount(row) - for col in xrange(cols): - widgets.append(self.flextable.getWidget(row, col)) - return widgets - - def getLiberviaWidgetsCount(self): - """ Get count of contained widgets """ - return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)]) - - def getIndex(self, wid): - return self.flextable.getIndex(wid) - - def getColSpan(self, row, col): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.getColSpan(row, col) - - def setColSpan(self, row, col, value): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.setColSpan(row, col, value) - - def getRowSpan(self, row, col): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.getRowSpan(row, col) - - def setRowSpan(self, row, col, value): - cellFormatter = self.flextable.getFlexCellFormatter() - return cellFormatter.setRowSpan(row, col, value) - - -class DropTab(Label, DropWidget): - - def __init__(self, tab_panel, text): - Label.__init__(self, text) - DropWidget.__init__(self, tab_panel) - self.tab_panel = tab_panel - self.setStyleName('dropCell') - self.setWordWrap(False) - DOM.setStyleAttribute(self.getElement(), "min-width", "30px") - - def _getIndex(self): - """ get current index of the DropTab """ - # XXX: awful hack, but seems the only way to get index - return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1 - - def onDragEnter(self, event): - #if self == LiberviaDragWidget.current: - # return - self.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - self.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def onDrop(self, event): - DOM.eventPreventDefault(event) - self.removeStyleName('dragover') - if self._getIndex() == self.tab_panel.tabBar.getSelectedTab(): - # the widget come from the DragTab, so nothing to do, we let it there - return - - # FIXME: quite the same stuff as in DropCell, need some factorisation - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - try: - item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed - if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and - item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report - # item_type = dt.getData("type") - log.debug("message: %s" % item) - log.debug("type: %s" % item_type) - except: - log.debug("no message found") - item = ' ' - item_type = None - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - _new_panel.getWidgetsPanel().removeWidget(_new_panel) - elif item_type in DropCell.drop_keys: - _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item) - else: - log.warning("unmanaged item type") - return - - widgets_panel = self.tab_panel.getWidget(self._getIndex()) - widgets_panel.addWidget(_new_panel) - - -class MainTabPanel(TabPanel): - - def __init__(self, host): - TabPanel.__init__(self) - self.host = host - self.tabBar.setVisible(False) - self.setStyleName('liberviaTabPanel') - self.addStyleName('mainTabPanel') - Window.addWindowResizeListener(self) - - def getCurrentPanel(self): - """ Get the panel of the currently selected tab """ - return self.deck.visibleWidget - - def onWindowResized(self, width, height): - tab_panel_elt = self.getElement() - _elts = doc().getElementsByClassName('gwt-TabBar') - if not _elts.length: - log.error("no TabBar found, it should exist !") - tab_bar_h = 0 - else: - tab_bar_h = _elts.item(0).offsetHeight - ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5 - ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5 - self.setWidth("%s%s" % (ideal_width, "px")) - self.setHeight("%s%s" % (ideal_height, "px")) - - def add(self, widget, text=''): - tab = DropTab(self, text) - TabPanel.add(self, widget, tab, False) - if self.getWidgetCount() > 1: - self.tabBar.setVisible(True) - self.host.resize() - - def onWidgetPanelRemove(self, panel): - """ Called when a child WidgetsPanel is empty and need to be removed """ - self.remove(panel) - widgets_count = self.getWidgetCount() - if widgets_count == 1: - self.tabBar.setVisible(False) - self.host.resize() - self.selectTab(0) - else: - self.selectTab(widgets_count - 1)
--- a/browser_side/card_game.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,386 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Label import Label -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui import HasAlignment -from pyjamas import Window -from pyjamas import DOM - -from dialog import ConfirmDialog, GenericDialog -from xmlui import XMLUI -from sat_frontends.tools.games import TarotCard -from sat.core.i18n import _ - - -CARD_WIDTH = 74 -CARD_HEIGHT = 136 -CARD_DELTA_Y = 30 -MIN_WIDTH = 950 # Minimum size of the panel -MIN_HEIGHT = 500 - - -class CardWidget(TarotCard, Image, MouseHandler): - """This class is used to represent a card, graphically and logically""" - - def __init__(self, parent, file_): - """@param file: path of the PNG file""" - self._parent = parent - Image.__init__(self, file_) - root_name = file_[file_.rfind("/") + 1:-4] - suit, value = root_name.split('_') - TarotCard.__init__(self, (suit, value)) - MouseHandler.__init__(self) - self.addMouseListener(self) - - def onMouseEnter(self, sender): - if self._parent.state == "ecart" or self._parent.state == "play": - DOM.setStyleAttribute(self.getElement(), "top", "0px") - - def onMouseLeave(self, sender): - if not self in self._parent.hand: - return - if not self in list(self._parent.selected): # FIXME: Workaround pyjs bug, must report it - DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y) - - def onMouseUp(self, sender, x, y): - if self._parent.state == "ecart": - if self not in list(self._parent.selected): - self._parent.addToSelection(self) - else: - self._parent.removeFromSelection(self) - elif self._parent.state == "play": - self._parent.playCard(self) - - -class CardPanel(DockPanel, ClickHandler): - - def __init__(self, parent, referee, player_nick, players): - DockPanel.__init__(self) - ClickHandler.__init__(self) - self._parent = parent - self._autoplay = None # XXX: use 0 to activate fake play, None else - self.referee = referee - self.players = players - self.player_nick = player_nick - self.bottom_nick = self.player_nick - idx = self.players.index(self.player_nick) - idx = (idx + 1) % len(self.players) - self.right_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.top_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.left_nick = self.players[idx] - self.bottom_nick = player_nick - self.selected = set() # Card choosed by the player (e.g. during ecart) - self.hand_size = 13 # number of cards in a hand - self.hand = [] - self.to_show = [] - self.state = None - self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT) - self.setStyleName("cardPanel") - - # Now we set up the layout - _label = Label(self.top_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.NORTH) - self.setCellWidth(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER) - - self.hand_panel = AbsolutePanel() - self.add(self.hand_panel, DockPanel.SOUTH) - self.setCellWidth(self.hand_panel, '100%') - self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER) - - _label = Label(self.left_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.WEST) - self.setCellHeight(_label, '100%') - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - _label = Label(self.right_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.EAST) - self.setCellHeight(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - self.center_panel = DockPanel() - self.inner_left = SimplePanel() - self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_left, DockPanel.WEST) - self.center_panel.setCellHeight(self.inner_left, '100%') - self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT) - self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE) - - self.inner_right = SimplePanel() - self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_right, DockPanel.EAST) - self.center_panel.setCellHeight(self.inner_right, '100%') - self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE) - - self.inner_top = SimplePanel() - self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_top, DockPanel.NORTH) - self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM) - - self.inner_bottom = SimplePanel() - self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_bottom, DockPanel.SOUTH) - self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP) - - self.inner_center = SimplePanel() - self.center_panel.add(self.inner_center, DockPanel.CENTER) - self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE) - - self.add(self.center_panel, DockPanel.CENTER) - self.setCellWidth(self.center_panel, '100%') - self.setCellHeight(self.center_panel, '100%') - self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE) - self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER) - - self.loadCards() - self.mouse_over_card = None # contain the card to highlight - self.visible_size = CARD_WIDTH / 2 # number of pixels visible for cards - self.addClickListener(self) - - def loadCards(self): - """Load all the cards in memory""" - def _getTarotCardsPathsCb(paths): - log.debug("_getTarotCardsPathsCb") - for file_ in paths: - log.debug("path:", file_) - card = CardWidget(self, file_) - log.debug("card:", card) - self.cards[(card.suit, card.value)] = card - self.deck.append(card) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - self.cards = {} - self.deck = [] - self.cards["atout"] = {} # As Tarot is a french game, it's more handy & logical to keep french names - self.cards["pique"] = {} # spade - self.cards["coeur"] = {} # heart - self.cards["carreau"] = {} # diamond - self.cards["trefle"] = {} # club - self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb) - - def onClick(self, sender): - if self.state == "chien": - self.to_show = [] - self.state = "wait" - self.updateToShow() - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self.updateToShow() - self.updateHand() - - def tarotGameNew(self, hand): - """Start a new game, with given hand""" - if hand is []: # reset the display after the scores have been showed - self.selected.clear() - del self.hand[:] - del self.to_show[:] - self.state = None - #empty hand - self.updateHand() - #nothing on the table - self.updateToShow() - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - return - for suit, value in hand: - self.hand.append(self.cards[(suit, value)]) - self.hand.sort() - self.state = "init" - self.updateHand() - - def updateHand(self): - """Show the cards in the hand in the hand_panel (SOUTH panel)""" - self.hand_panel.clear() - self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10)) - x_pos = 0 - y_pos = CARD_DELTA_Y - for card in self.hand: - self.hand_panel.add(card, x_pos, y_pos) - x_pos += self.visible_size - - def updateToShow(self): - """Show cards in the center panel""" - if not self.to_show: - _widget = self.inner_center.getWidget() - if _widget: - self.inner_center.remove(_widget) - return - panel = AbsolutePanel() - panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT)) - x_pos = 0 - y_pos = 0 - for card in self.to_show: - panel.add(card, x_pos, y_pos) - x_pos += CARD_WIDTH + 5 - self.inner_center.setWidget(panel) - - def _ecartConfirm(self, confirm): - if not confirm: - return - ecart = [] - for card in self.selected: - ecart.append((card.suit, card.value)) - self.hand.remove(card) - self.selected.clear() - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart) - self.state = "wait" - self.updateHand() - - def addToSelection(self, card): - self.selected.add(card) - if len(self.selected) == 6: - ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameInvalidCards(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - - if phase == "play": - self.state = "play" - elif phase == "ecart": - self.state = "ecart" - else: - log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here - - for suit, value in played_cards: - self.hand.append(self.cards[(suit, value)]) - - self.hand.sort() - self.updateHand() - if self._autoplay == None: # No dialog if there is autoplay - Window.alert('Cards played are invalid !') - self.__fakePlay() - - def removeFromSelection(self, card): - self.selected.remove(card) - if len(self.selected) == 6: - ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameChooseContrat(self, xml_data): - """Called when the player has to select his contrat - @param xml_data: SàT xml representation of the form""" - body = XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL']) - _dialog = GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - - def tarotGameShowCards(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - self.to_show = [] - for suit, value in cards: - self.to_show.append(self.cards[(suit, value)]) - self.updateToShow() - if game_stage == "chien" and data['attaquant'] == self.player_nick: - self.state = "wait_for_ecart" - else: - self.state = "chien" - - def getPlayerLocation(self, nick): - """return player location (top,bottom,left or right)""" - for location in ['top', 'left', 'bottom', 'right']: - if getattr(self, '%s_nick' % location) == nick: - return location - log.error("This line should not be reached") - - def tarotGameCardsPlayed(self, player, cards): - """A card has been played by player""" - if not len(cards): - log.warning("cards should not be empty") - return - if len(cards) > 1: - log.error("can't manage several cards played") - if self.to_show: - self.to_show = [] - self.updateToShow() - suit, value = cards[0] - player_pos = self.getPlayerLocation(player) - player_panel = getattr(self, "inner_%s" % player_pos) - - if player_panel.getWidget() != None: - #We have already cards on the table, we remove them - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - - card = self.cards[(suit, value)] - DOM.setElemAttribute(card.getElement(), "style", "") - player_panel.setWidget(card) - - def tarotGameYourTurn(self): - """Called when we have to play :)""" - if self.state == "chien": - self.to_show = [] - self.updateToShow() - self.state = "play" - self.__fakePlay() - - def __fakePlay(self): - """Convenience method for stupid autoplay - /!\ don't forgot to comment any interactive dialog for invalid card""" - if self._autoplay == None: - return - if self._autoplay >= len(self.hand): - self._autoplay = 0 - card = self.hand[self._autoplay] - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - del self.hand[self._autoplay] - self.state = "wait" - self._autoplay += 1 - - def playCard(self, card): - self.hand.remove(card) - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - self.state = "wait" - self.updateHand() - - def tarotGameScore(self, xml_data, winners, loosers): - """Show score at the end of a round""" - if not winners and not loosers: - title = "Draw game" - else: - if self.player_nick in winners: - title = "You <b>win</b> !" - else: - title = "You <b>loose</b> :(" - body = XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) - _dialog = GenericDialog(title, body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show()
--- a/browser_side/contact.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,414 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from jid import JID -from pyjamas import Window -from pyjamas import DOM - -from browser_side.base_panels import PopupMenuPanel -from browser_side.base_widget import DragLabel -from browser_side.panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel -from browser_side.html_tools import html_sanitize -from __pyjamas__ import doc - - -def setPresenceStyle(element, presence, base_style="contact"): - """ - Set the CSS style of a contact's element according to its presence. - @param item: the UI element of the contact - @param presence: a value in ("", "chat", "away", "dnd", "xa"). - @param base_style: the base name of the style to apply - """ - if not hasattr(element, 'presence_style'): - element.presence_style = None - style = '%s-%s' % (base_style, presence or 'connected') - if style == element.presence_style: - return - if element.presence_style is not None: - element.removeStyleName(element.presence_style) - element.addStyleName(style) - element.presence_style = style - - -class GroupLabel(DragLabel, Label, ClickHandler): - def __init__(self, host, group): - self.group = group - self.host = host - Label.__init__(self, group) #, Element=DOM.createElement('div') - self.setStyleName('group') - DragLabel.__init__(self, group, "GROUP") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group) - - -class ContactLabel(DragLabel, HTML, ClickHandler): - def __init__(self, host, jid, name=None, handleClick=True): - HTML.__init__(self) - self.host = host - self.name = name or jid - self.waiting = False - self.jid = jid - self._fill() - self.setStyleName('contact') - DragLabel.__init__(self, jid, "CONTACT") - if handleClick: - ClickHandler.__init__(self) - self.addClickListener(self) - - def _fill(self): - if self.waiting: - _wait_html = "<b>(*)</b> " - self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html, - 'name': html_sanitize(self.name)}) - - def setMessageWaiting(self, waiting): - """Show a visual indicator if message are waiting - @param waiting: True if message are waiting""" - self.waiting = waiting - self._fill() - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(ChatPanel, self.jid) - - -class GroupList(VerticalPanel): - - def __init__(self, parent): - VerticalPanel.__init__(self) - self.setStyleName('groupList') - self._parent = parent - - def add(self, group): - _item = GroupLabel(self._parent.host, group) - _item.addMouseListener(self._parent) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - index = 0 - for group_ in [group.group for group in self.getChildren()]: - if group_ > group: - break - index += 1 - VerticalPanel.insert(self, _item, index) - - def remove(self, group): - for wid in self: - if isinstance(wid, GroupLabel) and wid.group == group: - VerticalPanel.remove(self, wid) - - -class GenericContactList(VerticalPanel): - """Class that can be used to represent a contact list, but not necessarily - the one that is displayed on the left side. Special features like popup menu - panel or changing the contact states must be done in a sub-class.""" - - def __init__(self, host, handleClick=False): - VerticalPanel.__init__(self) - self.host = host - self.contacts = [] - self.handleClick = handleClick - - def add(self, jid, name=None, item_cb=None): - if jid in self.contacts: - return - index = 0 - for contact_ in self.contacts: - if contact_ > jid: - break - index += 1 - self.contacts.insert(index, jid) - _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - VerticalPanel.insert(self, _item, index) - if item_cb is not None: - item_cb(_item) - - def remove(self, jid): - wid = self.getContactLabel(jid) - if not wid: - return - VerticalPanel.remove(self, wid) - self.contacts.remove(jid) - - def isContactPresent(self, contact_jid): - """Return True if a contact is present in the panel""" - return contact_jid in self.contacts - - def getContacts(self): - return self.contacts - - def getContactLabel(self, contact_jid): - """get contactList widget of a contact - @return: ContactLabel item if present, else None""" - for wid in self: - if isinstance(wid, ContactLabel) and wid.jid == contact_jid: - return wid - return None - - -class ContactList(GenericContactList): - """The contact list that is displayed on the left side.""" - - def __init__(self, host): - GenericContactList.__init__(self, host, handleClick=True) - self.menu_entries = {"blog": {"title": "Public blog..."}} - self.context_menu = PopupMenuPanel(entries=self.menu_entries, - hide=self.contextMenuHide, - callback=self.contextMenuCallback, - vertical=False, style={"selected": "menu-selected"}) - - def contextMenuHide(self, sender, key): - """Return True if the item for that sender should be hidden.""" - # TODO: enable the blogs of users that are on another server - return JID(sender.jid).domain != self.host._defaultDomain - - def contextMenuCallback(self, sender, key): - if key == "blog": - # TODO: use the bare when all blogs can be retrieved - node = JID(sender.jid).node - web_panel = WebPanel(self.host, "/blog/%s" % node) - self.host.addTab("%s's blog" % node, web_panel) - else: - sender.onClick(sender) - - def add(self, jid, name=None): - def item_cb(item): - self.context_menu.registerRightClickSender(item) - GenericContactList.add(self, jid, name, item_cb) - - def setState(self, jid, type_, state): - """Change the appearance of the contact, according to the state - @param jid: jid which need to change state - @param type_: one of availability, messageWaiting - @param state: - - for messageWaiting type: - True if message are waiting - - for availability type: - 'unavailable' if not connected, else presence like RFC6121 #4.7.2.1""" - _item = self.getContactLabel(jid) - if _item: - if type_ == 'availability': - setPresenceStyle(_item, state) - elif type_ == 'messageWaiting': - _item.setMessageWaiting(state) - - -class ContactTitleLabel(DragLabel, Label, ClickHandler): - def __init__(self, host, text): - Label.__init__(self, text) #, Element=DOM.createElement('div') - self.host = host - self.setStyleName('contactTitle') - DragLabel.__init__(self, text, "CONTACT_TITLE") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(MicroblogPanel, None) - - -class ContactPanel(SimplePanel): - """Manage the contacts and groups""" - - def __init__(self, host): - SimplePanel.__init__(self) - - self.scroll_panel = ScrollPanel() - - self.host = host - self.groups = {} - self.connected = {} # jid connected as key and their status - - self.vPanel = VerticalPanel() - _title = ContactTitleLabel(host, 'Contacts') - DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") - - self._contact_list = ContactList(host) - self._contact_list.setStyleName('contactList') - self._groupList = GroupList(self) - self._groupList.setStyleName('groupList') - - self.vPanel.add(_title) - self.vPanel.add(self._groupList) - self.vPanel.add(self._contact_list) - self.scroll_panel.add(self.vPanel) - self.add(self.scroll_panel) - self.setStyleName('contactBox') - Window.addWindowResizeListener(self) - - def onWindowResized(self, width, height): - contact_panel_elt = self.getElement() - classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), UniBoxPanel) else'gwt-TabBar' - _elts = doc().getElementsByClassName(classname) - if not _elts.length: - log.error("no element of class %s found, it should exist !" % classname) - tab_bar_h = height - else: - tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden - - ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 - self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) - - def updateContact(self, jid, attributes, groups): - """Add a contact to the panel if it doesn't exist, update it else - @param jid: jid userhost as unicode - @attributes: cf SàT Bridge API's newContact - @param groups: list of groups""" - _current_groups = self.getContactGroups(jid) - _new_groups = set(groups) - _key = "@%s: " - - for group in _current_groups.difference(_new_groups): - # We remove the contact from the groups where he isn't anymore - self.groups[group].remove(jid) - if not self.groups[group]: - # The group is now empty, we must remove it - del self.groups[group] - self._groupList.remove(group) - if self.host.uni_box: - self.host.uni_box.removeKey(_key % group) - - for group in _new_groups.difference(_current_groups): - # We add the contact to the groups he joined - if not group in self.groups.keys(): - self.groups[group] = set() - self._groupList.add(group) - if self.host.uni_box: - self.host.uni_box.addKey(_key % group) - self.groups[group].add(jid) - - # We add the contact to contact list, it will check if contact already exists - self._contact_list.add(jid) - - def removeContact(self, jid): - """Remove contacts from groups where he is and contact list""" - self.updateContact(jid, {}, []) # we remove contact from every group - self._contact_list.remove(jid) - - def setConnected(self, jid, resource, availability, priority, statuses): - """Set connection status - @param jid: JID userhost as unicode - """ - if availability == 'unavailable': - if jid in self.connected: - if resource in self.connected[jid]: - del self.connected[jid][resource] - if not self.connected[jid]: - del self.connected[jid] - else: - if not jid in self.connected: - self.connected[jid] = {} - self.connected[jid][resource] = (availability, priority, statuses) - - # check if the contact is connected with another resource, use the one with highest priority - if jid in self.connected: - max_resource = max_priority = None - for tmp_resource in self.connected[jid]: - if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority: - max_resource = tmp_resource - max_priority = self.connected[jid][tmp_resource][1] - if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one - priority = max_priority - availability = self.connected[jid][max_resource][0] - if jid not in self.connected or priority >= max_priority: - # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' - # case 2: update (or confirm) with the values of the resource which takes precedence - self._contact_list.setState(jid, "availability", availability) - - # update the connected contacts chooser live - if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: - self.host.room_contacts_chooser.resetContacts() - - def setContactMessageWaiting(self, jid, waiting): - """Show an visual indicator that contact has send a message - @param jid: jid of the contact - @param waiting: True if message are waiting""" - self._contact_list.setState(jid, "messageWaiting", waiting) - - def getConnected(self, filter_muc=False): - """return a list of all jid (bare jid) connected - @param filter_muc: if True, remove the groups from the list - """ - contacts = self.connected.keys() - contacts.sort() - return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) - - def getContactGroups(self, contact_jid): - """Get groups where contact is - @param group: string of single group, or list of string - @param contact_jid: jid to test - """ - result = set() - for group in self.groups: - if self.isContactInGroup(group, contact_jid): - result.add(group) - return result - - def isContactInGroup(self, group, contact_jid): - """Test if the contact_jid is in the group - @param group: string of single group, or list of string - @param contact_jid: jid to test - @return: True if contact_jid is in on of the groups""" - if group in self.groups and contact_jid in self.groups[group]: - return True - return False - - def isContactInRoster(self, contact_jid): - """Test if the contact is in our roster list""" - for _contact_label in self._contact_list: - if contact_jid == _contact_label.jid: - return True - return False - - def getContacts(self): - return self._contact_list.getContacts() - - def getGroups(self): - return self.groups.keys() - - def onMouseMove(self, sender, x, y): - pass - - def onMouseDown(self, sender, x, y): - pass - - def onMouseUp(self, sender, x, y): - pass - - def onMouseEnter(self, sender): - if isinstance(sender, GroupLabel): - for contact in self._contact_list: - if contact.jid in self.groups[sender.group]: - contact.addStyleName("selected") - - def onMouseLeave(self, sender): - if isinstance(sender, GroupLabel): - for contact in self._contact_list: - if contact.jid in self.groups[sender.group]: - contact.removeStyleName("selected") -
--- a/browser_side/contact_group.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,236 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. - -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.Timer import Timer -from pyjamas.ui.Button import Button -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui import HasAlignment -from browser_side.dialog import ConfirmDialog, InfoDialog -from list_manager import ListManager -from browser_side import dialog -from browser_side import contact - - -class ContactGroupManager(ListManager): - """A manager for sub-panels to assign contacts to each group.""" - - def __init__(self, parent, keys_dict, contact_list, offsets, style): - ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style) - self.registerPopupMenuPanel(entries={"Remove group": {}}, - callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key))) - - def removeContactKey(self, sender, key): - key = sender.getText() - - def confirm_cb(answer): - if answer: - ListManager.removeContactKey(self, key) - self._parent.removeKeyFromAddGroupPanel(key) - - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key) - _dialog.show() - - def removeFromRemainingList(self, contacts): - ListManager.removeFromRemainingList(self, contacts) - self._parent.updateContactList(contacts=contacts) - - def addToRemainingList(self, contacts, ignore_key=None): - ListManager.addToRemainingList(self, contacts, ignore_key) - self._parent.updateContactList(contacts=contacts) - - -class ContactGroupEditor(DockPanel): - """Panel for the contact groups manager.""" - - def __init__(self, host, parent=None, onCloseCallback=None): - DockPanel.__init__(self) - self.host = host - - # eventually display in a popup - if parent is None: - parent = DialogBox(autoHide=False, centered=True) - parent.setHTML("Manage contact groups") - self._parent = parent - self._on_close_callback = onCloseCallback - self.all_contacts = self.host.contact_panel.getContacts() - - groups_list = self.host.contact_panel.groups.keys() - groups_list.sort() - - self.add_group_panel = self.getAddGroupPanel(groups_list) - south_panel = self.getCloseSaveButtons() - center_panel = self.getContactGroupManager(groups_list) - east_panel = self.getContactList() - - self.add(self.add_group_panel, DockPanel.CENTER) - self.add(east_panel, DockPanel.EAST) - self.add(center_panel, DockPanel.NORTH) - self.add(south_panel, DockPanel.SOUTH) - - self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT) - self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP) - self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP) - self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM) - self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT) - self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM) - self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) - - # need to be done after the contact list has been initialized - self.groups.setContacts(self.host.contact_panel.groups) - self.toggleContacts(showAll=True) - - # Hide the contacts list from the main panel to not confuse the user - self.restore_contact_panel = False - if self.host.contact_panel.getVisible(): - self.restore_contact_panel = True - self.host.panel._contactsSwitch() - - parent.add(self) - parent.setVisible(True) - if isinstance(parent, DialogBox): - parent.center() - - def getContactGroupManager(self, groups_list): - """Set the list manager for the groups""" - flex_table = FlexTable(len(groups_list), 2) - flex_table.addStyleName('contactGroupEditor') - # overwrite the default style which has been set for rich text editor - style = { - "keyItem": "group", - "popupMenuItem": "popupMenuItem", - "removeButton": "contactGroupRemoveButton", - "buttonCell": "contactGroupButtonCell", - "keyPanel": "contactGroupPanel" - } - self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style) - self.groups.createWidgets() # widgets are automatically added to FlexTable - # FIXME: clean that part which is dangerous - flex_table.updateContactList = self.updateContactList - flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove - return flex_table - - def getAddGroupPanel(self, groups_list): - """Add the 'Add group' panel to the FlexTable""" - - def add_group_cb(text): - self.groups.addContactKey(text) - self.add_group_panel.textbox.setFocus(True) - - add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb) - add_group_panel.addStyleName("addContactGroupPanel") - return add_group_panel - - def getCloseSaveButtons(self): - """Add the buttons to close the dialog / save the groups""" - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - buttons.add(Button("Save", listener=self.closeAndSave)) - buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) - return buttons - - def getContactList(self): - """Add the contact list to the DockPanel""" - self.toggle = Button("", self.toggleContacts) - self.toggle.addStyleName("toggleAssignedContacts") - self.contacts = contact.GenericContactList(self.host) - for contact_ in self.all_contacts: - self.contacts.add(contact_) - contact_panel = VerticalPanel() - contact_panel.add(self.toggle) - contact_panel.add(self.contacts) - return contact_panel - - def toggleContacts(self, sender=None, showAll=None): - """Callback for the toggle button""" - if sender is None: - sender = self.toggle - sender.showAll = showAll if showAll is not None else not sender.showAll - if sender.showAll: - sender.setText("Hide assigned") - else: - sender.setText("Show assigned") - self.updateContactList(sender) - - def updateContactList(self, sender=None, contacts=None): - """Update the contact list regarding the toggle button""" - if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"): - return - sender = self.toggle - if contacts is not None: - if not isinstance(contacts, list): - contacts = [contacts] - for contact_ in contacts: - if contact_ not in self.all_contacts: - contacts.remove(contact_) - else: - contacts = self.all_contacts - for contact_ in contacts: - if sender.showAll: - self.contacts.getContactLabel(contact_).setVisible(True) - else: - if contact_ in self.groups.remaining_list: - self.contacts.getContactLabel(contact_).setVisible(True) - else: - self.contacts.getContactLabel(contact_).setVisible(False) - - def __close(self): - """Remove the widget from parent or close the popup.""" - if isinstance(self._parent, DialogBox): - self._parent.hide() - self._parent.remove(self) - if self._on_close_callback is not None: - self._on_close_callback() - if self.restore_contact_panel: - self.host.panel._contactsSwitch() - - def cancelWithoutSaving(self): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.__close() - - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?") - _dialog.show() - - def closeAndSave(self): - """Call bridge methods to save the changes and close the dialog""" - map_ = {} - for contact_ in self.all_contacts: - map_[contact_] = set() - contacts = self.groups.getContacts() - for group in contacts.keys(): - for contact_ in contacts[group]: - try: - map_[contact_].add(group) - except KeyError: - InfoDialog("Invalid contact", - "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) + - "Your changes could not be saved: please check your assignments and save again.", Width="400px").center() - return - for contact_ in map_.keys(): - groups = map_[contact_] - current_groups = self.host.contact_panel.getContactGroups(contact_) - if groups != current_groups: - self.host.bridge.call('updateContact', None, contact_, '', list(groups)) - self.__close()
--- a/browser_side/dialog.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,546 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.RadioButton import RadioButton -from pyjamas.ui import HasAlignment -from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER -from pyjamas.ui.MouseListener import MouseWheelHandler -from pyjamas import Window - -from base_panels import ToggleStackPanel - -from sat_frontends.tools.misc import DEFAULT_MUC - -# List here the patterns that are not allowed in contact group names -FORBIDDEN_PATTERNS_IN_GROUP = () - - -class RoomChooser(Grid): - """Select a room from the rooms you already joined, or create a new one""" - - GENERATE_MUC = "<use random name>" - - def __init__(self, host, default_room=DEFAULT_MUC): - Grid.__init__(self, 2, 2, Width='100%') - self.host = host - - self.new_radio = RadioButton("room", "Discussion room:") - self.new_radio.setChecked(True) - self.box = TextBox(Width='95%') - self.box.setText(self.GENERATE_MUC if default_room == "" else default_room) - self.exist_radio = RadioButton("room", "Already joined:") - self.rooms_list = ListBox(Width='95%') - - self.add(self.new_radio, 0, 0) - self.add(self.box, 0, 1) - self.add(self.exist_radio, 1, 0) - self.add(self.rooms_list, 1, 1) - - self.box.addFocusListener(self) - self.rooms_list.addFocusListener(self) - - self.exist_radio.setVisible(False) - self.rooms_list.setVisible(False) - self.setRooms() - - def onFocus(self, sender): - if sender == self.rooms_list: - self.exist_radio.setChecked(True) - elif sender == self.box: - if self.box.getText() == self.GENERATE_MUC: - self.box.setText("") - self.new_radio.setChecked(True) - - def onLostFocus(self, sender): - if sender == self.box: - if self.box.getText() == "": - self.box.setText(self.GENERATE_MUC) - - def setRooms(self): - for room in self.host.room_list: - self.rooms_list.addItem(room.bare) - if len(self.host.room_list) > 0: - self.exist_radio.setVisible(True) - self.rooms_list.setVisible(True) - self.exist_radio.setChecked(True) - - def getRoom(self): - if self.exist_radio.getChecked(): - values = self.rooms_list.getSelectedValues() - return "" if values == [] else values[0] - value = self.box.getText() - return "" if value == self.GENERATE_MUC else value - - -class ContactsChooser(VerticalPanel): - """Select one or several connected contacts""" - - def __init__(self, host, nb_contact=None, ok_button=None): - """ - @param host: SatWebFrontend instance - @param nb_contact: number of contacts that have to be selected, None for no limit - If a tuple is given instead of an integer, nb_contact[0] is the minimal and - nb_contact[1] is the maximal number of contacts to be chosen. - """ - self.host = host - if isinstance(nb_contact, tuple): - if len(nb_contact) == 0: - nb_contact = None - elif len(nb_contact) == 1: - nb_contact = (nb_contact[0], nb_contact[0]) - elif nb_contact is not None: - nb_contact = (nb_contact, nb_contact) - if nb_contact is None: - log.warning("Need to select as many contacts as you want") - else: - log.warning("Need to select between %d and %d contacts" % nb_contact) - self.nb_contact = nb_contact - self.ok_button = ok_button - VerticalPanel.__init__(self, Width='100%') - self.contacts_list = ListBox() - self.contacts_list.setMultipleSelect(True) - self.contacts_list.setWidth("95%") - self.contacts_list.addStyleName('contactsChooser') - self.contacts_list.addChangeListener(self.onChange) - self.add(self.contacts_list) - self.setContacts() - self.onChange() - - def onChange(self, sender=None): - if self.ok_button is None: - return - if self.nb_contact: - selected = len(self.contacts_list.getSelectedValues(True)) - if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: - self.ok_button.setEnabled(True) - else: - self.ok_button.setEnabled(False) - - def setContacts(self, selected=[]): - """Fill the list with the connected contacts - @param select: list of the contacts to select by default - """ - self.contacts_list.clear() - contacts = self.host.contact_panel.getConnected(filter_muc=True) - self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) - self.contacts_list.addItem("") - for contact in contacts: - if contact not in [room.bare for room in self.host.room_list]: - self.contacts_list.addItem(contact) - self.contacts_list.setItemTextSelection(selected) - - def getContacts(self): - return self.contacts_list.getSelectedValues(True) - - -class RoomAndContactsChooser(DialogBox): - """Select a room and some users to invite in""" - - def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", - title_room="Join room", title_invite="Invite contacts", visible=(True, True)): - DialogBox.__init__(self, centered=True) - self.host = host - self.callback = callback - self.title_room = title_room - self.title_invite = title_invite - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - ok_button = Button("OK", self.onOK) - button_panel.add(ok_button) - button_panel.add(Button("Cancel", self.onCancel)) - - self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) - self.contact_panel = ContactsChooser(host, nb_contact, ok_button) - - self.stack_panel = ToggleStackPanel(Width="100%") - self.stack_panel.add(self.room_panel, visible=visible[0]) - self.stack_panel.add(self.contact_panel, visible=visible[1]) - self.stack_panel.addStackChangeListener(self) - self.onStackChanged(self.stack_panel, 0, visible[0]) - self.onStackChanged(self.stack_panel, 1, visible[1]) - - main_panel = VerticalPanel() - main_panel.setStyleName("room-contact-chooser") - main_panel.add(self.stack_panel) - main_panel.add(button_panel) - - self.setWidget(main_panel) - self.setHTML(title) - self.show() - - # needed to update the contacts list when someone logged in/out - self.host.room_contacts_chooser = self - - def getRoom(self, asSuffix=False): - room = self.room_panel.getRoom() - if asSuffix: - return room if room == "" else ": %s" % room - else: - return room - - def getContacts(self, asSuffix=False): - contacts = self.contact_panel.getContacts() - if asSuffix: - return "" if contacts == [] else ": %s" % ", ".join(contacts) - else: - return contacts - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - if index == 0: - sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True))) - elif index == 1: - sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True))) - - def resetContacts(self): - """Called when someone log in/out to update the list""" - self.contact_panel.setContacts(self.getContacts()) - - def onOK(self, sender): - room_jid = self.getRoom() - if room_jid != "" and "@" not in room_jid: - Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain) - return - self.hide() - self.callback(room_jid, self.getContacts()) - - def onCancel(self, sender): - self.hide() - - def hide(self): - self.host.room_contacts_chooser = None - DialogBox.hide(self, autoClosed=True) - - -class GenericConfirmDialog(DialogBox): - - def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs): - """ - Dialog to confirm an action - @param widgets: widgets to attach - @param callback: method to call when a button is clicked - @param title: title of the dialog - @param prompt: textbox from which to retrieve the string value to be passed to the callback when - OK button is pressed. If None, OK button will return "True". Cancel button always returns "False". - """ - self.callback = callback - DialogBox.__init__(self, centered=True, **kwargs) - - content = VerticalPanel() - content.setWidth('100%') - for wid in widgets: - content.add(wid) - if wid == prompt: - wid.setWidth('100%') - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - self.confirm_button = Button("OK", self.onConfirm) - button_panel.add(self.confirm_button) - self.cancel_button = Button("Cancel", self.onCancel) - button_panel.add(self.cancel_button) - content.add(button_panel) - self.setHTML(title) - self.setWidget(content) - self.prompt = prompt - - def onConfirm(self, sender): - self.hide() - self.callback(self.prompt.getText() if self.prompt else True) - - def onCancel(self, sender): - self.hide() - self.callback(False) - - def show(self): - DialogBox.show(self) - if self.prompt: - self.prompt.setFocus(True) - - -class ConfirmDialog(GenericConfirmDialog): - - def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): - GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) - - -class GenericDialog(DialogBox): - """Dialog which just show a widget and a close button""" - - def __init__(self, title, main_widget, callback=None, options=None, **kwargs): - """Simple notice dialog box - @param title: HTML put in the header - @param main_widget: widget put in the body - @param callback: method to call on closing - @param options: one or more of the following options: - - NO_CLOSE: don't add a close button""" - DialogBox.__init__(self, centered=True, **kwargs) - self.callback = callback - if not options: - options = [] - _body = VerticalPanel() - _body.setSize('100%','100%') - _body.setSpacing(4) - _body.add(main_widget) - _body.setCellWidth(main_widget, '100%') - _body.setCellHeight(main_widget, '100%') - if not 'NO_CLOSE' in options: - _close_button = Button("Close", self.onClose) - _body.add(_close_button) - _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) - self.setHTML(title) - self.setWidget(_body) - self.panel.setSize('100%', '100%') #Need this hack to have correct size in Gecko & Webkit - - def close(self): - """Same effect as clicking the close button""" - self.onClose(None) - - def onClose(self, sender): - self.hide() - if self.callback: - self.callback() - - -class InfoDialog(GenericDialog): - - def __init__(self, title, body, callback=None, options=None, **kwargs): - GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) - - -class PromptDialog(GenericConfirmDialog): - - def __init__(self, callback, text='', title='User input', **kwargs): - prompt = TextBox() - prompt.setText(text) - GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs) - - -class PopupPanelWrapper(PopupPanel): - """This wrapper catch Escape event to avoid request cancellation by Firefox""" - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - #needed to prevent request cancellation in Firefox - event.preventDefault() - return PopupPanel.onEventPreview(self, event) - - -class ExtTextBox(TextBox): - """Extended TextBox""" - - def __init__(self, *args, **kwargs): - if 'enter_cb' in kwargs: - self.enter_cb = kwargs['enter_cb'] - del kwargs['enter_cb'] - TextBox.__init__(self, *args, **kwargs) - self.addKeyboardListener(self) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onKeyPress(self, sender, keycode, modifiers): - if self.enter_cb and keycode == KEY_ENTER: - self.enter_cb(self) - - -class GroupSelector(DialogBox): - - def __init__(self, top_widgets, initial_groups, selected_groups, - ok_title="OK", ok_cb=None, cancel_cb=None): - DialogBox.__init__(self, centered=True) - main_panel = VerticalPanel() - self.ok_cb = ok_cb - self.cancel_cb = cancel_cb - - for wid in top_widgets: - main_panel.add(wid) - - main_panel.add(Label('Select in which groups your contact is:')) - self.list_box = ListBox() - self.list_box.setMultipleSelect(True) - self.list_box.setVisibleItemCount(5) - self.setAvailableGroups(initial_groups) - self.setGroupsSelected(selected_groups) - main_panel.add(self.list_box) - - def cb(text): - self.list_box.addItem(text) - self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") - - main_panel.add(AddGroupPanel(initial_groups, cb)) - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - button_panel.add(Button(ok_title, self.onOK)) - button_panel.add(Button("Cancel", self.onCancel)) - main_panel.add(button_panel) - - self.setWidget(main_panel) - - def getSelectedGroups(self): - """Return a list of selected groups""" - return self.list_box.getSelectedValues() - - def setAvailableGroups(self, groups): - _groups = list(set(groups)) - _groups.sort() - self.list_box.clear() - for group in _groups: - self.list_box.addItem(group) - - def setGroupsSelected(self, selected_groups): - self.list_box.setItemTextSelection(selected_groups) - - def onOK(self, sender): - self.hide() - if self.ok_cb: - self.ok_cb(self) - - def onCancel(self, sender): - self.hide() - if self.cancel_cb: - self.cancel_cb(self) - - -class AddGroupPanel(HorizontalPanel): - def __init__(self, groups, cb=None): - """ - @param groups: list of the already existing groups - """ - HorizontalPanel.__init__(self) - self.groups = groups - self.add(Label('Add group:')) - self.textbox = ExtTextBox(enter_cb=self.onGroupInput) - self.add(self.textbox) - self.add(Button("add", lambda sender: self.onGroupInput(self.textbox))) - self.cb = cb - - def onGroupInput(self, sender): - text = sender.getText() - if text == "": - return - for group in self.groups: - if text == group: - Window.alert("The group '%s' already exists." % text) - return - for pattern in FORBIDDEN_PATTERNS_IN_GROUP: - if pattern in text: - Window.alert("The pattern '%s' is not allowed in group names." % pattern) - return - sender.setText('') - self.groups.append(text) - if self.cb is not None: - self.cb(text) - - -class WheelTextBox(TextBox, MouseWheelHandler): - - def __init__(self, *args, **kwargs): - TextBox.__init__(self, *args, **kwargs) - MouseWheelHandler.__init__(self) - - -class IntSetter(HorizontalPanel): - """This class show a bar with button to set an int value""" - - def __init__(self, label, value=0, value_max=None, visible_len=3): - """initialize the intSetter - @param label: text shown in front of the setter - @param value: initial value - @param value_max: limit value, None or 0 for unlimited""" - HorizontalPanel.__init__(self) - self.value = value - self.value_max = value_max - _label = Label(label) - self.add(_label) - self.setCellWidth(_label, "100%") - minus_button = Button("-", self.onMinus) - self.box = WheelTextBox() - self.box.setVisibleLength(visible_len) - self.box.setText(str(value)) - self.box.addInputListener(self) - self.box.addMouseWheelListener(self) - plus_button = Button("+", self.onPlus) - self.add(minus_button) - self.add(self.box) - self.add(plus_button) - self.valueChangedListener = [] - - def addValueChangeListener(self, listener): - self.valueChangedListener.append(listener) - - def removeValueChangeListener(self, listener): - if listener in self.valueChangedListener: - self.valueChangedListener.remove(listener) - - def _callListeners(self): - for listener in self.valueChangedListener: - listener(self.value) - - def setValue(self, value): - """Change the value and fire valueChange listeners""" - self.value = value - self.box.setText(str(value)) - self._callListeners() - - def onMinus(self, sender, step=1): - self.value = max(0, self.value - step) - self.box.setText(str(self.value)) - self._callListeners() - - def onPlus(self, sender, step=1): - self.value += step - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(str(self.value)) - self._callListeners() - - def onInput(self, sender): - """Accept only valid integer && normalize print (no leading 0)""" - try: - self.value = int(self.box.getText()) if self.box.getText() else 0 - except ValueError: - pass - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(str(self.value)) - self._callListeners() - - def onMouseWheel(self, sender, velocity): - if velocity > 0: - self.onMinus(sender, 10) - else: - self.onPlus(sender, 10)
--- a/browser_side/file_tools.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.FileUpload import FileUpload -from pyjamas.ui.FormPanel import FormPanel -from pyjamas import Window -from pyjamas import DOM -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label - - -class FilterFileUpload(FileUpload): - - def __init__(self, name, max_size, types=None): - """ - @param name: the input element name and id - @param max_size: maximum file size in MB - @param types: allowed types as a list of couples (x, y, z): - - x: MIME content type e.g. "audio/ogg" - - y: file extension e.g. "*.ogg" - - z: description for the user e.g. "Ogg Vorbis Audio" - If types is None, all file format are accepted - """ - FileUpload.__init__(self) - self.setName(name) - while DOM.getElementById(name): - name = "%s_" % name - self.setID(name) - self._id = name - self.max_size = max_size - self.types = types - - def getFileInfo(self): - from __pyjamas__ import JS - JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]") - - def check(self): - if self.getFilename() == "": - return False - (size, filetype) = self.getFileInfo() - if self.types and filetype not in [x for (x, y, z) in self.types]: - types = [] - for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]: - if type_ not in types: - types.append(type_) - Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types)) - return False - if size > self.max_size * pow(2, 20): - Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size) - return False - return True - - -class FileUploadPanel(FormPanel): - - def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None): - """Build a form panel to upload a file. - @param action_url: the form action URL - @param input_id: the input element name and id - @param max_size: maximum file size in MB - @param texts: a dict to ovewrite the default textual values - @param close_cb: the close button callback method - """ - FormPanel.__init__(self) - self.texts = {'ok_button': 'Upload file', - 'cancel_button': 'Cancel', - 'body': 'Please select a file.', - 'submitting': '<strong>Submitting, please wait...</strong>', - 'errback': "Your file has been rejected...", - 'body_errback': 'Please select another file.', - 'callback': "Your file has been accepted!"} - if isinstance(texts, dict): - self.texts.update(texts) - self.close_cb = close_cb - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction(action_url) - self.vPanel = VerticalPanel() - self.message = HTML(self.texts['body']) - self.vPanel.add(self.message) - - hPanel = HorizontalPanel() - hPanel.setSpacing(5) - hPanel.setStyleName('marginAuto') - self.file_upload = FilterFileUpload(input_id, max_size) - self.vPanel.add(self.file_upload) - - self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick")) - hPanel.add(self.upload_btn) - hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick"))) - - self.status = Label() - hPanel.add(self.status) - - self.vPanel.add(hPanel) - - self.add(self.vPanel) - self.addFormHandler(self) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def onCloseBtnClick(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def onSubmitBtnClick(self): - if not self.file_upload.check(): - return - self.message.setHTML(self.texts['submitting']) - self.upload_btn.setEnabled(False) - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result != "OK": - Window.alert(self.texts['errback']) - self.message.setHTML(self.texts['body_errback']) - self.upload_btn.setEnabled(True) - else: - Window.alert(self.texts['callback']) - self.close_cb()
--- a/browser_side/html_tools.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -from nativedom import NativeDOM -from sat_frontends.tools import xmltools -import re - -dom = NativeDOM() - - -def html_sanitize(html): - """Naive sanitization of HTML""" - return html.replace('<', '<').replace('>', '>') - - -def html_strip(html): - """Strip leading/trailing white spaces, HTML line breaks and sequences.""" - cleaned = re.sub(r"^(<br/?>| |\s)+", "", html) - cleaned = re.sub(r"(<br/?>| |\s)+$", "", cleaned) - return cleaned - - -def inlineRoot(xhtml): - """ make root element inline """ - doc = dom.parseString(xhtml) - return xmltools.inlineRoot(doc) - - -def convertNewLinesToXHTML(text): - return text.replace('\n', '<br/>')
--- a/browser_side/jid.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - - -class JID(object): - """This class help manage JID (Node@Domaine/Resource)""" - - def __init__(self, jid): - self.__raw = str(jid) - self.__parse() - - def __parse(self): - """find node domaine and resource""" - node_end = self.__raw.find('@') - if node_end < 0: - node_end = 0 - domain_end = self.__raw.find('/') - if domain_end < 1: - domain_end = len(self.__raw) - self.node = self.__raw[:node_end] - self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end] - self.resource = self.__raw[domain_end + 1:] - if not node_end: - self.bare = self.__raw - else: - self.bare = self.node + '@' + self.domain - - def __str__(self): - return self.__raw.__str__() - - def is_valid(self): - """return True if the jid is xmpp compliant""" - #FIXME: always return True for the moment - return True - - def __eq__(self, other): - """Redefine equality operator to implement the naturally expected test""" - return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- a/browser_side/list_manager.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,608 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.Grid import Grid -from pyjamas.ui.Button import Button -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.AutoComplete import AutoCompleteTextBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.KeyboardListener import KEY_ENTER -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.DropWidget import DropWidget -from pyjamas.Timer import Timer -from pyjamas import DOM - -from base_panels import PopupMenuPanel -from base_widget import DragLabel - -# HTML content for the removal button (image or text) -REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>' - -# Item to be considered for an empty list box selection. -# Could be whatever which doesn't look like a JID or a group name. -EMPTY_SELECTION_ITEM = "" - - -class ListManager(): - """A manager for sub-panels to assign elements to lists.""" - - def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}): - """ - @param parent: FlexTable parent widget for the manager - @param keys_dict: dict with the contact keys mapped to data - @param contact_list: list of string (the contact JID userhosts) - @param offsets: dict to set widget positions offset within parent - - "x_first": the x offset for the first widget's row on the grid - - "x": the x offset for all widgets rows, except the first one if "x_first" is defined - - "y": the y offset for all widgets columns on the grid - """ - self._parent = parent - if isinstance(keys_dict, set) or isinstance(keys_dict, list): - tmp = {} - for key in keys_dict: - tmp[key] = {} - keys_dict = tmp - self.__keys_dict = keys_dict - if isinstance(contact_list, set): - contact_list = list(contact_list) - self.__list = contact_list - self.__list.sort() - # store the list of contacts that are not assigned yet - self.__remaining_list = [] - self.__remaining_list.extend(self.__list) - # mark a change to sort the list before it's used - self.__remaining_list_sorted = True - - self.offsets = {"x_first": 0, "x": 0, "y": 0} - if "x" in offsets and not "x_first" in offsets: - offsets["x_first"] = offsets["x"] - self.offsets.update(offsets) - - self.style = { - "keyItem": "recipientTypeItem", - "popupMenuItem": "recipientTypeItem", - "buttonCell": "recipientButtonCell", - "dragoverPanel": "dragover-recipientPanel", - "keyPanel": "recipientPanel", - "textBox": "recipientTextBox", - "textBox-invalid": "recipientTextBox-invalid", - "removeButton": "recipientRemoveButton", - } - self.style.update(style) - - def createWidgets(self, title_format="%s"): - """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" - self.__children = {} - for key in self.__keys_dict: - self.addContactKey(key, title_format=title_format) - - def addContactKey(self, key, dict_={}, title_format="%s"): - if key not in self.__keys_dict: - self.__keys_dict[key] = dict_ - # copy the key to its associated sub-map - self.__keys_dict[key]["title"] = key - self._addChild(self.__keys_dict[key], title_format) - - def removeContactKey(self, key): - """Remove a list panel and all its associated data.""" - contacts = self.__children[key]["panel"].getContacts() - (y, x) = self._parent.getIndex(self.__children[key]["button"]) - self._parent.removeRow(y) - del self.__children[key] - del self.__keys_dict[key] - self.addToRemainingList(contacts) - - def _addChild(self, entry, title_format): - """Add a button and FlowPanel for the corresponding map entry.""" - button = Button(title_format % entry["title"]) - button.setStyleName(self.style["keyItem"]) - if hasattr(entry, "desc"): - button.setTitle(entry["desc"]) - if not "optional" in entry: - entry["optional"] = False - button.setVisible(not entry["optional"]) - y = len(self.__children) + self.offsets["y"] - x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] - - self._parent.insertRow(y) - self._parent.setWidget(y, x, button) - self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) - - _child = ListPanel(self, entry, self.style) - self._parent.setWidget(y, x + 1, _child) - - self.__children[entry["title"]] = {} - self.__children[entry["title"]]["button"] = button - self.__children[entry["title"]]["panel"] = _child - - if hasattr(self, "popup_menu"): - # this is done if self.registerPopupMenuPanel has been called yet - self.popup_menu.registerClickSender(button) - - def _refresh(self, visible=True): - """Set visible the sub-panels that are non optional or non empty, hide the rest.""" - for key in self.__children: - self.setContactPanelVisible(key, False) - if not visible: - return - _map = self.getContacts() - for key in _map: - if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: - self.setContactPanelVisible(key, True) - - def setVisible(self, visible): - self._refresh(visible) - - def setContactPanelVisible(self, key, visible=True, sender=None): - """Do not remove the "sender" param as it is needed for the context menu.""" - self.__children[key]["button"].setVisible(visible) - self.__children[key]["panel"].setVisible(visible) - - @property - def list(self): - """Return the full list of potential contacts.""" - return self.__list - - @property - def keys(self): - return self.__keys_dict.keys() - - @property - def keys_dict(self): - return self.__keys_dict - - @property - def remaining_list(self): - """Return the contacts that have not been selected yet.""" - if not self.__remaining_list_sorted: - self.__remaining_list_sorted = True - self.__remaining_list.sort() - return self.__remaining_list - - def setRemainingListUnsorted(self): - """Mark a change (deletion) so the list will be sorted before it's used.""" - self.__remaining_list_sorted = False - - def removeFromRemainingList(self, contacts): - """Remove contacts after they have been added to a sub-panel.""" - if not isinstance(contacts, list): - contacts = [contacts] - for contact_ in contacts: - if contact_ in self.__remaining_list: - self.__remaining_list.remove(contact_) - - def addToRemainingList(self, contacts, ignore_key=None): - """Add contacts after they have been removed from a sub-panel.""" - if not isinstance(contacts, list): - contacts = [contacts] - assigned_contacts = set() - assigned_map = self.getContacts() - for key_ in assigned_map.keys(): - if ignore_key is not None and key_ == ignore_key: - continue - assigned_contacts.update(assigned_map[key_]) - for contact_ in contacts: - if contact_ not in self.__list or contact_ in self.__remaining_list: - continue - if contact_ in assigned_contacts: - continue # the contact is assigned somewhere else - self.__remaining_list.append(contact_) - self.setRemainingListUnsorted() - - def setContacts(self, _map={}): - """Set the contacts for each contact key.""" - for key in self.__keys_dict: - if key in _map: - self.__children[key]["panel"].setContacts(_map[key]) - else: - self.__children[key]["panel"].setContacts([]) - self._refresh() - - def getContacts(self): - """Get the contacts for all the lists. - @return: a mapping between keys and contact lists.""" - _map = {} - for key in self.__children: - _map[key] = self.__children[key]["panel"].getContacts() - return _map - - @property - def target_drop_cell(self): - """@return: the panel where something has been dropped.""" - return self._target_drop_cell - - def setTargetDropCell(self, target_drop_cell): - """@param: target_drop_cell: the panel where something has been dropped.""" - self._target_drop_cell = target_drop_cell - - def registerPopupMenuPanel(self, entries, hide, callback): - "Register a popup menu panel that will be bound to all contact keys elements." - self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) - - -class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler): - """A draggable AutoCompleteTextBox which is used for representing a contact. - This class is NOT generic because of the onDragEnd method which call methods - from ListPanel. It's probably not reusable for another scenario. - """ - - def __init__(self, parent, event_cbs, style): - AutoCompleteTextBox.__init__(self) - DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type - self._parent = parent - self.event_cbs = event_cbs - self.style = style - self.addMouseListener(self) - self.addFocusListener(self) - self.addChangeListener(self) - self.addStyleName(style["textBox"]) - self.reset() - - def reset(self): - self.setText("") - self.setValid() - - def setValid(self, valid=True): - if self.getText() == "": - valid = True - if valid: - self.removeStyleName(self.style["textBox-invalid"]) - else: - self.addStyleName(self.style["textBox-invalid"]) - self.valid = valid - - def onDragStart(self, event): - self._text = self.getText() - DragLabel.onDragStart(self, event) - self._parent.setTargetDropCell(None) - self.setSelectionRange(len(self.getText()), 0) - - def onDragEnd(self, event): - target = self._parent.target_drop_cell # parent or another ListPanel - if self.getText() == "" or target is None: - return - self.event_cbs["drop"](self, target) - - def setRemoveButton(self): - - def remove_cb(sender): - """Callback for the button to remove this contact.""" - self._parent.remove(self) - self._parent.remove(self.remove_btn) - self.event_cbs["remove"](self) - - self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) - self.remove_btn.setStyleName(self.style["removeButton"]) - self._parent.add(self.remove_btn) - - def removeOrReset(self): - if hasattr(self, "remove_btn"): - self.remove_btn.click() - else: - self.reset() - - def onMouseMove(self, sender): - """Mouse enters the area of a DragAutoCompleteTextBox.""" - if hasattr(sender, "remove_btn"): - sender.remove_btn.setVisible(True) - - def onMouseLeave(self, sender): - """Mouse leaves the area of a DragAutoCompleteTextBox.""" - if hasattr(sender, "remove_btn"): - Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) - - def onFocus(self, sender): - sender.setSelectionRange(0, len(self.getText())) - self.event_cbs["focus"](sender) - - def validate(self): - self.setSelectionRange(len(self.getText()), 0) - self.event_cbs["validate"](self) - - def onChange(self, sender): - """The textbox or list selection is changed""" - if isinstance(sender, ListBox): - AutoCompleteTextBox.onChange(self, sender) - self.validate() - - def onClick(self, sender): - """The list is clicked""" - AutoCompleteTextBox.onClick(self, sender) - self.validate() - - def onKeyUp(self, sender, keycode, modifiers): - """Listen for ENTER key stroke""" - AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) - if keycode == KEY_ENTER: - self.validate() - - -class DropCell(DropWidget): - """A cell where you can drop widgets. This class is NOT generic because of - onDrop which uses methods from ListPanel. It has been created to - separate the drag and drop methods from the others and add a bit of - lisibility, but it's probably not reusable for another scenario. - """ - - def __init__(self, drop_cbs): - DropWidget.__init__(self) - self.drop_cbs = drop_cbs - - def onDragEnter(self, event): - self.addStyleName(self.style["dragoverPanel"]) - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - 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, we don't want the style to be removed - self.removeStyleName(self.style["dragoverPanel"]) - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def onDrop(self, event): - DOM.eventPreventDefault(event) - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed - if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and - item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report - if item_type in self.drop_cbs.keys(): - self.drop_cbs[item_type](self, item) - self.removeStyleName(self.style["dragoverPanel"]) - - -VALID = 1 -INVALID = 2 -DELETE = 3 - - -class ListPanel(FlowPanel, DropCell): - """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel - is not fully implemented yet and can not be used with pyjamas.ui.Label.""" - - def __init__(self, parent, entry, style={}): - """Initialization with a button and a DragAutoCompleteTextBox.""" - FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) - drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), - "CONTACT": lambda panel, item: self.addContact(item), - "CONTACT_TITLE": lambda panel, item: self.addContact('@@'), - "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) - } - DropCell.__init__(self, drop_cbs) - self.style = style - self.addStyleName(self.style["keyPanel"]) - self._parent = parent - self.key = entry["title"] - self._addTextBox() - - def _addTextBox(self, switchPrevious=False): - """Add a text box to the last position. If switchPrevious is True, simulate - an insertion before the current last textbox by copying the text and valid state. - @return: the created textbox or the previous one if switchPrevious is True. - """ - if hasattr(self, "_last_textbox"): - if self._last_textbox.getText() == "": - return - self._last_textbox.setRemoveButton() - else: - switchPrevious = False - - def focus_cb(sender): - if sender != self._last_textbox: - # save the current value before it's being modified - self._parent.addToRemainingList(sender.getText(), ignore_key=self.key) - sender.setCompletionItems(self._parent.remaining_list) - - def remove_cb(sender): - """Callback for the button to remove this contact.""" - self._parent.addToRemainingList(sender.getText()) - self._parent.setRemainingListUnsorted() - self._last_textbox.setFocus(True) - - def drop_cb(sender, target): - """Callback when the textbox is drag-n-dropped.""" - parent = sender._parent - if target != parent and target.addContact(sender.getText()): - sender.removeOrReset() - else: - parent._parent.removeFromRemainingList(sender.getText()) - - events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} - textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) - self.add(textbox) - if switchPrevious: - textbox.setText(self._last_textbox.getText()) - textbox.setValid(self._last_textbox.valid) - self._last_textbox.reset() - previous = self._last_textbox - self._last_textbox = textbox - return previous if switchPrevious else textbox - - def _checkContact(self, contact, modify): - """ - @param contact: the contact to check - @param modify: True if the contact is being modified - @return: - - VALID if the contact is valid - - INVALID if the contact is not valid but can be displayed - - DELETE if the contact should not be displayed at all - """ - def countItemInList(list_, item): - """For some reason the built-in count function doesn't work...""" - count = 0 - for elem in list_: - if elem == item: - count += 1 - return count - if contact is None or contact == "": - return DELETE - if countItemInList(self.getContacts(), contact) > (1 if modify else 0): - return DELETE - return VALID if contact in self._parent.list else INVALID - - def addContact(self, contact, sender=None): - """The first parameter type is checked, so it is also possible to call addContact(sender). - If contact is not defined, sender.getText() is used. If sender is not defined, contact will - be written to the last textbox and a new textbox is added afterward. - @param contact: unicode - @param sender: DragAutoCompleteTextBox instance - """ - if isinstance(contact, DragAutoCompleteTextBox): - sender = contact - contact = sender.getText() - valid = self._checkContact(contact, sender is not None) - if sender is None: - # method has been called to modify but to add a contact - if valid == VALID: - # eventually insert before the last textbox if it's not empty - sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox - sender.setText(contact) - else: - sender.setValid(valid == VALID) - if valid != VALID: - if sender is not None and valid == DELETE: - sender.removeOrReset() - return False - if sender == self._last_textbox: - self._addTextBox() - try: - sender.setVisibleLength(len(contact)) - except: - # IndexSizeError: Index or size is negative or greater than the allowed amount - log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact))) - self._parent.removeFromRemainingList(contact) - self._last_textbox.setFocus(True) - return True - - def emptyContacts(self): - """Empty the list of contacts.""" - for child in self.getChildren(): - if hasattr(child, "remove_btn"): - child.remove_btn.click() - - def setContacts(self, tab): - """Set the contacts.""" - self.emptyContacts() - if isinstance(tab, set): - tab = list(tab) - tab.sort() - for contact in tab: - self.addContact(contact) - - def getContacts(self): - """Get the contacts - @return: an array of string""" - tab = [] - for widget in self.getChildren(): - if isinstance(widget, DragAutoCompleteTextBox): - # not to be mixed with EMPTY_SELECTION_ITEM - if widget.getText() != "": - tab.append(widget.getText()) - return tab - - @property - def target_drop_cell(self): - """@return: the panel where something has been dropped.""" - return self._parent.target_drop_cell - - def setTargetDropCell(self, target_drop_cell): - """ - XXX: Property setter here would not make it, you need a proper method! - @param target_drop_cell: the panel where something has been dropped.""" - self._parent.setTargetDropCell(target_drop_cell) - - -class ContactChooserPanel(DialogBox): - """Display the contacts chooser dialog. This has been implemented while - prototyping and is currently not used. Left for an eventual later use. - Replaced by the popup menu which allows to add a panel for Cc or Bcc. - """ - - def __init__(self, manager, **kwargs): - """Display a listbox for each contact key""" - DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) - self.setHTML("Select contacts") - self.manager = manager - self.listboxes = {} - self.contacts = manager.getContacts() - - container = VerticalPanel(Visible=True) - container.addStyleName("marginAuto") - - grid = Grid(2, len(self.manager.keys_dict)) - index = -1 - for key in self.manager.keys_dict: - index += 1 - grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index) - listbox = ListBox() - listbox.setMultipleSelect(True) - listbox.setVisibleItemCount(15) - listbox.addItem(EMPTY_SELECTION_ITEM) - for element in manager.list: - listbox.addItem(element) - self.listboxes[key] = listbox - grid.add(listbox, 1, index) - self._reset() - - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - btn_close = Button("Cancel", self.hide) - buttons.add(btn_close) - btn_reset = Button("Reset", self._reset) - buttons.add(btn_reset) - btn_ok = Button("OK", self._validate) - buttons.add(btn_ok) - - container.add(grid) - container.add(buttons) - - self.add(container) - self.center() - - def _reset(self): - """Reset the selections.""" - for key in self.manager.keys_dict: - listbox = self.listboxes[key] - for i in xrange(0, listbox.getItemCount()): - if listbox.getItemText(i) in self.contacts[key]: - listbox.setItemSelected(i, "selected") - else: - listbox.setItemSelected(i, "") - - def _validate(self): - """Sets back the selected contacts to the good sub-panels.""" - _map = {} - for key in self.manager.keys_dict: - selections = self.listboxes[key].getSelectedItemText() - if EMPTY_SELECTION_ITEM in selections: - selections.remove(EMPTY_SELECTION_ITEM) - _map[key] = selections - self.manager.setContacts(_map) - self.hide()
--- a/browser_side/logging.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. -"""This module configure logs for Libervia browser side""" - -from __pyjamas__ import console -from constants import Const as C -from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas - - -class LiberviaLogger(log.Logger): - - def out(self, message, level=None): - if level == C.LOG_LVL_DEBUG: - console.debug(message) - elif level == C.LOG_LVL_INFO: - console.info(message) - elif level == C.LOG_LVL_WARNING: - console.warn(message) - else: - console.error(message) - - -def configure(): - fmt = '[%(name)s] %(message)s' - log.configure(C.LOG_BACKEND_CUSTOM, - logger_class = LiberviaLogger, - level = C.LOG_LVL_DEBUG, - fmt = fmt, - output = None, - logger = None, - colors = False, - force_colors = False) - # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed - LiberviaLogger.fmt = fmt
--- a/browser_side/menu.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,267 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.MenuBar import MenuBar -from pyjamas.ui.MenuItem import MenuItem -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Frame import Frame -from pyjamas import Window -from jid import JID -from file_tools import FileUploadPanel -from xmlui import XMLUI -from browser_side import panels -from browser_side import dialog -from contact_group import ContactGroupEditor -from sat.core.i18n import _ - - -class MenuCmd: - - def __init__(self, object_, handler): - self._object = object_ - self._handler = handler - - def execute(self): - handler = getattr(self._object, self._handler) - handler() - - -class PluginMenuCmd: - - def __init__(self, host, action_id): - self.host = host - self.action_id = action_id - - def execute(self): - self.host.launchAction(self.action_id, None) - - -class LiberviaMenuBar(MenuBar): - - def __init__(self): - MenuBar.__init__(self, vertical=False) - self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal") - # TODO: properly adapt CSS to the new class name - - def doItemAction(self, item, fireCommand): - MenuBar.doItemAction(self, item, fireCommand) - if item == self.items[-1] and self.popup: - self.popup.setPopupPosition(Window.getClientWidth() - - self.popup.getOffsetWidth() - 22, - self.getAbsoluteTop() + - self.getOffsetHeight() - 1) - self.popup.addStyleName('menuLastPopup') - - -class AvatarUpload(FileUploadPanel): - def __init__(self): - texts = {'ok_button': 'Upload avatar', - 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.', - 'errback': "Can't open image... did you actually submit an image?", - 'body_errback': 'Please select another image file.', - 'callback': "Your new profile picture has been set!"} - FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts) - - -class Menu(SimplePanel): - - def __init__(self, host): - self.host = host - SimplePanel.__init__(self) - self.setStyleName('menuContainer') - - def createMenus(self, add_menus): - _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s" - menus_dict = {} - menus_order = [] - - def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd): - """ add a menu to menu_dict """ - log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd)) - try: - menu_bar = menus_dict[menu_name] - except KeyError: - menu_bar = menus_dict[menu_name] = MenuBar(vertical=True) - menus_order.append((menu_name, menu_name_i18n, icon)) - if item_name_i18n and menu_cmd: - menu_bar.addItem(item_name_i18n, menu_cmd) - - addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget")) - addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect")) - addMenu("Contacts", _("Contacts"), None, 'social', None) - addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom")) - addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio")) - addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame")) - addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame")) - - # additional menus - for action_id, type_, path, path_i18n in add_menus: - if not path: - log.warning("skipping menu without path") - continue - if len(path) != len(path_i18n): - log.error("inconsistency between menu paths") - continue - menu_name = path[0] - menu_name_i18n = path_i18n[0] - item_name = path[1:] - if not item_name: - log.warning("skipping menu with a path of lenght 1 [%s]" % path[0]) - continue - item_name_i18n = ' | '.join(path_i18n[1:]) - addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id)) - - # menu items that should be displayed after the automatically added ones - addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups")) - - menus_order.append(None) # we add separator - - addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract")) - addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout")) - addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount")) - addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters")) - - # XXX: temporary, will change when a full profile will be managed in SàT - addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload")) - - menubar = LiberviaMenuBar() - - for menu_data in menus_order: - if menu_data is None: - _separator = MenuItem('', None) - _separator.setStyleName('menuSeparator') - menubar.addItem(_separator, None) - else: - menu_name, menu_name_i18n, icon = menu_data - menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name])) - - self.add(menubar) - - #General menu - def onWebWidget(self): - web_panel = panels.WebPanel(self.host, "http://www.goffi.org") - self.host.addWidget(web_panel) - self.host.setSelected(web_panel) - - def onDisconnect(self): - def confirm_cb(answer): - if answer: - log.info("disconnection") - self.host.bridge.call('disconnect', None) - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?") - _dialog.show() - - def onSocialContract(self): - _frame = Frame('contrat_social.html') - _frame.setStyleName('infoFrame') - _dialog = dialog.GenericDialog("Contrat Social", _frame) - _dialog.setSize('80%', '80%') - _dialog.show() - - def onAbout(self): - _about = HTML("""<b>Libervia</b>, a Salut à Toi project<br /> -<br /> -You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br /> -Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br /> -Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br /> -<br /> -Any help welcome :) -<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p> -""") - _dialog = dialog.GenericDialog("About", _about) - _dialog.show() - - #Contact menu - def onManageContactGroups(self): - """Open the contact groups manager.""" - - def onCloseCallback(): - pass - - ContactGroupEditor(self.host, None, onCloseCallback) - - #Group menu - def onJoinRoom(self): - - def invite(room_jid, contacts): - for contact in contacts: - self.host.bridge.call('inviteMUC', None, contact, room_jid) - - def join(room_jid, contacts): - if self.host.whoami: - nick = self.host.whoami.node - if room_jid not in [room.bare for room in self.host.room_list]: - self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick) - else: - self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare) - invite(room_jid, contacts) - - dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False)) - - def onCollectiveRadio(self): - def callback(room_jid, contacts): - self.host.bridge.call('launchRadioCollective', None, contacts, room_jid) - dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True)) - - #Game menu - def onTarotGame(self): - def onPlayersSelected(room_jid, other_players): - self.host.bridge.call('launchTarotGame', None, other_players, room_jid) - dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True)) - - def onXiangqiGame(self): - Window.alert("A Xiangqi game is planed, but not available yet") - - #Settings menu - - def onAccount(self): - def gotUI(xmlui): - if not xmlui: - return - body = XMLUI(self.host, xmlui) - _dialog = dialog.GenericDialog("Manage your XMPP account", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - self.host.bridge.call('getAccountDialogUI', gotUI) - - def onParameters(self): - def gotParams(xmlui): - if not xmlui: - return - body = XMLUI(self.host, xmlui) - _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setSize('80%', '80%') - _dialog.show() - self.host.bridge.call('getParamsUI', gotParams) - - def removeItemParams(self): - """Remove the Parameters item from the Settings menu bar.""" - self.menu_settings.removeItem(self.item_params) - - def onAvatarUpload(self): - body = AvatarUpload() - _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setWidth('40%') - _dialog.show()
--- a/browser_side/nativedom.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -""" -This class provide basic DOM parsing based on native javascript parser -__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989 -""" - -from __pyjamas__ import JS - - -class Node(): - - def __init__(self, js_node): - self._node = js_node - - def _jsNodesList2List(self, js_nodes_list): - ret=[] - for i in range(len(js_nodes_list)): - #ret.append(Element(js_nodes_list.item(i))) - ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug - return ret - - @property - def nodeName(self): - return self._node.nodeName - - @property - def wholeText(self): - return self._node.wholeText - - @property - def childNodes(self): - return self._jsNodesList2List(self._node.childNodes) - - def getAttribute(self, attr): - return self._node.getAttribute(attr) - - def setAttribute(self, attr, value): - return self._node.setAttribute(attr, value) - - def hasAttribute(self, attr): - return self._node.hasAttribute(attr) - - def toxml(self): - return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""") - - -class Element(Node): - - def __init__(self, js_node): - Node.__init__(self, js_node) - - def getElementsByTagName(self, tagName): - return self._jsNodesList2List(self._node.getElementsByTagName(tagName)) - - -class Document(Node): - - def __init__(self, js_document): - Node.__init__(self, js_document) - - @property - def documentElement(self): - return Element(self._node.documentElement) - - -class NativeDOM: - - def __init__(self): - JS(""" - - if (typeof window.DOMParser != "undefined") { - this.parseXml = function(xmlStr) { - return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); - }; - } else if (typeof window.ActiveXObject != "undefined" && - new window.ActiveXObject("Microsoft.XMLDOM")) { - this.parseXml = function(xmlStr) { - var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlStr); - return xmlDoc; - }; - } else { - throw new Error("No XML parser found"); - } - """) - - def parseString(self, xml): - return Document(self.parseXml(xml)) -
--- a/browser_side/notification.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,120 +0,0 @@ -from __pyjamas__ import JS, wnd -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas import Window -from pyjamas.Timer import Timer -from browser_side import dialog -from sat.core.i18n import _ - -TIMER_DELAY = 5000 - - -class Notification(object): - """ - If the browser supports it, the user allowed it to and the tab is in the - background, send desktop notifications on messages. - - Requires both Web Notifications and Page Visibility API. - """ - - def __init__(self): - self.enabled = False - user_agent = None - notif_permission = None - JS(""" - if (!('hidden' in document)) - document.hidden = false; - - user_agent = navigator.userAgent - - if (!('Notification' in window)) - return; - - notif_permission = Notification.permission - - if (Notification.permission === 'granted') - this.enabled = true; - - else if (Notification.permission === 'default') { - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - - self.enabled = true; //need to use self instead of this - }); - } - """) - - if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']: - self.user_agent = user_agent - self._installChromiumWorkaround() - - wnd().onfocus = self.onFocus - # wnd().onblur = self.onBlur - self._notif_count = 0 - self._orig_title = Window.getTitle() - - def _installChromiumWorkaround(self): - # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event - # see https://code.google.com/p/chromium/issues/detail?id=274284 - # FIXME: need to be removed if Chromium behaviour changes - try: - version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1] - version = int(version_full.split('.')[0]) - except (IndexError, ValueError): - log.warning("Can't find Chromium version") - version = 0 - log.info("Chromium version: %d" % (version,)) - if version < 22: - log.info("Notification use the old prefixed version or are unmanaged") - return - if version < 32: - dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show() - return - - log.info("==> Installing Chromium notifications request workaround <==") - self._old_click = wnd().onclick - wnd().onclick = self._chromiumWorkaround - - def _chromiumWorkaround(self): - log.info("Activating workaround") - JS(""" - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - self.enabled = true; //need to use self instead of this - }); - """) - wnd().onclick = self._old_click - - def onFocus(self): - Window.setTitle(self._orig_title) - self._notif_count = 0 - - # def onBlur(self): - # pass - - def isHidden(self): - JS("""return document.hidden;""") - - def _notify(self, title, body, icon): - if not self.enabled: - return - notification = None - JS(""" - notification = new Notification(title, {body: body, icon: icon}); - // Probably won’t work, but it doesn’t hurt to try. - notification.addEventListener('click', function() { - window.focus(); - }); - """) - notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close()) - - def highlightTab(self): - self._notif_count += 1 - Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count)) - - def notify(self, title, body, icon='/media/icons/apps/48/sat.png'): - if self.isHidden(): - self._notify(title, body, icon) - self.highlightTab()
--- a/browser_side/panels.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1408 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -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.FlowPanel import FlowPanel -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.Timer import Timer -from pyjamas import DOM -from pyjamas import Window -from __pyjamas__ import doc - -from datetime import datetime -from time import time -from jid import JID - -from html_tools import html_sanitize -from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor -from card_game import CardPanel -from radiocol import RadioColPanel -from menu import Menu -from browser_side import dialog -from browser_side import base_widget -from browser_side import richtext -from browser_side import contact - -from constants import Const as C -from plugin_xep_0085 import ChatStateMachine -from sat_frontends.tools.strings import addURLToText -from sat_frontends.tools.games import SYMBOLS -from sat.core.i18n import _ - - -# TODO: at some point we should decide which behaviors to keep and remove these two constants -TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button -NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry - - -class UniBoxPanel(HorizontalPanel): - """Panel containing the UniBox""" - - def __init__(self, host): - HorizontalPanel.__init__(self) - self.host = host - self.setStyleName('uniBoxPanel') - self.unibox = None - - def refresh(self): - """Enable or disable this panel. Contained widgets are created when necessary.""" - enable = self.host.params_ui['unibox']['value'] - self.setVisible(enable) - if enable and not self.unibox: - 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.openRichMessageEditor) - self.unibox.addKey("@@: ") - self.unibox.onSelectedChange(self.host.getSelected()) - - def openRichMessageEditor(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 afterEditCb(): - 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.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) - 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 timer: 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') - 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: - log.warning("trying to remove an unknown key") - - 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: - log.error("Unknown target") - target_hook, _type, msg = getSelectedOrStatus() - - return (target_hook, _type, msg) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - target_hook, type_, msg = self._getTarget(_txt) - - if keycode == KEY_ENTER: - if _txt: - if target_hook: - parsed_txt, data = target_hook - self.host.send([(type_, data)], parsed_txt) - self.host._updateInputHistory(_txt) - self.setText('') - self.host.showWarning(None, None) - else: - self.host.showWarning(type_, msg) - 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 WarningPopup(): - - def __init__(self): - self._popup = None - self._timer = Timer(notify=self._timeCb) - - def showWarning(self, type_=None, msg=None, duration=2000): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see __showWarning) - @msg: message to be displayed - """ - if type_ is None: - self.__removeWarning() - return - if not self._popup: - self.__showWarning(type_, msg) - elif (type_, msg) != self._popup.target_data: - self._timeCb(None) # we remove the popup - self.__showWarning(type_, msg) - - self._timer.schedule(duration) - - def __showWarning(self, type_, msg): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - @type_: a type determining the CSS style to be applied. For now the defined styles are - "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". - @msg: message to be displayed - """ - if type_ == "NONE": - return - if not msg: - log.warning("no msg set uniBox warning") - return - if type_ == "PUBLIC": - style = "targetPublic" - elif type_ == "GROUP": - style = "targetGroup" - elif type_ == "STATUS": - style = "targetStatus" - elif type_ == "ONE2ONE": - style = "targetOne2One" - else: - log.error("unknown message type") - return - contents = HTML(msg) - - self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) - self._popup.target_data = (type_, msg) - 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 __removeWarning(self): - """Remove the popup""" - self._timeCb(None) - - -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.title = data.get('title', '') - self.title_xhtml = data.get('title_xhtml', '') - self.content = data.get('content', '') - self.content_xhtml = data.get('content_xhtml', '') - self.author = data['author'] - self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here - self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here - self.service = data.get('service', '') - self.node = data.get('node', '') - self.comments = data.get('comments', False) - self.comments_service = data.get('comments_service', '') - self.comments_node = data.get('comments_node', '') - - -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. - """ - self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) - for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', - 'author', 'updated', 'published', 'comments', 'service', 'node', - 'comments_service', 'comments_node']: - getter = lambda attr: lambda inst: getattr(inst._base_item, attr) - setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) - setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) - - 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) - - if TOGGLE_EDITION_USE_ICON: - self.entry_dialog = HorizontalPanel() - else: - self.entry_dialog = VerticalPanel() - 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.service, self.node, self.id) - self.__setContent() - - def __setContent(self): - """Actually set the entry content (header, icons, bubble...)""" - self.delete_label = self.update_label = self.comment_label = None - self.bubble = self._current_comment = None - self.__setHeader() - self.__setBubble() - 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.comments: - self.comment_label = addIcon(u"↶", "Comment this message") - self.comment_label.setStyleName('mb_entry_action_larger') - is_publisher = self.author == self._blog_panel.host.whoami.bare - if is_publisher: - self.update_label = addIcon(u"✍", "Edit this message") - if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): - self.delete_label = addIcon(u"✗", "Delete 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: - try: # prevent re-selection of the main entry after a comment has been focused - if self.__ignoreNextEvent: - self.__ignoreNextEvent = False - return - except AttributeError: - pass - self._blog_panel.setSelectedEntry(self) - elif sender == self.delete_label: - self._delete() - elif sender == self.update_label: - self.edit(True) - elif sender == self.comment_label: - self.__ignoreNextEvent = True - self._comment() - - def __modifiedCb(self, content): - """Send the new content to the backend - @return: False to restore the original content if a deletion has been cancelled - """ - if not content['text']: # previous content has been emptied - self._delete(True) - return False - extra = {'published': str(self.published)} - if isinstance(self.bubble, richtext.RichTextEditor): - # TODO: if the user change his parameters after the message edition started, - # the message syntax could be different then the current syntax: pass the - # message syntax in extra for the frontend to use it instead of current syntax. - extra.update({'content_rich': content['text'], 'title': content['title']}) - if self.empty: - if self.type == 'main_item': - self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) - else: - self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) - else: - self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) - return True - - def __afterEditCb(self, content): - """Remove the entry if it was an empty one (used for creating a new blog post). - Data for the actual new blog post will be received from the bridge""" - if self.empty: - self._blog_panel.removeEntry(self.type, self.id) - if self.type == 'main_item': # restore the "New message" button - self._blog_panel.refresh() - else: # allow to create a new comment - self._parent_entry._current_comment = None - self.entry_dialog.setWidth('auto') - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - - def __setBubble(self, edit=False): - """Set the bubble displaying the initial content.""" - content = {'text': self.content_xhtml if self.content_xhtml else self.content, - 'title': self.title_xhtml if self.title_xhtml else self.title} - if self.content_xhtml: - content.update({'syntax': C.SYNTAX_XHTML}) - if self.author != self._blog_panel.host.whoami.bare: - options = ['read_only'] - else: - options = [] if self.empty else ['update_msg'] - self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) - else: # assume raw text message have no title - self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) - self.bubble.addStyleName("bubble") - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - self.entry_dialog.add(self.bubble) - self.edit(edit) - self.bubble.addEditListener(self.__showWarning) - - def __showWarning(self, sender, keycode): - if keycode == KEY_ENTER: - self._blog_panel.host.showWarning(None, None) - else: - self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) - - def _delete(self, empty=False): - """Ask confirmation for deletion. - @return: False if the deletion has been cancelled.""" - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) - else: # restore the text if it has been emptied during the edition - self.bubble.setContent(self.bubble._original_content) - - if self.empty: - text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") - dialog.InfoDialog(_("Information"), text).show() - return - text = "" - if empty: - text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>") - target = _('message and all its comments') if self.comments else _('comment') - text += _("Do you really want to delete this %s?") % target - dialog.ConfirmDialog(confirm_cb, text=text).show() - - def _comment(self): - """Add an empty entry for a new comment""" - if self._current_comment: - self._current_comment.bubble.setFocus(True) - self._blog_panel.setSelectedEntry(self._current_comment) - return - data = {'id': str(time()), - 'new': True, - 'type': 'comment', - 'author': self._blog_panel.host.whoami.bare, - 'service': self.comments_service, - 'node': self.comments_node - } - entry = self._blog_panel.addEntry(data) - if entry is None: - log.info("The entry of id %s can not be commented" % self.id) - return - entry._parent_entry = self - self._current_comment = entry - self.edit(True, entry) - self._blog_panel.setSelectedEntry(entry) - - def edit(self, edit, entry=None): - """Toggle the bubble between display and edit mode - @edit: boolean value - @entry: MicroblogEntry instance, or None to use self - """ - if entry is None: - entry = self - try: - entry.toggle_syntax_button.removeFromParent() - except TypeError: - pass - entry.bubble.edit(edit) - if edit: - if isinstance(entry.bubble, richtext.RichTextEditor): - image = '<a class="richTextIcon">A</a>' - html = '<a style="color: blue;">raw text</a>' - title = _('Switch to raw text edition') - else: - image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>' - html = '<a style="color: blue;">rich text</a>' - title = _('Switch to rich text edition') - if TOGGLE_EDITION_USE_ICON: - entry.entry_dialog.setWidth('80%') - entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) - entry.toggle_syntax_button.setTitle(title) - entry.entry_dialog.add(entry.toggle_syntax_button) - else: - entry.toggle_syntax_button = HTML(html) - entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) - entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') - entry.entry_dialog.add(entry.toggle_syntax_button) - entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - entry.toggle_syntax_button.setStyleAttribute('left', '-20px') - - def toggleContentSyntax(self): - """Toggle the editor between raw and rich text""" - original_content = self.bubble.getOriginalContent() - rich = not isinstance(self.bubble, richtext.RichTextEditor) - if rich: - original_content['syntax'] = C.SYNTAX_XHTML - - def setBubble(text): - self.content = text - self.content_xhtml = text if rich else '' - self.content_title = self.content_title_xhtml = '' - self.bubble.removeFromParent() - self.__setBubble(True) - self.bubble.setOriginalContent(original_content) - if rich: - self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble - - text = self.bubble.getContent()['text'] - if not text: - setBubble(' ') # something different than empty string is needed to initialize the rich text editor - return - if not rich: - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) - dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() - else: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) - - -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) - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - display the 'New message' button or an empty bubble on top of the panel""" - if hasattr(self, 'new_button'): - self.new_button.setVisible(self.host.uni_box is None) - return - if self.host.uni_box is None: - def addBox(): - if hasattr(self, 'new_button'): - self.new_button.setVisible(False) - data = {'id': str(time()), - 'new': True, - 'author': self.host.whoami.bare, - } - entry = self.addEntry(data) - entry.edit(True) - if NEW_MESSAGE_USE_BUTTON: - self.new_button = Button("New message", listener=addBox) - self.new_button.setStyleName("microblogNewButton") - self.vpanel.insert(self.new_button, 0) - else: - addBox() - - @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) - _new_panel.refresh() - 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, comment=None): - """ - @param comment: True if the composed message is a comment. If None, consider we are - composing from the unibox and guess the message type from self.selected_entry - @return: a couple (type, msg) for calling self.host.showWarning""" - if comment is None: # composing from the unibox - if self.selected_entry and not self.selected_entry.comments: - log.error("an item without comment is selected") - return ("NONE", None) - comment = self.selected_entry is not None - if comment: - 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_url = self.selected_entry.comments - if not comments_url: - raise Exception("ERROR: the comments URL is empty") - target = ("COMMENT", comments_url) - 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 - """ - log.debug("Massive insertion of %d microblogs" % len(mblogs)) - for publisher in mblogs: - log.debug("adding blogs for [%s]" % publisher) - for mblog in mblogs[publisher]: - if not "content" in mblog: - log.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: - log.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": - comments_hash = (_entry.service, _entry.node) - if not comments_hash in self.comments: - # The comments node is not known in this panel - return None - parent = self.comments[comments_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.id == _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 comments service/node as a reference - comments_hash = (_entry.comments_service, _entry.comments_node) - self.comments[comments_hash] = _entry - self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_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.id == 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.id == id_: - comment.removeFromParent() - self.selected_entry = None - break - - def setSelectedEntry(self, entry): - try: - self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry - except AttributeError: - log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") - removeStyle = lambda entry: entry.removeStyleName('selected_entry') - if not self.host.uni_box or not entry.comments: - entry.addStyleName('selected_entry') # blink the clicked entry - clicked_entry = entry # entry may be None when the timer is done - Timer(500, lambda timer: removeStyle(clicked_entry)) - if not self.host.uni_box: - return # unibox is disabled - # from here the previous behavior (toggle main item selection) is conserved - entry = entry if entry.comments else None - if self.selected_entry == entry: - entry = None - if self.selected_entry: - removeStyle(self.selected_entry) - if entry: - log.debug("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(HTMLTextEditor): - - EMPTY_STATUS = '<click to set a status>' - - def __init__(self, host, status=''): - self.host = host - modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True - HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True}) - self.edit(False) - self.setStyleName('statusPanel') - - @property - def status(self): - return self._original_content['text'] - - def __cleanContent(self, content): - status = content['text'] - if status == self.EMPTY_STATUS or status in C.PRESENCE.values(): - content['text'] = '' - return content - - def getContent(self): - return self.__cleanContent(HTMLTextEditor.getContent(self)) - - def setContent(self, content): - content = self.__cleanContent(content) - BaseTextEditor.setContent(self, content) - - def setDisplayContent(self): - status = self._original_content['text'] - try: - presence = self.host.status_panel.presence - except AttributeError: # during initialization - presence = None - if not status: - if presence and presence in C.PRESENCE: - status = C.PRESENCE[presence] - else: - status = self.EMPTY_STATUS - self.display.setHTML(addURLToText(status)) - - -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 C.PRESENCE.keys(): - entries.update({C.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) - - self.status_panel.edit(False) - - ClickHandler.__init__(self) - self.addClickListener(self) - - @property - def presence(self): - return self._presence - - @property - def status(self): - return self.status_panel._original_content['text'] - - def setPresence(self, presence): - self._presence = presence - contact.setPresenceStyle(self.presence_button, self._presence) - - def setStatus(self, status): - self.status_panel.setContent({'text': status}) - self.status_panel.setDisplayContent() - - 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 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, title=target.bare, selectable=True) - self.vpanel = VerticalPanel() - self.vpanel.setSize('100%', '100%') - self.type = type_ - self.nick = None - if not target: - log.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._state = None - - @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) - _new_panel.refresh() - return _new_panel - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - add a message box at the bottom of the panel""" - self.host.contact_panel.setContactMessageWaiting(self.target.bare, False) - self.content_scroll.scrollToBottom() - - enable_box = self.host.uni_box is None - if hasattr(self, 'message_box'): - self.message_box.setVisible(enable_box) - return - if enable_box: - self.message_box = MessageBox(self.host) - self.message_box.onSelectedChange(self) - self.vpanel.add(self.message_box) - - 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 changeUserNick(self, old_nick, new_nick): - assert(self.type == "group") - self.occupants_list.removeOccupant(old_nick) - self.occupants_list.addOccupant(new_nick) - self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_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', link_cb=None): - """Print general info - @param msg: message to print - @param type_: one of: - "normal": general info like "toto has joined the room" - "link": general info that is clickable like "click here to join the main room" - "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link' - """ - _wid = HTML(msg) if type_ == 'link' else Label(msg) - if type_ == 'normal': - _wid.setStyleName('chatTextInfo') - elif type_ == 'link': - _wid.setStyleName('chatTextInfo-link') - if link_cb: - _wid.addClickListener(link_cb) - 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.updateSpecials(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 - log.info("%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 - - def setState(self, state, nick=None): - """Set the chat state (XEP-0085) of the contact. Leave nick to None - to set the state for a one2one conversation, or give a nickname or - C.ALL_OCCUPANTS to set the state of a participant within a MUC. - @param state: the new chat state - @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS - """ - if nick: - assert(self.type == 'group') - occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick] - for occupant in occupants: - self.occupants_list.occupants_list[occupant].setState(state) - else: - assert(self.type == 'one2one') - self._state = state - self.refreshTitle() - self.state_machine.started = not not state # start to send "composing" state from now - - def refreshTitle(self): - """Refresh the title of this ChatPanel dialog""" - if self._state: - self.setTitle(self.target.bare + " (" + self._state + ")") - else: - self.setTitle(self.target.bare) - - -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) - - # 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) - - self.header = AbsolutePanel() - self.header.add(self.menu) - self.header.add(self.unibox_panel) - self.header.add(self.host.status_panel) - self.header.setStyleName('header') - self.add(self.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 refresh(self): - """Refresh the main panel""" - self.unibox_panel.refresh()
--- a/browser_side/plugin_xep_0085.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,82 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# SAT plugin for Chat State Notifications Protocol (xep-0085) -# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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/>. - -from pyjamas.Timer import Timer - - -# Copy of the map from sat/src/plugins/plugin_xep_0085 -TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120}, - "inactive": {"next_state": "gone", "delay": 480}, - "gone": {"next_state": "", "delay": 0}, - "composing": {"next_state": "paused", "delay": 30}, - "paused": {"next_state": "inactive", "delay": 450} - } - - -class ChatStateMachine: - """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085 - which manage a timer on the web browser and keep it synchronized with the timer that runs - on the backend. This is only needed to avoid calling the bridge method chatStateComposing - too often ; accuracy is not needed so we can ignore the delay of the communication between - the web browser and the backend (the timer on the web browser always starts a bit before). - /!\ Keep this file up to date if you modify the one in the sat plugins directory. - """ - def __init__(self, host, target_s): - - self.host = host - self.target_s = target_s - self.started = False - self.state = None - self.timer = None - - def _onEvent(self, state): - """Pyjamas callback takes no extra argument so we need this trick""" - # Here we should check the value of the parameter "Send chat state notifications" - # but this costs two messages. It's even better to call chatStateComposing - # with a doubt, it will be checked by the back-end anyway before sending - # the actual notifications to the other client. - if state == "composing" and not self.started: - return - self.started = True - self.next_state = state - self.__onEvent(None) - - def __onEvent(self, timer): - # print "on event %s" % self.next_state - state = self.next_state - self.next_state = "" - if state != self.state and state == "composing": - self.host.bridge.call('chatStateComposing', None, self.target_s) - self.state = state - if not self.timer is None: - self.timer.cancel() - - if not state in TRANSITIONS: - return - if not "next_state" in TRANSITIONS[state]: - return - if not "delay" in TRANSITIONS[state]: - return - next_state = TRANSITIONS[state]["next_state"] - delay = TRANSITIONS[state]["delay"] - if next_state == "" or delay < 0: - return - self.next_state = next_state - # pyjamas timer in milliseconds - self.timer = Timer(delay * 1000, self.__onEvent)
--- a/browser_side/radiocol.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,321 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Hidden import Hidden -from pyjamas.ui.CaptionPanel import CaptionPanel -from pyjamas.media.Audio import Audio -from pyjamas import Window -from pyjamas.Timer import Timer - -from html_tools import html_sanitize -from file_tools import FilterFileUpload -from sat_frontends.tools.misc import DEFAULT_MUC -from sat.core.i18n import _ - - -class MetadataPanel(FlexTable): - - def __init__(self): - FlexTable.__init__(self) - title_lbl = Label("title:") - title_lbl.setStyleName('radiocol_metadata_lbl') - artist_lbl = Label("artist:") - artist_lbl.setStyleName('radiocol_metadata_lbl') - album_lbl = Label("album:") - album_lbl.setStyleName('radiocol_metadata_lbl') - self.title = Label("") - self.title.setStyleName('radiocol_metadata') - self.artist = Label("") - self.artist.setStyleName('radiocol_metadata') - self.album = Label("") - self.album.setStyleName('radiocol_metadata') - self.setWidget(0, 0, title_lbl) - self.setWidget(1, 0, artist_lbl) - self.setWidget(2, 0, album_lbl) - self.setWidget(0, 1, self.title) - self.setWidget(1, 1, self.artist) - self.setWidget(2, 1, self.album) - self.setStyleName("radiocol_metadata_pnl") - - def setTitle(self, title): - self.title.setText(title) - - def setArtist(self, artist): - self.artist.setText(artist) - - def setAlbum(self, album): - self.album.setText(album) - - -class ControlPanel(FormPanel): - """Panel used to show controls to add a song, or vote for the current one""" - - def __init__(self, parent): - FormPanel.__init__(self) - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction("upload_radiocol") - self.timer_on = False - self._parent = parent - vPanel = VerticalPanel() - - types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'), - ('video/ogg', '*.ogv', 'Ogg Vorbis Video'), - ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'), - ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'), - ('audio/mp3', '*.mp3', 'MPEG-Layer 3'), - ] - self.file_upload = FilterFileUpload("song", 10, types) - vPanel.add(self.file_upload) - - hPanel = HorizontalPanel() - self.upload_btn = Button("Upload song", getattr(self, "onBtnClick")) - hPanel.add(self.upload_btn) - self.status = Label() - self.updateStatus() - hPanel.add(self.status) - #We need to know the filename and the referee - self.filename_field = Hidden('filename', '') - hPanel.add(self.filename_field) - referee_field = Hidden('referee', self._parent.referee) - hPanel.add(self.filename_field) - hPanel.add(referee_field) - vPanel.add(hPanel) - - self.add(vPanel) - self.addFormHandler(self) - - def updateStatus(self): - if self.timer_on: - return - # TODO: the status should be different if a song is being played or not - queue = self._parent.getQueueSize() - queue_data = self._parent.queue_data - if queue < queue_data[0]: - left = queue_data[0] - queue - self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else "")) - elif queue < queue_data[1]: - left = queue_data[1] - queue - self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else "")) - elif queue >= queue_data[1]: - self.status.setText("[The queue is currently full]") - self.status.setStyleName('radiocol_status') - - def onBtnClick(self): - if self.file_upload.check(): - self.status.setText('[Submitting, please wait...]') - self.filename_field.setValue(self.file_upload.getFilename()) - if self.file_upload.getFilename().lower().endswith('.mp3'): - self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000) - self.submit() - self.file_upload.setFilename("") - - def onSubmit(self, event): - pass - - def blockUpload(self): - self.file_upload.setVisible(False) - self.upload_btn.setEnabled(False) - - def unblockUpload(self): - self.file_upload.setVisible(True) - self.upload_btn.setEnabled(True) - - def setTemporaryStatus(self, text, style): - self.status.setText(text) - self.status.setStyleName('radiocol_upload_status_%s' % style) - self.timer_on = True - - def cb(timer): - self.timer_on = False - self.updateStatus() - - Timer(5000, cb) - - def onSubmitComplete(self, event): - result = event.getResults() - if result == "OK": - # the song can still be rejected (not readable, full queue...) - self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok") - elif result == "KO": - self.setTemporaryStatus('[Something went wrong during your song upload]', "ko") - self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")) - # TODO: would be great to re-use the original Exception class and message - # but it is lost in the middle of the traceback and encapsulated within - # a DBusException instance --> extract the data from the traceback? - else: - Window.alert('Submit error: %s' % result) - self.status.setText('') - - -class Player(Audio): - - def __init__(self, player_id, metadata_panel): - Audio.__init__(self) - self._id = player_id - self.metadata = metadata_panel - self.timestamp = "" - self.title = "" - self.artist = "" - self.album = "" - self.filename = None - self.played = False # True when the song is playing/has played, becomes False on preload - self.setAutobuffer(True) - self.setAutoplay(False) - self.setVisible(False) - - - def preload(self, timestamp, filename, title, artist, album): - """preload the song but doesn't play it""" - self.timestamp = timestamp - self.filename = filename - self.title = title - self.artist = artist - self.album = album - self.played = False - self.setSrc("radiocol/%s" % html_sanitize(filename)) - log.debug("preloading %s in %s" % (title, self._id)) - - def play(self, play=True): - """Play or pause the song - @param play: set to True to play or to False to pause - """ - if play: - self.played = True - self.metadata.setTitle(self.title) - self.metadata.setArtist(self.artist) - self.metadata.setAlbum(self.album) - Audio.play(self) - else: - self.pause() - - -class RadioColPanel(HorizontalPanel, ClickHandler): - - def __init__(self, parent, referee, player_nick, players, queue_data): - """ - @param parent - @param referee - @param player_nick - @param players - @param queue_data: list of integers (queue to start, queue limit) - """ - # We need to set it here and not in the CSS :( - HorizontalPanel.__init__(self, Height="90px") - ClickHandler.__init__(self) - self._parent = parent - self.referee = referee - self.queue_data = queue_data - self.setStyleName("radiocolPanel") - - # Now we set up the layout - self.metadata_panel = MetadataPanel() - self.add(CaptionPanel("Now playing", self.metadata_panel)) - self.playlist_panel = VerticalPanel() - self.add(CaptionPanel("Songs queue", self.playlist_panel)) - self.control_panel = ControlPanel(self) - self.add(CaptionPanel("Controls", self.control_panel)) - - self.next_songs = [] - self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)] - self.current_player = None - for player in self.players: - self.add(player) - self.addClickListener(self) - - help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br /> - Please do not submit files that are protected by copyright.<br /> - Click <a style="color: red;">here</a> if you need some support :)""" - link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick) - self._parent.printInfo(help_msg, type_='link', link_cb=link_cb) - - def pushNextSong(self, title): - """Add a song to the left panel's next songs queue""" - next_song = Label(title) - next_song.setStyleName("radiocol_next_song") - self.next_songs.append(next_song) - self.playlist_panel.append(next_song) - self.control_panel.updateStatus() - - def popNextSong(self): - """Remove the first song of next songs list - should be called when the song is played""" - #FIXME: should check that the song we remove is the one we play - next_song = self.next_songs.pop(0) - self.playlist_panel.remove(next_song) - self.control_panel.updateStatus() - - def getQueueSize(self): - return len(self.playlist_panel.getChildren()) - - def radiocolCheckPreload(self, timestamp): - for player in self.players: - if player.timestamp == timestamp: - return False - return True - - def radiocolPreload(self, timestamp, filename, title, artist, album, sender): - if not self.radiocolCheckPreload(timestamp): - return # song already preloaded - preloaded = False - for player in self.players: - if not player.filename or \ - (player.played and player != self.current_player): - #if player has no file loaded, or it has already played its song - #we use it to preload the next one - player.preload(timestamp, filename, title, artist, album) - preloaded = True - break - if not preloaded: - log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1]) - else: - self.pushNextSong(title) - self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title}) - - def radiocolPlay(self, filename): - found = False - for player in self.players: - if not found and player.filename == filename: - player.play() - self.popNextSong() - self.current_player = player - found = True - else: - player.play(False) # in case the previous player was not sync - if not found: - log.error("Song not found in queue, can't play it. This should not happen") - - def radiocolNoUpload(self): - self.control_panel.blockUpload() - - def radiocolUploadOk(self): - self.control_panel.unblockUpload() - - def radiocolSongRejected(self, reason): - Window.alert("Song rejected: %s" % reason)
--- a/browser_side/register.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,249 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> -# Copyright (C) 2011, 2012 Adrien Vigneron <adrienvigneron@mailoo.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/>. - -#This page manage subscription and new account creation -import pyjd # this is dummy in pyjs - -from constants import Const as C -from sat.core.i18n import _ -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.TabBar import TabBar -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Hidden import Hidden -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KEY_ENTER -import re -from pyjamas.Timer import Timer - - -class RegisterPanel(FormPanel): - - def __init__(self, callback): - """ - @param callback: method to call if login successful - """ - FormPanel.__init__(self) - self.setSize('600px', '350px') - self.callback = callback - self.setMethod(FormPanel.METHOD_POST) - main_panel = HorizontalPanel() - main_panel.setStyleName('registerPanel_main') - left_side = Image("media/libervia/register_left.png") - main_panel.add(left_side) - - ##TabPanel## - tab_bar = TabBar() - tab_bar.setStyleName('registerPanel_tabs') - self.right_side = TabPanel(tab_bar) - self.right_side.setStyleName('registerPanel_right_side') - main_panel.add(self.right_side) - main_panel.setCellWidth(self.right_side, '100%') - - ##Login tab## - login_tab = SimplePanel() - login_tab.setStyleName('registerPanel_content') - login_vpanel = VerticalPanel() - login_tab.setWidget(login_vpanel) - - self.login_warning_msg = Label('') - self.login_warning_msg.setVisible(False) - self.login_warning_msg.setStyleName('formWarning') - login_vpanel.add(self.login_warning_msg) - - login_label = Label('Login:') - self.login_box = TextBox() - self.login_box.setName("login") - self.login_box.addKeyboardListener(self) - login_pass_label = Label('Password:') - self.login_pass_box = PasswordTextBox() - self.login_pass_box.setName("login_password") - self.login_pass_box.addKeyboardListener(self) - - login_vpanel.add(login_label) - login_vpanel.add(self.login_box) - login_vpanel.add(login_pass_label) - login_vpanel.add(self.login_pass_box) - login_but = Button("Log in", getattr(self, "onLogin")) - login_but.setStyleName('button') - login_but.addStyleName('red') - login_vpanel.add(login_but) - - #The hidden submit_type field - self.submit_type = Hidden('submit_type') - login_vpanel.add(self.submit_type) - - ##Register tab## - register_tab = SimplePanel() - register_tab.setStyleName('registerPanel_content') - register_vpanel = VerticalPanel() - register_tab.setWidget(register_vpanel) - - self.register_warning_msg = HTML('') - self.register_warning_msg.setVisible(False) - self.register_warning_msg.setStyleName('formWarning') - register_vpanel.add(self.register_warning_msg) - - register_login_label = Label('Login:') - self.register_login_box = TextBox() - self.register_login_box.setName("register_login") - self.register_login_box.addKeyboardListener(self) - email_label = Label('E-mail:') - self.email_box = TextBox() - self.email_box.setName("email") - self.email_box.addKeyboardListener(self) - register_pass_label = Label('Password:') - self.register_pass_box = PasswordTextBox() - self.register_pass_box.setName("register_password") - self.register_pass_box.addKeyboardListener(self) - register_vpanel.add(register_login_label) - register_vpanel.add(self.register_login_box) - register_vpanel.add(email_label) - register_vpanel.add(self.email_box) - register_vpanel.add(register_pass_label) - register_vpanel.add(self.register_pass_box) - - register_but = Button("Register", getattr(self, "onRegister")) - register_but.setStyleName('button') - register_but.addStyleName('red') - register_vpanel.add(register_but) - - self.right_side.add(login_tab, 'Login') - self.right_side.add(register_tab, 'Register') - self.right_side.addTabListener(self) - self.right_side.selectTab(1) - login_tab.setWidth(None) - register_tab.setWidth(None) - - self.add(main_panel) - self.addFormHandler(self) - self.setAction('register_api/login') - - def onBeforeTabSelected(self, sender, tabIndex): - return True - - def onTabSelected(self, sender, tabIndex): - if tabIndex == 0: - self.login_box.setFocus(True) - elif tabIndex == 1: - self.register_login_box.setFocus(True) - - def onKeyPress(self, sender, keycode, modifiers): - if keycode == KEY_ENTER: - # Browsers offer an auto-completion feature to any - # text box, but the selected value is not set when - # the widget looses the focus. Using a timer with - # any delay value > 0 would do the trick. - if sender == self.login_box: - Timer(5, lambda timer: self.login_pass_box.setFocus(True)) - elif sender == self.login_pass_box: - self.onLogin(None) - elif sender == self.register_login_box: - Timer(5, lambda timer: self.email_box.setFocus(True)) - elif sender == self.email_box: - Timer(5, lambda timer: self.register_pass_box.setFocus(True)) - elif sender == self.register_pass_box: - self.onRegister(None) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onLogin(self, button): - if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE): - self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -') - self.login_warning_msg.setVisible(True) - else: - self.submit_type.setValue('login') - self.submit() - - def onRegister(self, button): - if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE): - self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -')) - self.register_warning_msg.setVisible(True) - elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE): - self.register_warning_msg.setHTML(_('Invalid email address')) - self.register_warning_msg.setVisible(True) - elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: - self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH) - self.register_warning_msg.setVisible(True) - else: - self.register_warning_msg.setVisible(False) - self.submit_type.setValue('register') - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result == "AUTH ERROR": - Window.alert('Your login and/or password is incorrect. Please try again') - elif result == "LOGGED": - self.callback() - elif result == "SESSION_ACTIVE": - Window.alert('Session already active, this should not happen, please contact the author to fix it') - elif result == "ALREADY EXISTS": - self.register_warning_msg.setHTML('This login already exists,<br>please choose another one') - self.register_warning_msg.setVisible(True) - elif result == "INTERNAL": - self.register_warning_msg.setHTML('SERVER ERROR: something went wrong during registration process, please contact the server administrator') - self.register_warning_msg.setVisible(True) - elif result == "REGISTRATION": - self.login_warning_msg.setVisible(False) - self.register_warning_msg.setVisible(False) - self.login_box.setText(self.register_login_box.getText()) - self.login_pass_box.setText('') - self.register_login_box.setText('') - self.register_pass_box.setText('') - self.email_box.setText('') - self.right_side.selectTab(0) - self.login_pass_box.setFocus(True) - Window.alert('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO') - else: - Window.alert('Submit error: %s' % result) - - -class RegisterBox(PopupPanel): - - def __init__(self, callback, *args, **kwargs): - PopupPanel.__init__(self, *args, **kwargs) - self._form = RegisterPanel(callback) - self.setWidget(self._form) - - def onWindowResized(self, width, height): - super(RegisterBox, self).onWindowResized(width, height) - self.centerBox() - - def show(self): - super(RegisterBox, self).show() - self.centerBox() - self._form.login_box.setFocus(True)
--- a/browser_side/richtext.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,536 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. - -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Button import Button -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KeyboardHandler -from __pyjamas__ import doc - -from constants import Const as C -from dialog import ConfirmDialog, InfoDialog -from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor -from list_manager import ListManager -from html_tools import html_sanitize -from browser_side import panels - -from sat_frontends.tools import composition -from sat.core.i18n import _ - - -class RichTextEditor(BaseTextEditor, FlexTable): - """Panel for the rich text editor.""" - - def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None): - """ - @param host: the SatWebFrontend instance - @param content: dict with at least a 'text' key - @param modifiedCb: method to be called when the text has been modified - @param afterEditCb: method to be called when the edition is done - @param options: list of UI options (see self.readOptions) - """ - self.host = host - self._debug = False # TODO: don't forget to set it False before commit - self.wysiwyg = False - self.__readOptions(options) - self.style = {'main': 'richTextEditor', - 'title': 'richTextTitle', - 'toolbar': 'richTextToolbar', - 'textarea': 'richTextArea'} - if isinstance(style, dict): - self.style.update(style) - self._prepareUI() - BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) - - def __readOptions(self, options): - """Set the internal flags according to the given options.""" - if options is None: - options = [] - self.read_only = 'read_only' in options - self.update_msg = 'update_msg' in options - self.no_title = 'no_title' in options or self.read_only - self.no_command = 'no_command' in options or self.read_only - - def _prepareUI(self, y_offset=0): - """Prepare the UI to host title panel, toolbar, text area... - @param y_offset: Y offset to start from (extra rows on top)""" - if not self.read_only: - self.title_offset = y_offset - self.toolbar_offset = self.title_offset + (0 if self.no_title else 1) - self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1) - self.command_offset = self.content_offset + 1 - else: - self.title_offset = self.toolbar_offset = self.content_offset = y_offset - self.command_offset = self.content_offset + 1 - FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2) - self.addStyleName(self.style['main']) - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - BaseTextEditor.addEditListener(self, listener) - if hasattr(self, 'display'): - self.display.addEditListener(listener) - - def refresh(self, edit=None): - """Refresh the UI for edition/display mode - @param edit: set to True to display the edition mode""" - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - - for widget in ['title_panel', 'command']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if hasattr(self, 'toolbar'): - self.toolbar.setVisible(False) - if not hasattr(self, 'display'): - self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode - for listener in self.edit_listeners: - self.display.addEditListener(listener) - if not self.read_only and not hasattr(self, 'textarea'): - self.textarea = EditTextArea(self) # for edition mode - self.textarea.addStyleName(self.style['textarea']) - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if edit and not self.wysiwyg: - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - self.setWidget(self.content_offset, 0, self.textarea) - else: - self.setWidget(self.content_offset, 0, self.display) - if not edit: - return - - if not self.no_title and not hasattr(self, 'title_panel'): - self.title_panel = TitlePanel() - self.title_panel.addStyleName(self.style['title']) - self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) - self.setWidget(self.title_offset, 0, self.title_panel) - - if not self.no_command and not hasattr(self, 'command'): - self.command = HorizontalPanel() - self.command.addStyleName("marginAuto") - self.command.add(Button("Cancel", lambda: self.edit(True, True))) - self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) - self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) - self.setWidget(self.command_offset, 0, self.command) - - def setToolBar(self, syntax): - """This method is called asynchronously after the parameter - holding the rich text syntax is retrieved. It is called at - each call of self.edit(True) because the user may - have change his setting since the last time.""" - if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): - syntax = composition.RICH_SYNTAXES.keys()[0] - if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: - self.toolbar.setVisible(True) - return - count = 0 - for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]: - self.toolbar = HorizontalPanel() - self.toolbar.syntax = syntax - self.toolbar.addStyleName(self.style['toolbar']) - for key in composition.RICH_SYNTAXES[syntax].keys(): - self.addToolbarButton(syntax, key) - self.wysiwyg_button = CheckBox(_('WYSIWYG edition')) - wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) - self.wysiwyg_button.addClickListener(wysiywgCb) - self.toolbar.add(self.wysiwyg_button) - self.syntax_label = Label(_("Syntax: %s") % syntax) - self.syntax_label.addStyleName("richTextSyntaxLabel") - self.toolbar.add(self.syntax_label) - self.toolbar.setCellWidth(self.syntax_label, "100%") - self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2) - self.setWidget(self.toolbar_offset + count, 0, self.toolbar) - count += 1 - - def setWysiwyg(self, wysiwyg, init=False): - """Toggle the edition mode between rich content syntax and wysiwyg. - @param wysiwyg: boolean value - @param init: set to True to re-init without switching the widgets.""" - def setWysiwyg(): - self.wysiwyg = wysiwyg - try: - self.wysiwyg_button.setChecked(wysiwyg) - except TypeError: - pass - try: - if wysiwyg: - self.syntax_label.addStyleName('transparent') - else: - self.syntax_label.removeStyleName('transparent') - except TypeError: - pass - if not wysiwyg: - self.display.removeStyleName('richTextWysiwyg') - - if init: - setWysiwyg() - return - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if wysiwyg: - def syntaxConvertCb(text): - self.display.setContent({'text': text}) - self.textarea.removeFromParent() # XXX: force as it is not always done... - self.setWidget(self.content_offset, 0, self.display) - self.display.addStyleName('richTextWysiwyg') - self.display.edit(True) - content = self.getContent() - if content['text'] and content['syntax'] != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) - else: - syntaxConvertCb(content['text']) - else: - syntaxConvertCb = lambda text: self.textarea.setText(text) - text = self.display.getContent()['text'] - if text and self.toolbar.syntax != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) - else: - syntaxConvertCb(text) - self.setWidget(self.content_offset, 0, self.textarea) - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - - setWysiwyg() # do it in the end because it affects self.getContent - - def addToolbarButton(self, syntax, key): - """Add a button with the defined parameters.""" - button = Button('<img src="%s" class="richTextIcon" />' % - composition.RICH_BUTTONS[key]["icon"]) - button.setTitle(composition.RICH_BUTTONS[key]["tip"]) - button.addStyleName('richTextToolButton') - self.toolbar.add(button) - - def buttonCb(): - """Generic callback for a toolbar button.""" - text = self.textarea.getText() - cursor_pos = self.textarea.getCursorPos() - selection_length = self.textarea.getSelectionLength() - data = composition.RICH_SYNTAXES[syntax][key] - if selection_length == 0: - middle_text = data[1] - else: - middle_text = text[cursor_pos:cursor_pos + selection_length] - self.textarea.setText(text[:cursor_pos] - + data[0] - + middle_text - + data[2] - + text[cursor_pos + selection_length:]) - self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) - self.textarea.setFocus(True) - self.textarea.onKeyDown() - - def wysiwygCb(): - """Callback for a toolbar button while wysiwyg mode is enabled.""" - data = composition.COMMANDS[key] - - def execCommand(command, arg): - self.display.setFocus(True) - doc().execCommand(command, False, arg.strip() if arg else '') - # use Window.prompt instead of dialog.PromptDialog to not loose the focus - prompt = lambda command, text: execCommand(command, Window.prompt(text)) - if isinstance(data, tuple) or isinstance(data, list): - if data[1]: - prompt(data[0], data[1]) - else: - execCommand(data[0], data[2]) - else: - execCommand(data, False, '') - self.textarea.onKeyDown() - - button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) - - def getContent(self): - assert(hasattr(self, 'textarea')) - assert(hasattr(self, 'toolbar')) - if self.wysiwyg: - content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} - else: - content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} - if hasattr(self, 'title_panel'): - content.update({'title': self.strproc(self.title_panel.getText())}) - return content - - def edit(self, edit=False, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if not (edit and abort): - self.refresh(edit) # not when we are asking for a confirmation - BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed - if (edit and abort): - return # self.abortEdition is called by BaseTextEditor.edit - self.setWysiwyg(False, init=True) # after BaseTextEditor (it affects self.getContent) - if sync: - return - # the following must NOT be done at each UI refresh! - content = self._original_content - if edit: - def getParamCb(syntax): - # set the editable text in the current user-selected syntax - def syntaxConvertCb(text=None): - if text is not None: - # Important: this also update self._original_content - content.update({'text': text}) - content.update({'syntax': syntax}) - self.textarea.setText(content['text']) - if hasattr(self, 'title_panel') and 'title' in content: - self.title_panel.setText(content['title']) - self.title_panel.setStackVisible(0, content['title'] != '') - self.setToolBar(syntax) - if content['text'] and content['syntax'] != syntax: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) - else: - syntaxConvertCb() - self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) - else: - if not self.initialized: - # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification - self.setDisplayContent() - self.display.edit(False) - - def setDisplayContent(self): - """Set the content of the HTMLTextEditor which is used for display/wysiwyg""" - content = self._original_content - text = content['text'] - if 'title' in content and content['title']: - text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text']) - self.display.setContent({'text': text}) - - def setFocus(self, focus): - self.textarea.setFocus(focus) - - def abortEdition(self, content): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.edit(False, True) - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) - _dialog.cancel_button.setText(_("No")) - _dialog.show() - - -class RichMessageEditor(RichTextEditor): - """Use the rich text editor for sending messages with extended addressing. - Recipient panels are on top and data may be synchronized from/to the unibox.""" - - @classmethod - def getOrCreate(cls, host, parent=None, callback=None): - """Get or create the message editor associated to that host. - Add it to parent if parent is not None, otherwise display it - in a popup dialog. - @param host: the host - @param parent: parent panel (or None to display in a popup). - @return: the RichTextEditor instance if parent is not None, - otherwise a popup DialogBox containing the RichTextEditor. - """ - if not hasattr(host, 'richtext'): - modifiedCb = lambda content: True - - def afterEditCb(content): - if hasattr(host.richtext, 'popup'): - host.richtext.popup.hide() - else: - host.richtext.setVisible(False) - callback() - options = ['no_title'] - style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'} - host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style) - - def add(widget, parent): - if widget.getParent() is not None: - if widget.getParent() != parent: - widget.removeFromParent() - parent.add(widget) - else: - parent.add(widget) - widget.setVisible(True) - widget.initialized = False # fake a new creation - widget.edit(True) - - if parent is None: - if not hasattr(host.richtext, 'popup'): - host.richtext.popup = DialogBox(autoHide=False, centered=True) - host.richtext.popup.setHTML("Compose your message") - host.richtext.popup.add(host.richtext) - add(host.richtext, host.richtext.popup) - host.richtext.popup.center() - else: - add(host.richtext, parent) - return host.richtext.popup if parent is None else host.richtext - - def _prepareUI(self, y_offset=0): - """Prepare the UI to host recipients panel, toolbar, text area... - @param y_offset: Y offset to start from (extra rows on top)""" - self.recipient_offset = y_offset - self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES) - RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1) - - def refresh(self, edit=None): - """Refresh the UI between edition/display mode - @param edit: set to True to display the edition mode""" - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - RichTextEditor.refresh(self, edit) - - for widget in ['recipient', 'recipient_spacer']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if not edit: - return - - if not hasattr(self, 'recipient'): - # recipient types sub-panels are automatically added by the manager - self.recipient = RecipientManager(self, self.recipient_offset) - self.recipient.createWidgets(title_format="%s: ") - self.recipient_spacer = HTML('') - self.recipient_spacer.setStyleName('recipientSpacer') - self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2) - self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer) - - if not hasattr(self, 'sync_button'): - self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True)) - self.command.insert(self.sync_button, 1) - - def syncToEditor(self): - """Synchronize from unibox.""" - def setContent(target, data): - if hasattr(self, 'recipient'): - self.recipient.setContacts({"To": [target]} if target else {}) - self.setContent({'text': data if data else '', 'syntax': ''}) - self.textarea.setText(data if data else '') - data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None) - setContent(target, data) - - def __syncToUniBox(self, recipients=None, emptyText=False): - """Synchronize to unibox if a maximum of one recipient is set. - @return True if the sync could be done, False otherwise""" - if not self.host.uni_box: - return - setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text']) - if not hasattr(self, 'recipient'): - setText() - return True - if recipients is None: - recipients = self.recipient.getContacts() - target = "" - # we could eventually allow more in the future - allowed = 1 - for key in recipients: - count = len(recipients[key]) - if count == 0: - continue - allowed -= count - if allowed < 0: - return False - # TODO: change this if later more then one recipients are allowed - target = recipients[key][0] - setText() - if target == "": - return True - if target.startswith("@"): - _class = panels.MicroblogPanel - target = None if target == "@@" else target[1:] - else: - _class = panels.ChatPanel - self.host.getOrCreateLiberviaWidget(_class, target) - return True - - def syncFromEditor(self, content): - """Synchronize to unibox and close the dialog afterward. Display - a message and leave the dialog open if the sync was not possible.""" - if self.__syncToUniBox(): - self._afterEditCb(content) - return - InfoDialog("Too many recipients", - "A message with more than one direct recipient (To)," + - " or with any special recipient (Cc or Bcc), could not be" + - " stored in the quick box.\n\nPlease finish your composing" + - " in the rich text editor, and send your message directly" + - " from here.", Width="400px").center() - - def edit(self, edit=True, abort=False, sync=False): - if not edit and not abort and not sync: # force sending message even when the text has not been modified - if not self.__sendMessage(): # message has not been sent (missing information), do nothing - return - RichTextEditor.edit(self, edit, abort, sync) - - def __sendMessage(self): - """Send the message.""" - recipients = self.recipient.getContacts() - targets = [] - for addr in recipients: - for recipient in recipients[addr]: - if recipient.startswith("@"): - targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr)) - else: - targets.append(("chat", recipient, addr)) - # check that we actually have a message target and data - content = self.getContent() - if content['text'] == "" or len(targets) == 0: - InfoDialog("Missing information", - "Some information are missing and the message hasn't been sent.", Width="400px").center() - return None - self.__syncToUniBox(recipients, emptyText=True) - extra = {'content_rich': content['text']} - if hasattr(self, 'title_panel'): - extra.update({'title': content['title']}) - self.host.send(targets, content['text'], extra=extra) - return True - - -class RecipientManager(ListManager): - """A manager for sub-panels to set the recipients for each recipient type.""" - - def __init__(self, parent, y_offset=0): - # TODO: be sure we also display empty groups and disconnected contacts + their groups - # store the full list of potential recipients (groups and contacts) - list_ = [] - list_.append("@@") - list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) - list_.extend(contact for contact in parent.host.contact_panel.getContacts()) - ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset}) - - self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, - hide=lambda sender, key: self.__children[key]["panel"].isVisible(), - callback=self.setContactPanelVisible) - - -class EditTextArea(TextArea, KeyboardHandler): - def __init__(self, _parent): - TextArea.__init__(self) - self._parent = _parent - KeyboardHandler.__init__(self) - self.addKeyboardListener(self) - - def onKeyDown(self, sender=None, keycode=None, modifiers=None): - for listener in self._parent.edit_listeners: - listener(self, keycode)
--- a/browser_side/xmlui.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,418 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.Label import Label -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from nativedom import NativeDOM -from sat_frontends.tools import xmlui - - -class EmptyWidget(xmlui.EmptyWidget, Label): - - def __init__(self, parent): - Label.__init__(self, '') - - -class TextWidget(xmlui.TextWidget, Label): - - def __init__(self, parent, value): - Label.__init__(self, value) - - -class LabelWidget(xmlui.LabelWidget, TextWidget): - - def __init__(self, parent, value): - TextWidget.__init__(self, parent, value+": ") - - -class JidWidget(xmlui.JidWidget, TextWidget): - - def __init__(self, parent, value): - TextWidget.__init__(self, parent, value) - - -class DividerWidget(xmlui.DividerWidget, HTML): - - def __init__(self, parent, style='line'): - """Add a divider - - @param parent - @param style (string): one of: - - line: a simple line - - dot: a line of dots - - dash: a line of dashes - - plain: a full thick line - - blank: a blank line/space - """ - HTML.__init__(self, "<hr/>") - self.addStyleName(style) - - -class StringWidget(xmlui.StringWidget, TextBox): - - def __init__(self, parent, value): - TextBox.__init__(self) - self.setText(value) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox): - - def __init__(self, parent, value): - PasswordTextBox.__init__(self) - self.setText(value) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class TextBoxWidget(xmlui.TextBoxWidget, TextArea): - - def __init__(self, parent, value): - TextArea.__init__(self) - self.setText(value) - - def _xmluiSetValue(self, value): - self.setText(value) - - def _xmluiGetValue(self): - return self.getText() - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - -class BoolWidget(xmlui.BoolWidget, CheckBox): - - def __init__(self, parent, state): - CheckBox.__init__(self) - self.setChecked(state) - - def _xmluiSetValue(self, value): - self.setChecked(value == "true") - - def _xmluiGetValue(self): - return "true" if self.isChecked() else "false" - - def _xmluiOnChange(self, callback): - self.addClickListener(callback) - - -class ButtonWidget(xmlui.ButtonWidget, Button): - - def __init__(self, parent, value, click_callback): - Button.__init__(self, value, click_callback) - - def _xmluiOnClick(self, callback): - self.addClickListener(callback) - - -class ListWidget(xmlui.ListWidget, ListBox): - - def __init__(self, parent, options, selected, flags): - ListBox.__init__(self) - multi_selection = 'single' not in flags - self.setMultipleSelect(multi_selection) - if multi_selection: - self.setVisibleItemCount(5) - for option in options: - self.addItem(option[1]) - self._xmlui_attr_map = {label: value for value, label in options} - self._xmluiSelectValues(selected) - - def _xmluiSelectValue(self, value): - """Select a value checking its item""" - try: - label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0] - except IndexError: - log.warning("Can't find value [%s] to select" % value) - return - self.selectItem(label) - - def _xmluiSelectValues(self, values): - """Select multiple values, ignore the items""" - self.setValueSelection(values) - - def _xmluiGetSelectedValues(self): - ret = [] - for label in self.getSelectedItemText(): - ret.append(self._xmlui_attr_map[label]) - return ret - - def _xmluiOnChange(self, callback): - self.addChangeListener(callback) - - def _xmluiAddValues(self, values, select=True): - selected = self._xmluiGetSelectedValues() - for value in values: - if value not in self._xmlui_attr_map.values(): - self.addItem(value) - self._xmlui_attr_map[value] = value - if value not in selected: - selected.append(value) - self._xmluiSelectValues(selected) - - -class LiberviaContainer(object): - - def _xmluiAppend(self, widget): - self.append(widget) - - -class AdvancedListContainer(xmlui.AdvancedListContainer, Grid): - - def __init__(self, parent, columns, selectable='no'): - Grid.__init__(self, 0, columns) - self.columns = columns - self.row = -1 - self.col = 0 - self._xmlui_rows_idx = [] - self._xmlui_selectable = selectable != 'no' - self._xmlui_selected_row = None - self.addTableListener(self) - if self._xmlui_selectable: - self.addStyleName('AdvancedListSelectable') - - def onCellClicked(self, grid, row, col): - if not self._xmlui_selectable: - return - self._xmlui_selected_row = row - try: - self._xmlui_select_cb(self) - except AttributeError: - log.warning("no select callback set") - - - def _xmluiAppend(self, widget): - self.setWidget(self.row, self.col, widget) - self.col += 1 - - def _xmluiAddRow(self, idx): - self.row += 1 - self.col = 0 - self._xmlui_rows_idx.insert(self.row, idx) - self.resizeRows(self.row+1) - - def _xmluiGetSelectedWidgets(self): - return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)] - - def _xmluiGetSelectedIndex(self): - try: - return self._xmlui_rows_idx[self._xmlui_selected_row] - except TypeError: - return None - - def _xmluiOnSelect(self, callback): - self._xmlui_select_cb = callback - - -class PairsContainer(xmlui.PairsContainer, Grid): - - def __init__(self, parent): - Grid.__init__(self, 0, 0) - self.row = 0 - self.col = 0 - - def _xmluiAppend(self, widget): - if self.col == 0: - self.resize(self.row+1, 2) - self.setWidget(self.row, self.col, widget) - self.col += 1 - if self.col == 2: - self.row +=1 - self.col = 0 - - - -class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel): - - def __init__(self, parent): - TabPanel.__init__(self) - self.setStyleName('liberviaTabPanel') - - def _xmluiAddTab(self, label): - tab_panel = VerticalContainer(self) - self.add(tab_panel, label) - if len(self.getChildren()) == 1: - self.selectTab(0) - return tab_panel - - -class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel): - __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel) - - def __init__(self, parent): - VerticalPanel.__init__(self) - - -class WidgetFactory(object): - # XXX: __getattr__ doesn't work here for an unknown reason - - def createVerticalContainer(self, *args, **kwargs): - instance = VerticalContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createPairsContainer(self, *args, **kwargs): - instance = PairsContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createTabsContainer(self, *args, **kwargs): - instance = TabsContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createAdvancedListContainer(self, *args, **kwargs): - instance = AdvancedListContainer(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createEmptyWidget(self, *args, **kwargs): - instance = EmptyWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createTextWidget(self, *args, **kwargs): - instance = TextWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createLabelWidget(self, *args, **kwargs): - instance = LabelWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createJidWidget(self, *args, **kwargs): - instance = JidWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createDividerWidget(self, *args, **kwargs): - instance = DividerWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createStringWidget(self, *args, **kwargs): - instance = StringWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createPasswordWidget(self, *args, **kwargs): - instance = PasswordWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createTextBoxWidget(self, *args, **kwargs): - instance = TextBoxWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createBoolWidget(self, *args, **kwargs): - instance = BoolWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createButtonWidget(self, *args, **kwargs): - instance = ButtonWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - def createListWidget(self, *args, **kwargs): - instance = ListWidget(*args, **kwargs) - instance._xmlui_main = self._xmlui_main - return instance - - - # def __getattr__(self, attr): - # if attr.startswith("create"): - # cls = globals()[attr[6:]] - # cls._xmlui_main = self._xmlui_main - # return cls - - -class XMLUI(xmlui.XMLUI, VerticalPanel): - widget_factory = WidgetFactory() - - def __init__(self, host, xml_data, title = None, flags = None): - self.widget_factory._xmlui_main = self - self.dom = NativeDOM() - dom_parse = lambda xml_data: self.dom.parseString(xml_data) - VerticalPanel.__init__(self) - self.setSize('100%', '100%') - xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def _xmluiClose(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def _xmluiLaunchAction(self, action_id, data): - self.host.launchAction(action_id, data) - - def _xmluiSetParam(self, name, value, category): - self.host.bridge.call('setParam', None, name, value, category) - - def constructUI(self, xml_data): - super(XMLUI, self).constructUI(xml_data) - self.add(self.main_cont) - self.setCellHeight(self.main_cont, '100%') - if self.type == 'form': - hpanel = HorizontalPanel() - hpanel.setStyleName('marginAuto') - hpanel.add(Button('Submit',self.onFormSubmitted)) - if not 'NO_CANCEL' in self.flags: - hpanel.add(Button('Cancel',self.onFormCancelled)) - self.add(hpanel) - elif self.type == 'param': - assert(isinstance(self.children[0][0],TabPanel)) - hpanel = HorizontalPanel() - hpanel.add(Button('Save', self.onSaveParams)) - hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose())) - self.add(hpanel)
--- a/constants.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Primitivus: a SAT frontend -# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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/>. - -from sat.core.i18n import D_ -from sat_frontends import constants - - -class Const(constants.Const): - - APP_NAME = 'Libervia' - SERVICE_PROFILE = 'libervia' # the SàT profile that is used for exporting the service - - TIMEOUT = 300 # Session's time out, after that the user will be disconnected - LIBERVIA_DIR = "output/" - MEDIA_DIR = "media/" - AVATARS_DIR = "avatars/" - CARDS_DIR = "games/cards/tarot" - - ERRNUM_BRIDGE_ERRBACK = 0 # FIXME - ERRNUM_LIBERVIA = 0 # FIXME - - # Security limit for Libervia (get/set params) - SECURITY_LIMIT = 5 - - # Security limit for Libervia server_side - SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT - - # Frontend parameters - ENABLE_UNIBOX_KEY = D_("Composition") - ENABLE_UNIBOX_PARAM = D_("Enable unibox") - - # MISC - PASSWORD_MIN_LENGTH = 6 # for new account creation - LOG_OPT_SECTION = APP_NAME.lower()
--- a/libervia.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,903 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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 - -### logging configuration ### -from browser_side import logging -logging.configure() -from sat.core.log import getLogger -log = getLogger(__name__) -### - -from pyjamas.ui.RootPanel import RootPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.KeyboardListener import KEY_ESCAPE -from pyjamas.Timer import Timer -from pyjamas import Window, DOM -from pyjamas.JSONService import JSONProxy - -from browser_side.register import RegisterBox -from browser_side.contact import ContactPanel -from browser_side.base_widget import WidgetsPanel -from browser_side.panels import MicroblogItem -from browser_side import panels, dialog -from browser_side.jid import JID -from browser_side.xmlui import XMLUI -from browser_side.html_tools import html_sanitize -from browser_side.notification import Notification - -from sat_frontends.tools.misc import InputHistory -from sat_frontends.tools.strings import getURLParams -from sat.core.i18n import _ -from constants import Const as C - - -MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories - -# Set to true to not create a new LiberviaWidget when a similar one -# already exist (i.e. a chat panel with the same target). Instead -# the existing widget will be eventually removed from its parent -# and added to new WidgetsPanel, or replaced to the expected -# position if the previous and the new parent are the same. -REUSE_EXISTING_LIBERVIA_WIDGETS = True - - -class LiberviaJsonProxy(JSONProxy): - def __init__(self, *args, **kwargs): - JSONProxy.__init__(self, *args, **kwargs) - self.handler = self - self.cb = {} - self.eb = {} - - def call(self, method, cb, *args): - _id = self.callMethod(method, args) - if cb: - if isinstance(cb, tuple): - if len(cb) != 2: - log.error("tuple syntax for bridge.call is (callback, errback), aborting") - return - if cb[0] is not None: - self.cb[_id] = cb[0] - self.eb[_id] = cb[1] - else: - self.cb[_id] = cb - - def onRemoteResponse(self, response, request_info): - if request_info.id in self.cb: - _cb = self.cb[request_info.id] - # if isinstance(_cb, tuple): - # #we have arguments attached to the callback - # #we send them after the answer - # callback, args = _cb - # callback(response, *args) - # else: - # #No additional argument, we call directly the callback - _cb(response) - del self.cb[request_info.id] - if request_info.id in self.eb: - del self.eb[request_info.id] - - def onRemoteError(self, code, errobj, request_info): - """def dump(obj): - print "\n\nDUMPING %s\n\n" % obj - for i in dir(obj): - print "%s: %s" % (i, getattr(obj,i))""" - if request_info.id in self.eb: - _eb = self.eb[request_info.id] - _eb((code, errobj)) - del self.cb[request_info.id] - del self.eb[request_info.id] - else: - if code != 0: - log.error("Internal server error") - """for o in code, error, request_info: - dump(o)""" - else: - if isinstance(errobj['message'], dict): - log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) - else: - log.error("%s" % errobj['message']) - - -class RegisterCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/register_api", - ["isRegistered", "isConnected", "connect", "registerParams", "getMenus"]) - - -class BridgeCall(LiberviaJsonProxy): - def __init__(self): - LiberviaJsonProxy.__init__(self, "/json_api", - ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", - "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid", - "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", - "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", - "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", - "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", - "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", - "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", - "syntaxConvert", "getAccountDialogUI", - ]) - - -class BridgeSignals(LiberviaJsonProxy): - RETRY_BASE_DELAY = 1000 - - def __init__(self, host): - self.host = host - self.retry_delay = self.RETRY_BASE_DELAY - LiberviaJsonProxy.__init__(self, "/json_signal_api", - ["getSignals"]) - - def onRemoteResponse(self, response, request_info): - self.retry_delay = self.RETRY_BASE_DELAY - LiberviaJsonProxy.onRemoteResponse(self, response, request_info) - - def onRemoteError(self, code, errobj, request_info): - if errobj['message'] == 'Empty Response': - Window.getLocation().reload() # XXX: reset page in case of session ended. - # FIXME: Should be done more properly without hard reload - LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info) - #we now try to reconnect - if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0: - Window.alert('You are not allowed to connect to server') - else: - def _timerCb(timer): - self.host.bridge_signals.call('getSignals', self.host._getSignalsCB) - Timer(notify=_timerCb).schedule(self.retry_delay) - self.retry_delay *= 2 - - -class SatWebFrontend(InputHistory): - def onModuleLoad(self): - log.info("============ onModuleLoad ==============") - panels.ChatPanel.registerClass() - panels.MicroblogPanel.registerClass() - self.whoami = None - self._selected_listeners = set() - self.bridge = BridgeCall() - self.bridge_signals = BridgeSignals(self) - self.uni_box = None - self.status_panel = HTML('<br />') - self.contact_panel = ContactPanel(self) - self.panel = panels.MainPanel(self) - self.discuss_panel = self.panel.discuss_panel - self.tab_panel = self.panel.tab_panel - self.tab_panel.addTabListener(self) - self.libervia_widgets = set() # keep track of all actives LiberviaWidgets - self.room_list = [] # list of rooms - self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel - self.avatars_cache = {} # keep track of jid's avatar hash (key=jid, value=file) - self._register_box = None - RootPanel().add(self.panel) - self.notification = Notification() - DOM.addEventPreview(self) - self._register = RegisterCall() - self._register.call('getMenus', self.panel.menu.createMenus) - self._register.call('registerParams', None) - self._register.call('isRegistered', self._isRegisteredCB) - self.initialised = False - self.init_cache = [] # used to cache events until initialisation is done - # define here the parameters that have an incidende to UI refresh - self.params_ui = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM, - "category": C.ENABLE_UNIBOX_KEY, - "cast": lambda value: value == 'true', - "value": None - } - } - - def addSelectedListener(self, callback): - self._selected_listeners.add(callback) - - def getSelected(self): - wid = self.tab_panel.getCurrentPanel() - if not isinstance(wid, WidgetsPanel): - log.error("Tab widget is not a WidgetsPanel, can't get selected widget") - return None - return wid.selected - - def setSelected(self, widget): - """Define the selected widget""" - widgets_panel = self.tab_panel.getCurrentPanel() - if not isinstance(widgets_panel, WidgetsPanel): - return - - selected = widgets_panel.selected - - if selected == widget: - return - - if selected: - selected.removeStyleName('selected_widget') - - widgets_panel.selected = widget - - if widget: - widgets_panel.selected.addStyleName('selected_widget') - - for callback in self._selected_listeners: - callback(widget) - - def resize(self): - """Resize elements""" - Window.onResize() - - def onBeforeTabSelected(self, sender, tab_index): - return True - - def onTabSelected(self, sender, tab_index): - selected = self.getSelected() - for callback in self._selected_listeners: - callback(selected) - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - #needed to prevent request cancellation in Firefox - event.preventDefault() - return True - - def getAvatar(self, jid_str): - """Return avatar of a jid if in cache, else ask for it""" - def dataReceived(result): - if 'avatar' in result: - self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar']) - else: - self.bridge.call("getCard", None, jid_str) - - def avatarError(error_data): - # The jid is maybe not in our roster, we ask for the VCard - self.bridge.call("getCard", None, jid_str) - - if jid_str not in self.avatars_cache: - self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar']) - self.avatars_cache[jid_str] = "/media/icons/tango/emotes/64/face-plain.png" - return self.avatars_cache[jid_str] - - def registerWidget(self, wid): - log.debug("Registering %s" % wid.getDebugName()) - self.libervia_widgets.add(wid) - - def unregisterWidget(self, wid): - try: - self.libervia_widgets.remove(wid) - except KeyError: - log.warning('trying to remove a non registered Widget: %s' % wid.getDebugName()) - - def refresh(self): - """Refresh the general display.""" - self.panel.refresh() - if self.params_ui['unibox']['value']: - self.uni_box = self.panel.unibox_panel.unibox - else: - self.uni_box = None - for lib_wid in self.libervia_widgets: - lib_wid.refresh() - self.resize() - - def addTab(self, label, wid, select=True): - """Create a new tab and eventually add a widget in - @param label: label of the tab - @param wid: LiberviaWidget to add - @param select: True to select the added tab - """ - widgets_panel = WidgetsPanel(self) - self.tab_panel.add(widgets_panel, label) - widgets_panel.addWidget(wid) - if select: - self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1) - return widgets_panel - - def addWidget(self, wid, tab_index=None): - """ Add a widget at the bottom of the current or specified tab - @param wid: LiberviaWidget to add - @param tab_index: index of the tab to add the widget to""" - if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount(): - panel = self.tab_panel.getCurrentPanel() - else: - panel = self.tab_panel.tabBar.getTabWidget(tab_index) - panel.addWidget(wid) - - def displayNotification(self, title, body): - self.notification.notify(title, body) - - def _isRegisteredCB(self, result): - registered, warning = result - if not registered: - self._register_box = RegisterBox(self.logged) - self._register_box.centerBox() - self._register_box.show() - if warning: - dialog.InfoDialog(_('Security warning'), warning).show() - self._tryAutoConnect(skip_validation=not not warning) - else: - self._register.call('isConnected', self._isConnectedCB) - - def _isConnectedCB(self, connected): - if not connected: - self._register.call('connect', lambda x: self.logged()) - else: - self.logged() - - def logged(self): - if self._register_box: - self._register_box.hide() - del self._register_box # don't work if self._register_box is None - - # display the real presence status panel - self.panel.header.remove(self.status_panel) - self.status_panel = panels.PresenceStatusPanel(self) - self.panel.header.add(self.status_panel) - - #it's time to fill the page - self.bridge.call('getContacts', self._getContactsCB) - self.bridge.call('getParamsUI', self._getParamsUICB) - self.bridge_signals.call('getSignals', self._getSignalsCB) - #We want to know our own jid - self.bridge.call('getProfileJid', self._getProfileJidCB) - - def domain_cb(value): - self._defaultDomain = value - log.info("new account domain: %s" % value) - - def domain_eb(value): - self._defaultDomain = "libervia.org" - - self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb)) - self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) - - # get ui params and refresh the display - count = 0 # used to do something similar to DeferredList - - def params_ui_cb(param, value=None): - count += 1 - refresh = count == len(self.params_ui) - self._paramUpdate(param['name'], value, param['category'], refresh) - for param in self.params_ui: - self.bridge.call('asyncGetParamA', lambda value: params_ui_cb(self.params_ui[param], value), - self.params_ui[param]['name'], self.params_ui[param]['category']) - - def _tryAutoConnect(self, skip_validation=False): - """This method retrieve the eventual URL parameters to auto-connect the user. - @param skip_validation: if True, set the form values but do not validate it - """ - params = getURLParams(Window.getLocation().getSearch()) - if "login" in params: - self._register_box._form.login_box.setText(params["login"]) - self._register_box._form.login_pass_box.setFocus(True) - if "passwd" in params: - # try to connect - self._register_box._form.login_pass_box.setText(params["passwd"]) - if not skip_validation: - self._register_box._form.onLogin(None) - return True - else: - # this would eventually set the browser saved password - Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True)) - - def _actionCb(self, data): - if not data: - # action was a one shot, nothing to do - pass - elif "xmlui" in data: - ui = XMLUI(self, xml_data = data['xmlui']) - options = ['NO_CLOSE'] if ui.type == 'form' else [] - _dialog = dialog.GenericDialog(ui.title, ui, options=options) - ui.setCloseCb(_dialog.close) - _dialog.show() - else: - dialog.InfoDialog("Error", - "Unmanaged action result", Width="400px").center() - - def _actionEb(self, err_data): - err_code, err_obj = err_data - dialog.InfoDialog("Error", - str(err_obj), Width="400px").center() - - def launchAction(self, callback_id, data): - """ Launch a dynamic action - @param callback_id: id of the action to launch - @param data: data needed only for certain actions - - """ - if data is None: - data = {} - self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data) - - def _getContactsCB(self, contacts_data): - for contact in contacts_data: - jid, attributes, groups = contact - self._newContactCb(jid, attributes, groups) - - def _getSignalsCB(self, signal_data): - self.bridge_signals.call('getSignals', self._getSignalsCB) - log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1])) - name, args = signal_data - if name == 'personalEvent': - self._personalEventCb(*args) - elif name == 'newMessage': - self._newMessageCb(*args) - elif name == 'presenceUpdate': - self._presenceUpdateCb(*args) - elif name == 'paramUpdate': - self._paramUpdate(*args) - elif name == 'roomJoined': - self._roomJoinedCb(*args) - elif name == 'roomLeft': - self._roomLeftCb(*args) - elif name == 'roomUserJoined': - self._roomUserJoinedCb(*args) - elif name == 'roomUserLeft': - self._roomUserLeftCb(*args) - elif name == 'roomUserChangedNick': - self._roomUserChangedNickCb(*args) - elif name == 'askConfirmation': - self._askConfirmation(*args) - elif name == 'newAlert': - self._newAlert(*args) - elif name == 'tarotGamePlayers': - self._tarotGameStartedCb(True, *args) - elif name == 'tarotGameStarted': - self._tarotGameStartedCb(False, *args) - elif name == 'tarotGameNew' or \ - name == 'tarotGameChooseContrat' or \ - name == 'tarotGameShowCards' or \ - name == 'tarotGameInvalidCards' or \ - name == 'tarotGameCardsPlayed' or \ - name == 'tarotGameYourTurn' or \ - name == 'tarotGameScore': - self._tarotGameGenericCb(name, args[0], args[1:]) - elif name == 'radiocolPlayers': - self._radioColStartedCb(True, *args) - elif name == 'radiocolStarted': - self._radioColStartedCb(False, *args) - elif name == 'radiocolPreload': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolPlay': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolNoUpload': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolUploadOk': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'radiocolSongRejected': - self._radioColGenericCb(name, args[0], args[1:]) - elif name == 'subscribe': - self._subscribeCb(*args) - elif name == 'contactDeleted': - self._contactDeletedCb(*args) - elif name == 'newContact': - self._newContactCb(*args) - elif name == 'entityDataUpdated': - self._entityDataUpdatedCb(*args) - elif name == 'chatStateReceived': - self._chatStateReceivedCb(*args) - - def _getParamsUICB(self, xmlui): - """Hide the parameters item if there's nothing to display""" - if not xmlui: - self.panel.menu.removeItemParams() - - def _ownBlogsFills(self, mblogs): - #put our own microblogs in cache, then fill all panels with them - for publisher in mblogs: - for mblog in mblogs[publisher]: - if not mblog.has_key('content'): - log.warning("No content found in microblog [%s]" % mblog) - continue - if mblog.has_key('groups'): - _groups = set(mblog['groups'].split() if mblog['groups'] else []) - else: - _groups = None - mblog_entry = MicroblogItem(mblog) - self.mblog_cache.append((_groups, mblog_entry)) - - if len(self.mblog_cache) > MAX_MBLOG_CACHE: - del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - self.FillMicroblogPanel(lib_wid) - self.initialised = True # initialisation phase is finished here - for event_data in self.init_cache: # so we have to send all the cached events - self._personalEventCb(*event_data) - del self.init_cache - - def _getProfileJidCB(self, jid): - self.whoami = JID(jid) - #we can now ask our status - self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb) - #the rooms where we are - self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb) - #and if there is any subscription request waiting for us - self.bridge.call('getWaitingSub', self._getWaitingSubCb) - #we fill the panels already here - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - if lib_wid.accept_all(): - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10) - else: - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10) - - #we ask for our own microblogs: - self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10) - - ## Signals callbacks ## - - def _personalEventCb(self, sender, event_type, data): - if not self.initialised: - self.init_cache.append((sender, event_type, data)) - return - sender = JID(sender).bare - if event_type == "MICROBLOG": - if not 'content' in data: - log.warning("No content found in microblog data") - return - if 'groups' in data: - _groups = set(data['groups'].split() if data['groups'] else []) - else: - _groups = None - mblog_entry = MicroblogItem(data) - - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - self.addBlogEntry(lib_wid, sender, _groups, mblog_entry) - - if sender == self.whoami.bare: - found = False - for index in xrange(0, len(self.mblog_cache)): - entry = self.mblog_cache[index] - if entry[1].id == mblog_entry.id: - # replace existing entry - self.mblog_cache.remove(entry) - self.mblog_cache.insert(index, (_groups, mblog_entry)) - found = True - break - if not found: - self.mblog_cache.append((_groups, mblog_entry)) - if len(self.mblog_cache) > MAX_MBLOG_CACHE: - del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - elif event_type == 'MICROBLOG_DELETE': - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - lib_wid.removeEntry(data['type'], data['id']) - log.debug("%s %s %s" % (self.whoami.bare, sender, data['type'])) - - if sender == self.whoami.bare and data['type'] == 'main_item': - for index in xrange(0, len(self.mblog_cache)): - entry = self.mblog_cache[index] - if entry[1].id == data['id']: - self.mblog_cache.remove(entry) - break - - def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry): - """Check if an entry can go in MicroblogPanel and add to it - @param mblog_panel: MicroblogPanel instance - @param sender: jid of the entry sender - @param _groups: groups which can receive this entry - @param mblog_entry: MicroblogItem instance""" - if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \ - or (_groups and _groups.intersection(mblog_panel.accepted_groups)): - mblog_panel.addEntry(mblog_entry) - - def FillMicroblogPanel(self, mblog_panel): - """Fill a microblog panel with entries in cache - @param mblog_panel: MicroblogPanel instance - """ - #XXX: only our own entries are cached - for cache_entry in self.mblog_cache: - _groups, mblog_entry = cache_entry - self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry) - - def getEntityMBlog(self, entity): - log.info("geting mblog for entity [%s]" % (entity,)) - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - if lib_wid.isJidAccepted(entity): - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10) - - def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True): - """Get the corresponding panel if it exists. - @param class_: class of the panel (ChatPanel, MicroblogPanel...) - @param entity: polymorphic parameter, see class_.matchEntity. - @param ignoreOtherTabs: if True, the widgets that are not - contained by the currently selected tab will be ignored - @return: the existing widget that has been found or None.""" - selected_tab = self.tab_panel.getCurrentPanel() - for lib_wid in self.libervia_widgets: - parent = lib_wid.getWidgetsPanel(verbose=False) - if parent is None or (ignoreOtherTabs and parent != selected_tab): - # do not return a widget that is not in the currently selected tab - continue - if isinstance(lib_wid, class_): - try: - if lib_wid.matchEntity(entity): - log.debug("existing widget found: %s" % lib_wid.getDebugName()) - return lib_wid - except AttributeError as e: - e.stack_list() - return None - return None - - def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None): - """Get the matching LiberviaWidget if it exists, or create a new one. - @param class_: class of the panel (ChatPanel, MicroblogPanel...) - @param entity: polymorphic parameter, see class_.matchEntity. - @param select: if True, select the widget that has been found or created - @param new_tab: if not None, a widget which is created is created in - a new tab. In that case new_tab is a unicode to label that new tab. - If new_tab is not None and a widget is found, no tab is created. - @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS - is set to False or if the widget has not been found, the existing - widget that has been found otherwise.""" - lib_wid = None - tab = None - if REUSE_EXISTING_LIBERVIA_WIDGETS: - lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None) - if lib_wid is None: # create a new widget - lib_wid = class_.createPanel(self, entity[0] if isinstance(entity, tuple) else entity) - if new_tab is None: - self.addWidget(lib_wid) - else: - tab = self.addTab(new_tab, lib_wid, False) - else: # reuse existing widget - tab = lib_wid.getWidgetsPanel(verbose=False) - if new_tab is None: - if tab is not None: - tab.removeWidget(lib_wid) - self.addWidget(lib_wid) - if select: - if new_tab is not None: - self.tab_panel.selectTab(tab) - # must be done after the widget is added, - # for example to scroll to the bottom - self.setSelected(lib_wid) - lib_wid.refresh() - return lib_wid - - def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): - _from = JID(from_jid) - _to = JID(to_jid) - other = _to if _from.bare == self.whoami.bare else _from - lib_wid = self.getLiberviaWidget(panels.ChatPanel, other, ignoreOtherTabs=False) - self.displayNotification(_from, msg) - if lib_wid is not None: - lib_wid.printMessage(_from, msg, extra) - else: - # The message has not been shown, we must indicate it - self.contact_panel.setContactMessageWaiting(other.bare, True) - - def _presenceUpdateCb(self, entity, show, priority, statuses): - entity_jid = JID(entity) - if self.whoami and self.whoami == entity_jid: # XXX: QnD way to get our presence/status - self.status_panel.setPresence(show) - if statuses: - self.status_panel.setStatus(statuses.values()[0]) - else: - self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses) - - def _roomJoinedCb(self, room_jid, room_nicks, user_nick): - _target = JID(room_jid) - if _target not in self.room_list: - self.room_list.append(_target) - chat_panel = panels.ChatPanel(self, _target, type_='group') - chat_panel.setUserNick(user_nick) - if _target.node.startswith('sat_tarot_'): #XXX: it's not really beautiful, but it works :) - self.addTab("Tarot", chat_panel) - elif _target.node.startswith('sat_radiocol_'): - self.addTab("Radio collective", chat_panel) - else: - self.addTab(_target.node, chat_panel) - chat_panel.setPresents(room_nicks) - chat_panel.historyPrint() - chat_panel.refresh() - - def _roomLeftCb(self, room_jid, room_nicks, user_nick): - # FIXME: room_list contains JID instances so why MUST we do - # 'remove(room_jid)' and not 'remove(JID(room_jid))' ????!! - # This looks like a pyjamas bug --> check/report - try: - self.room_list.remove(room_jid) - except KeyError: - pass - - def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.userJoined(user_nick, user_data) - - def _roomUserLeftCb(self, room_jid_s, user_nick, user_data): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.userLeft(user_nick, user_data) - - def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick): - """Called when an user joined a MUC room""" - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.changeUserNick(old_nick, new_nick) - - def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.startGame("Tarot", waiting, referee, players) - - def _tarotGameGenericCb(self, event_name, room_jid_s, args): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - getattr(lib_wid.getGame("Tarot"), event_name)(*args) - - def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - lib_wid.startGame("RadioCol", waiting, referee, players, queue_data) - - def _radioColGenericCb(self, event_name, room_jid_s, args): - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: - getattr(lib_wid.getGame("RadioCol"), event_name)(*args) - - def _getPresenceStatusesCb(self, presence_data): - for entity in presence_data: - for resource in presence_data[entity]: - args = presence_data[entity][resource] - self._presenceUpdateCb("%s/%s" % (entity, resource), *args) - - def _getRoomsJoinedCb(self, room_data): - for room in room_data: - self._roomJoinedCb(*room) - - def _getWaitingSubCb(self, waiting_sub): - for sub in waiting_sub: - self._subscribeCb(waiting_sub[sub], sub) - - def _subscribeCb(self, sub_type, entity): - if sub_type == 'subscribed': - dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show() - self.getEntityMBlog(entity) - - elif sub_type == 'unsubscribed': - dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show() - #TODO: remove microblogs from panels - - elif sub_type == 'subscribe': - #The user want to subscribe to our presence - _dialog = None - msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity)) - - def ok_cb(ignore): - self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups()) - def cancel_cb(ignore): - self.bridge.call('subscription', None, "unsubscribed", entity, '', '') - - _dialog = dialog.GroupSelector([msg], self.contact_panel.getGroups(), [], "Add", ok_cb, cancel_cb) - _dialog.setHTML('<b>Add contact request</b>') - _dialog.show() - - def _contactDeletedCb(self, entity): - self.contact_panel.removeContact(entity) - - def _newContactCb(self, contact, attributes, groups): - self.contact_panel.updateContact(contact, attributes, groups) - - def _entityDataUpdatedCb(self, entity_jid_s, key, value): - if key == "avatar": - avatar = '/avatars/%s' % value - - self.avatars_cache[entity_jid_s] = avatar - - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.MicroblogPanel): - if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare): - lib_wid.updateValue('avatar', entity_jid_s, avatar) - - def _chatStateReceivedCb(self, from_jid_s, state): - """Callback when a new chat state is received. - @param from_jid_s: JID of the contact who sent his state, or '@ALL@' - @param state: new state (string) - """ - if from_jid_s == '@ALL@': - target = '@ALL@' - nick = C.ALL_OCCUPANTS - else: - from_jid = JID(from_jid_s) - target = from_jid.bare - nick = from_jid.resource - - for lib_wid in self.libervia_widgets: - if isinstance(lib_wid, panels.ChatPanel): - if target == '@ALL' or target == lib_wid.target.bare: - if lib_wid.type == 'one2one': - lib_wid.setState(state) - elif lib_wid.type == 'group': - lib_wid.setState(state, nick=nick) - - def _askConfirmation(self, confirmation_id, confirmation_type, data): - answer_data = {} - - def confirm_cb(result): - self.bridge.call('confirmationAnswer', None, confirmation_id, result, answer_data) - - if confirmation_type == "YES/NO": - dialog.ConfirmDialog(confirm_cb, text=data["message"], title=data["title"]).show() - - def _newAlert(self, message, title, alert_type): - dialog.InfoDialog(title, message).show() - - def _paramUpdate(self, name, value, category, refresh=True): - """This is called when the paramUpdate signal is received, but also - during initialization when the UI parameters values are retrieved. - @param refresh: set to True to refresh the general UI - """ - for param in self.params_ui: - if name == self.params_ui[param]['name']: - self.params_ui[param]['value'] = self.params_ui[param]['cast'](value) - if refresh: - self.refresh() - break - - def sendError(self, errorData): - dialog.InfoDialog("Error while sending message", - "Your message can't be sent", Width="400px").center() - log.error("sendError: %s" % str(errorData)) - - def send(self, targets, text, extra={}): - """Send a message to any target type. - @param targets: list of tuples (type, entities, addr) with: - - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat") - - entities could be a JID, a list groups, a node hash... depending the target - - addr in ("To", "Cc", "Bcc") - ignore case - @param text: the message content - @param extra: options - """ - # FIXME: too many magic strings, we should use constants instead - addresses = [] - for target in targets: - type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower() - if type_ in ("PUBLIC", "GROUP"): - self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra) - elif type_ == "COMMENT": - self.bridge.call("sendMblogComment", None, entities, text, extra) - elif type_ == "STATUS": - self.bridge.call('setStatus', None, self.status_panel.presence, text) - elif type_ in ("groupchat", "chat"): - addresses.append((addr, entities)) - else: - log.error("Unknown target type") - if addresses: - if len(addresses) == 1 and addresses[0][0] == 'to': - self.bridge.call('sendMessage', (None, self.sendError), addresses[0][1], text, '', type_, extra) - else: - extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])}) - self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra) - - def showWarning(self, type_=None, msg=None): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning) - @msg: message to be displayed - """ - if not hasattr(self, "warning_popup"): - self.warning_popup = panels.WarningPopup() - self.warning_popup.showWarning(type_, msg) - - -if __name__ == '__main__': - pyjd.setup("http://localhost:8080/libervia.html") - app = SatWebFrontend() - app.onModuleLoad() - pyjd.run()
--- a/libervia_server/__init__.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1142 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -from twisted.application import service -from twisted.internet import glib2reactor -glib2reactor.install() -from twisted.internet import reactor, defer -from twisted.web import server -from twisted.web.static import File -from twisted.web.resource import Resource, NoResource -from twisted.web.util import Redirect, redirectTo -from twisted.python.components import registerAdapter -from twisted.python.failure import Failure -from twisted.words.protocols.jabber.jid import JID -from txjsonrpc.web import jsonrpc -from txjsonrpc import jsonrpclib - -from sat.core.log import getLogger -log = getLogger(__name__) -import re -import glob -import os.path -import sys -import tempfile -import shutil -import uuid -from zope.interface import Interface, Attribute, implements -from xml.dom import minidom -from httplib import HTTPS_PORT - -from constants import Const as C -from libervia_server.blog import MicroBlog -from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService -from sat.core.i18n import _, D_ -from sat.tools.xml_tools import paramsXML2XMLUI -try: - import OpenSSL - from twisted.internet import ssl - ssl_available = True -except: - ssl_available = False - - -class ISATSession(Interface): - profile = Attribute("Sat profile") - jid = Attribute("JID associated with the profile") - -class SATSession(object): - implements(ISATSession) - def __init__(self, session): - self.profile = None - self.jid = None - -class LiberviaSession(server.Session): - sessionTimeout = C.TIMEOUT - - def __init__(self, *args, **kwargs): - self.__lock = False - server.Session.__init__(self, *args, **kwargs) - - def lock(self): - """Prevent session from expiring""" - self.__lock = True - self._expireCall.reset(sys.maxint) - - def unlock(self): - """Allow session to expire again, and touch it""" - self.__lock = False - self.touch() - - def touch(self): - if not self.__lock: - server.Session.touch(self) - -class ProtectedFile(File): - """A File class which doens't show directory listing""" - - def directoryListing(self): - return NoResource() - -class SATActionIDHandler(object): - """Manage SàT action action_id lifecycle""" - ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored - - def __init__(self): - self.waiting_ids = {} - - def waitForId(self, callback, action_id, profile, *args, **kwargs): - """Wait for an action result - @param callback: method to call when action gave a result back - @param action_id: action_id to wait for - @param profile: %(doc_profile)s - @param *args: additional argument to pass to callback - @param **kwargs: idem""" - action_tuple = (action_id, profile) - self.waiting_ids[action_tuple] = (callback, args, kwargs) - reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple) - - def purgeID(self, action_tuple): - """Called when an action_id has not be handled in time""" - if action_tuple in self.waiting_ids: - log.warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple) - del self.waiting_ids[action_tuple] - - def actionResultCb(self, answer_type, action_id, data, profile): - """Manage the actionResult signal""" - action_tuple = (action_id, profile) - if action_tuple in self.waiting_ids: - callback, args, kwargs = self.waiting_ids[action_tuple] - del self.waiting_ids[action_tuple] - callback(answer_type, action_id, data, *args, **kwargs) - -class JSONRPCMethodManager(jsonrpc.JSONRPC): - - def __init__(self, sat_host): - jsonrpc.JSONRPC.__init__(self) - self.sat_host=sat_host - - def asyncBridgeCall(self, method_name, *args, **kwargs): - """Call an asynchrone bridge method and return a deferred - @param method_name: name of the method as a unicode - @return: a deferred which trigger the result - - """ - d = defer.Deferred() - - def _callback(*args): - if not args: - d.callback(None) - else: - if len(args) != 1: - Exception("Multiple return arguments not supported") - d.callback(args[0]) - - def _errback(result): - d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(result)))) - - kwargs["callback"] = _callback - kwargs["errback"] = _errback - getattr(self.sat_host.bridge, method_name)(*args, **kwargs) - return d - - -class MethodHandler(JSONRPCMethodManager): - - def __init__(self, sat_host): - JSONRPCMethodManager.__init__(self, sat_host) - self.authorized_params = None - - def render(self, request): - self.session = request.getSession() - profile = ISATSession(self.session).profile - if not profile: - #user is not identified, we return a jsonrpc fault - parsed = jsonrpclib.loads(request.content.read()) - fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) - return jsonrpc.JSONRPC.render(self, request) - - def jsonrpc_getProfileJid(self): - """Return the jid of the profile""" - sat_session = ISATSession(self.session) - profile = sat_session.profile - sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile)) - return sat_session.jid.full() - - def jsonrpc_disconnect(self): - """Disconnect the profile""" - sat_session = ISATSession(self.session) - profile = sat_session.profile - self.sat_host.bridge.disconnect(profile) - - def jsonrpc_getContacts(self): - """Return all passed args.""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getContacts(profile) - - def jsonrpc_addContact(self, entity, name, groups): - """Subscribe to contact presence, and add it to the given groups""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.addContact(entity, profile) - self.sat_host.bridge.updateContact(entity, name, groups, profile) - - def jsonrpc_delContact(self, entity): - """Remove contact from contacts list""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.delContact(entity, profile) - - def jsonrpc_updateContact(self, entity, name, groups): - """Update contact's roster item""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.updateContact(entity, name, groups, profile) - - def jsonrpc_subscription(self, sub_type, entity, name, groups): - """Confirm (or infirm) subscription, - and setup user roster in case of subscription""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.subscription(sub_type, entity, profile) - if sub_type == 'subscribed': - self.sat_host.bridge.updateContact(entity, name, groups, profile) - - def jsonrpc_getWaitingSub(self): - """Return list of room already joined by user""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getWaitingSub(profile) - - def jsonrpc_setStatus(self, presence, status): - """Change the presence and/or status - @param presence: value from ("", "chat", "away", "dnd", "xa") - @param status: any string to describe your status - """ - profile = ISATSession(self.session).profile - self.sat_host.bridge.setPresence('', presence, {'': status}, profile) - - - def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}): - """send message""" - profile = ISATSession(self.session).profile - return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile) - - def jsonrpc_sendMblog(self, type_, dest, text, extra={}): - """ Send microblog message - @param type_: one of "PUBLIC", "GROUP" - @param dest: destinees (list of groups, ignored for "PUBLIC") - @param text: microblog's text - """ - profile = ISATSession(self.session).profile - extra['allow_comments'] = 'True' - - if not type_: # auto-detect - type_ = "PUBLIC" if dest == [] else "GROUP" - - if type_ in ("PUBLIC", "GROUP") and text: - if type_ == "PUBLIC": - #This text if for the public microblog - print "sending public blog" - return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile) - else: - print "sending group blog" - dest = dest if isinstance(dest, list) else [dest] - return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) - else: - raise Exception("Invalid data") - - def jsonrpc_deleteMblog(self, pub_data, comments): - """Delete a microblog node - @param pub_data: a tuple (service, comment node identifier, item identifier) - @param comments: comments node identifier (for main item) or False - """ - profile = ISATSession(self.session).profile - return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) - - def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): - """Modify a microblog node - @param pub_data: a tuple (service, comment node identifier, item identifier) - @param comments: comments node identifier (for main item) or False - @param message: new message - @param extra: dict which option name as key, which can be: - - allow_comments: True to accept an other level of comments, False else (default: False) - - rich: if present, contain rich text in currently selected syntax - """ - profile = ISATSession(self.session).profile - if comments: - extra['allow_comments'] = 'True' - return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) - - def jsonrpc_sendMblogComment(self, node, text, extra={}): - """ Send microblog message - @param node: url of the comments node - @param text: comment - """ - profile = ISATSession(self.session).profile - if node and text: - return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) - else: - raise Exception("Invalid data") - - def jsonrpc_getMblogs(self, publisher_jid, item_ids): - """Get specified microblogs posted by a contact - @param publisher_jid: jid of the publisher - @param item_ids: list of microblogs items IDs - @return list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile) - return d - - def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids): - """Get specified microblogs posted by a contact and their comments - @param publisher_jid: jid of the publisher - @param item_ids: list of microblogs items IDs - @return list of couple (microblog data, list of microblog data)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile) - return d - - def jsonrpc_getLastMblogs(self, publisher_jid, max_item): - """Get last microblogs posted by a contact - @param publisher_jid: jid of the publisher - @param max_item: number of items to ask - @return list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile) - return d - - def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item): - """Get lasts microblogs posted by several contacts at once - @param publishers_type: one of "ALL", "GROUP", "JID" - @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids) - @param max_item: number of items to ask - @return: dictionary key=publisher's jid, value=list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile) - self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile) - return d - - def jsonrpc_getMblogComments(self, service, node): - """Get all comments of given node - @param service: jid of the service hosting the node - @param node: comments node - """ - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile) - return d - - - def jsonrpc_getPresenceStatuses(self): - """Get Presence information for connected contacts""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getPresenceStatuses(profile) - - def jsonrpc_getHistory(self, from_jid, to_jid, size, between): - """Return history for the from_jid/to_jid couple""" - sat_session = ISATSession(self.session) - profile = sat_session.profile - sat_jid = sat_session.jid - if not sat_jid: - log.error("No jid saved for this profile") - return {} - if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost(): - log.error("Trying to get history from a different jid, maybe a hack attempt ?") - return {} - d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile) - def show(result_dbus): - result = [] - for line in result_dbus: - #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types - # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query - timestamp, from_jid, to_jid, message, mess_type, extra = line - result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra))) - return result - d.addCallback(show) - return d - - def jsonrpc_joinMUC(self, room_jid, nick): - """Join a Multi-User Chat room - @room_jid: leave empty string to generate a unique name - """ - profile = ISATSession(self.session).profile - try: - if room_jid != "": - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) - return d - - def jsonrpc_inviteMUC(self, contact_jid, room_jid): - """Invite a user to a Multi-User Chat room""" - profile = ISATSession(self.session).profile - try: - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - room_id = room_jid.split("@")[0] - service = room_jid.split("@")[1] - self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile) - - def jsonrpc_mucLeave(self, room_jid): - """Quit a Multi-User Chat room""" - profile = ISATSession(self.session).profile - try: - room_jid = JID(room_jid) - except: - log.warning('Invalid room jid') - return - self.sat_host.bridge.mucLeave(room_jid.userhost(), profile) - - def jsonrpc_getRoomsJoined(self): - """Return list of room already joined by user""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getRoomsJoined(profile) - - def jsonrpc_launchTarotGame(self, other_players, room_jid=""): - """Create a room, invite the other players and start a Tarot game - @param room_jid: leave empty string to generate a unique room name - """ - profile = ISATSession(self.session).profile - try: - if room_jid != "": - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile) - - def jsonrpc_getTarotCardsPaths(self): - """Give the path of all the tarot cards""" - _join = os.path.join - _media_dir = _join(self.sat_host.media_dir,'') - return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png'))); - - def jsonrpc_tarotGameReady(self, player, referee): - """Tell to the server that we are ready to start the game""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.tarotGameReady(player, referee, profile) - - def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): - """Tell to the server the cards we want to put on the table""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile) - - def jsonrpc_launchRadioCollective(self, invited, room_jid=""): - """Create a room, invite people, and start a radio collective - @param room_jid: leave empty string to generate a unique room name - """ - profile = ISATSession(self.session).profile - try: - if room_jid != "": - room_jid = JID(room_jid).userhost() - except: - log.warning('Invalid room jid') - return - self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile) - - def jsonrpc_getEntityData(self, jid, keys): - """Get cached data for an entit - @param jid: jid of contact from who we want data - @param keys: name of data we want (list) - @return: requested data""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getEntityData(jid, keys, profile) - - def jsonrpc_getCard(self, jid): - """Get VCard for entiry - @param jid: jid of contact from who we want data - @return: id to retrieve the profile""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getCard(jid, profile) - - def jsonrpc_getAccountDialogUI(self): - """Get the dialog for managing user account - @return: XML string of the XMLUI""" - profile = ISATSession(self.session).profile - return self.sat_host.bridge.getAccountDialogUI(profile) - - def jsonrpc_getParamsUI(self): - """Return the parameters XML for profile""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getParams", C.SECURITY_LIMIT, C.APP_NAME, profile) - - def setAuthorizedParams(params_xml): - if self.authorized_params is None: - self.authorized_params = {} - for cat in minidom.parseString(params_xml.encode('utf-8')).getElementsByTagName("category"): - params = cat.getElementsByTagName("param") - params_list = [param.getAttribute("name") for param in params] - self.authorized_params[cat.getAttribute("name")] = params_list - if self.authorized_params: - return params_xml - else: - return None - - d.addCallback(setAuthorizedParams) - - d.addCallback(lambda params_xml: paramsXML2XMLUI(params_xml) if params_xml else "") - - return d - - def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): - """Return the parameter value for profile""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile) - return d - - def jsonrpc_setParam(self, name, value, category): - profile = ISATSession(self.session).profile - if category in self.authorized_params and name in self.authorized_params[category]: - return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile) - else: - log.warning("Trying to set parameter '%s' in category '%s' without authorization!!!" - % (name, category)) - - def jsonrpc_launchAction(self, callback_id, data): - #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed - # a security system with authorised callback_id must be implemented, similar to the one for authorised params - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("launchAction", callback_id, data, profile) - return d - - def jsonrpc_chatStateComposing(self, to_jid_s): - """Call the method to process a "composing" state. - @param to_jid_s: contact the user is composing to - """ - profile = ISATSession(self.session).profile - self.sat_host.bridge.chatStateComposing(to_jid_s, profile) - - def jsonrpc_getNewAccountDomain(self): - """@return: the domain for new account creation""" - d = self.asyncBridgeCall("getNewAccountDomain") - return d - - def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data): - """Send the user's answer to any previous 'askConfirmation' signal""" - profile = ISATSession(self.session).profile - self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile) - - def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT): - """ Convert a text between two syntaxes - @param text: text to convert - @param syntax_from: source syntax (e.g. "markdown") - @param syntax_to: dest syntax (e.g.: "XHTML") - @param safe: clean resulting XHTML to avoid malicious code if True (forced here) - @return: converted text """ - profile = ISATSession(self.session).profile - return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile) - - -class Register(JSONRPCMethodManager): - """This class manage the registration procedure with SàT - It provide an api for the browser, check password and setup the web server""" - - def __init__(self, sat_host): - JSONRPCMethodManager.__init__(self, sat_host) - self.profiles_waiting={} - self.request=None - - def getWaitingRequest(self, profile): - """Tell if a profile is trying to log in""" - if self.profiles_waiting.has_key(profile): - return self.profiles_waiting[profile] - else: - return None - - def render(self, request): - """ - Render method with some hacks: - - if login is requested, try to login with form data - - except login, every method is jsonrpc - - user doesn't need to be authentified for explicitely listed methods, but must be for all others - """ - if request.postpath == ['login']: - return self.loginOrRegister(request) - _session = request.getSession() - parsed = jsonrpclib.loads(request.content.read()) - method = parsed.get("method") - if method not in ['isRegistered', 'registerParams', 'getMenus']: - #if we don't call these methods, we need to be identified - profile = ISATSession(_session).profile - if not profile: - #user is not identified, we return a jsonrpc fault - fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) - self.request = request - return jsonrpc.JSONRPC.render(self, request) - - def loginOrRegister(self, request): - """This method is called with the POST information from the registering form. - - @param request: request of the register form - @return: a constant indicating the state: - - BAD REQUEST: something is wrong in the request (bad arguments) - - a return value from self._loginAccount or self._registerNewAccount - """ - try: - submit_type = request.args['submit_type'][0] - except KeyError: - return "BAD REQUEST" - - if submit_type == 'register': - return self._registerNewAccount(request) - elif submit_type == 'login': - return self._loginAccount(request) - return Exception('Unknown submit type') - - def _loginAccount(self, request): - """Try to authenticate the user with the request information. - @param request: request of the register form - @return: a constant indicating the state: - - BAD REQUEST: something is wrong in the request (bad arguments) - - AUTH ERROR: either the profile (login) or the password is wrong - - ALREADY WAITING: a request has already been submitted for this profile - - server.NOT_DONE_YET: the profile is being processed, the return - value will be given by self._logged or self._logginError - """ - try: - login_ = request.args['login'][0] - password_ = request.args['login_password'][0] - except KeyError: - return "BAD REQUEST" - - if login_.startswith('@'): - raise Exception('No profile_key allowed') - - profile_check = self.sat_host.bridge.getProfileName(login_) - if not profile_check or profile_check != login_ or not password_: - # profiles with empty passwords are restricted to local frontends - return "AUTH ERROR" - - if login_ in self.profiles_waiting: - return "ALREADY WAITING" - - def auth_eb(ignore=None): - self.__cleanWaiting(login_) - log.info("Profile %s doesn't exist or the submitted password is wrong" % login_) - request.write("AUTH ERROR") - request.finish() - - self.profiles_waiting[login_] = request - d = self.asyncBridgeCall("asyncConnect", login_, password_) - d.addCallbacks(lambda connected: self._logged(login_, request) if connected else None, auth_eb) - - return server.NOT_DONE_YET - - def _registerNewAccount(self, request): - """Create a new account, or return error - @param request: request of the register form - @return: a constant indicating the state: - - BAD REQUEST: something is wrong in the request (bad arguments) - - REGISTRATION: new account has been successfully registered - - ALREADY EXISTS: the given profile already exists - - INTERNAL or 'Unknown error (...)' - - server.NOT_DONE_YET: the profile is being processed, the return - value will be given later (one of those previously described) - """ - try: - profile = login = request.args['register_login'][0] - password = request.args['register_password'][0] - email = request.args['email'][0] - except KeyError: - return "BAD REQUEST" - if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \ - not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \ - len(password) < C.PASSWORD_MIN_LENGTH: - return "BAD REQUEST" - - def registered(result): - request.write('REGISTRATION') - request.finish() - - def registeringError(failure): - reason = failure.value.faultString - if reason == "ConflictError": - request.write('ALREADY EXISTS') - elif reason == "InternalError": - request.write('INTERNAL') - else: - log.error('Unknown registering error: %s' % (reason,)) - request.write('Unknown error (%s)' % reason) - request.finish() - - d = self.asyncBridgeCall("registerSatAccount", email, password, profile) - d.addCallback(registered) - d.addErrback(registeringError) - return server.NOT_DONE_YET - - def __cleanWaiting(self, login): - """Remove login from waiting queue""" - try: - del self.profiles_waiting[login] - except KeyError: - pass - - def _logged(self, profile, request): - """Set everything when a user just logged in - - @param profile - @param request - @return: a constant indicating the state: - - LOGGED - - SESSION_ACTIVE - """ - self.__cleanWaiting(profile) - _session = request.getSession() - sat_session = ISATSession(_session) - if sat_session.profile: - log.error(('/!\\ Session has already a profile, this should NEVER happen!')) - request.write('SESSION_ACTIVE') - request.finish() - return - sat_session.profile = profile - self.sat_host.prof_connected.add(profile) - - def onExpire(): - log.info("Session expired (profile=%s)" % (profile,)) - try: - #We purge the queue - del self.sat_host.signal_handler.queue[profile] - except KeyError: - pass - #and now we disconnect the profile - self.sat_host.bridge.disconnect(profile) - - _session.notifyOnExpire(onExpire) - - request.write('LOGGED') - request.finish() - - def _logginError(self, login, request, error_type): - """Something went wrong during logging in - @return: error - """ - self.__cleanWaiting(login) - return error_type - - def jsonrpc_isConnected(self): - _session = self.request.getSession() - profile = ISATSession(_session).profile - return self.sat_host.bridge.isConnected(profile) - - def jsonrpc_connect(self): - _session = self.request.getSession() - profile = ISATSession(_session).profile - if self.profiles_waiting.has_key(profile): - raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia - self.profiles_waiting[profile] = self.request - self.sat_host.bridge.connect(profile) - return server.NOT_DONE_YET - - def jsonrpc_isRegistered(self): - """ - @return: a couple (registered, message) with: - - registered: True if the user is already registered, False otherwise - - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise - """ - _session = self.request.getSession() - profile = ISATSession(_session).profile - if bool(profile): - return (True, None) - return (False, self.__getSecurityWarning()) - - def jsonrpc_registerParams(self): - """Register the frontend specific parameters""" - params = """ - <params> - <individual> - <category name="%(category_name)s" label="%(category_label)s"> - <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/> - </category> - </individual> - </params> - """ % { - 'category_name': C.ENABLE_UNIBOX_KEY, - 'category_label': _(C.ENABLE_UNIBOX_KEY), - 'param_name': C.ENABLE_UNIBOX_PARAM, - 'param_label': _(C.ENABLE_UNIBOX_PARAM) - } - - self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) - - def jsonrpc_getMenus(self): - """Return the parameters XML for profile""" - # XXX: we put this method in Register because we get menus before being logged - return self.sat_host.bridge.getMenus('', C.SECURITY_LIMIT) - - def __getSecurityWarning(self): - """@return: a security warning message, or None if the connection is secure""" - if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning: - return None - text = D_("You are about to connect to an unsecured service.") - if self.sat_host.connection_type == 'both': - new_port = (':%s' % self.sat_host.port_https_ext) if self.sat_host.port_https_ext != HTTPS_PORT else '' - url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port) - text += D_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'url': url} - return text - - -class SignalHandler(jsonrpc.JSONRPC): - - def __init__(self, sat_host): - Resource.__init__(self) - self.register=None - self.sat_host=sat_host - self.signalDeferred = {} - self.queue = {} - - def plugRegister(self, register): - self.register = register - - def jsonrpc_getSignals(self): - """Keep the connection alive until a signal is received, then send it - @return: (signal, *signal_args)""" - _session = self.request.getSession() - profile = ISATSession(_session).profile - if profile in self.queue: #if we have signals to send in queue - if self.queue[profile]: - return self.queue[profile].pop(0) - else: - #the queue is empty, we delete the profile from queue - del self.queue[profile] - _session.lock() #we don't want the session to expire as long as this connection is active - def unlock(signal, profile): - _session.unlock() - try: - source_defer = self.signalDeferred[profile] - if source_defer.called and source_defer.result[0] == "disconnected": - log.info(u"[%s] disconnected" % (profile,)) - _session.expire() - except IndexError: - log.error("Deferred result should be a tuple with fonction name first") - - self.signalDeferred[profile] = defer.Deferred() - self.request.notifyFinish().addBoth(unlock, profile) - return self.signalDeferred[profile] - - def getGenericCb(self, function_name): - """Return a generic function which send all params to signalDeferred.callback - function must have profile as last argument""" - def genericCb(*args): - profile = args[-1] - if not profile in self.sat_host.prof_connected: - return - if profile in self.signalDeferred: - self.signalDeferred[profile].callback((function_name,args[:-1])) - del self.signalDeferred[profile] - else: - if not self.queue.has_key(profile): - self.queue[profile] = [] - self.queue[profile].append((function_name, args[:-1])) - return genericCb - - def connected(self, profile): - assert(self.register) # register must be plugged - request = self.register.getWaitingRequest(profile) - if request: - self.register._logged(profile, request) - - def disconnected(self, profile): - if not profile in self.sat_host.prof_connected: - log.error("'disconnected' signal received for a not connected profile") - return - self.sat_host.prof_connected.remove(profile) - if profile in self.signalDeferred: - self.signalDeferred[profile].callback(("disconnected",)) - del self.signalDeferred[profile] - else: - if not self.queue.has_key(profile): - self.queue[profile] = [] - self.queue[profile].append(("disconnected",)) - - - def connectionError(self, error_type, profile): - assert(self.register) #register must be plugged - request = self.register.getWaitingRequest(profile) - if request: #The user is trying to log in - if error_type == "AUTH_ERROR": - _error_t = "AUTH ERROR" - else: - _error_t = "UNKNOWN" - self.register._logginError(profile, request, _error_t) - - def render(self, request): - """ - Render method wich reject access if user is not identified - """ - _session = request.getSession() - parsed = jsonrpclib.loads(request.content.read()) - profile = ISATSession(_session).profile - if not profile: - #user is not identified, we return a jsonrpc fault - fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia - return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) - self.request = request - return jsonrpc.JSONRPC.render(self, request) - -class UploadManager(Resource): - """This class manage the upload of a file - It redirect the stream to SàT core backend""" - isLeaf = True - NAME = 'path' #name use by the FileUpload - - def __init__(self, sat_host): - self.sat_host=sat_host - self.upload_dir = tempfile.mkdtemp() - self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) - - def getTmpDir(self): - return self.upload_dir - - def _getFileName(self, request): - """Generate unique filename for a file""" - raise NotImplementedError - - def _fileWritten(self, request, filepath): - """Called once the file is actually written on disk - @param request: HTTP request object - @param filepath: full filepath on the server - @return: a tuple with the name of the async bridge method - to be called followed by its arguments. - """ - raise NotImplementedError - - def render(self, request): - """ - Render method with some hacks: - - if login is requested, try to login with form data - - except login, every method is jsonrpc - - user doesn't need to be authentified for isRegistered, but must be for all other methods - """ - filename = self._getFileName(request) - filepath = os.path.join(self.upload_dir, filename) - #FIXME: the uploaded file is fully loaded in memory at form parsing time so far - # (see twisted.web.http.Request.requestReceived). A custom requestReceived should - # be written in the futur. In addition, it is not yet possible to get progression informations - # (see http://twistedmatrix.com/trac/ticket/288) - - with open(filepath,'w') as f: - f.write(request.args[self.NAME][0]) - - def finish(d): - error = isinstance(d, Exception) or isinstance (d, Failure) - request.write('KO' if error else 'OK') - # TODO: would be great to re-use the original Exception class and message - # but it is lost in the middle of the backtrace and encapsulated within - # a DBusException instance --> extract the data from the backtrace? - request.finish() - - d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath)) - d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) - return server.NOT_DONE_YET - - -class UploadManagerRadioCol(UploadManager): - NAME = 'song' - - def _getFileName(self, request): - extension = os.path.splitext(request.args['filename'][0])[1] - return "%s%s" % (str(uuid.uuid4()), extension) # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type - - def _fileWritten(self, request, filepath): - """Called once the file is actually written on disk - @param request: HTTP request object - @param filepath: full filepath on the server - @return: a tuple with the name of the async bridge method - to be called followed by its arguments. - """ - profile = ISATSession(request.getSession()).profile - return ("radiocolSongAdded", request.args['referee'][0], filepath, profile) - - -class UploadManagerAvatar(UploadManager): - NAME = 'avatar_path' - - def _getFileName(self, request): - return str(uuid.uuid4()) - - def _fileWritten(self, request, filepath): - """Called once the file is actually written on disk - @param request: HTTP request object - @param filepath: full filepath on the server - @return: a tuple with the name of the async bridge method - to be called followed by its arguments. - """ - profile = ISATSession(request.getSession()).profile - return ("setAvatar", filepath, profile) - - -def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS - allowed_values = ('http', 'https', 'both') - if value not in allowed_values: - raise ValueError("Invalid parameter value, not in %s" % str(allowed_values)) - return value - - -class Libervia(service.Service): - - OPT_PARAMETERS = [['connection_type', 't', 'https', "'http', 'https' or 'both' (to launch both servers).", coerceConnectionType], - ['port', 'p', 8080, 'The port number to listen HTTP on.', int], - ['port_https', 's', 8443, 'The port number to listen HTTPS on.', int], - ['port_https_ext', 'e', 0, 'The external port number used for HTTPS (0 means port_https value).', int], - ['ssl_certificate', 'c', 'libervia.pem', 'PEM certificate with both private and public parts.', str], - ['redirect_to_https', 'r', 1, 'automatically redirect from HTTP to HTTPS.', int], - ['security_warning', 'w', 1, 'warn user that he is about to connect on HTTP.', int], - ['passphrase', 'k', '', u"passphrase for the SàT profile named '%s'" % C.SERVICE_PROFILE, str], - ] - - def __init__(self, *args, **kwargs): - if not kwargs: - # During the loading of the twisted plugins, we just need the default values. - # This part is not executed when the plugin is actually started. - for name, value in [(option[0], option[2]) for option in self.OPT_PARAMETERS]: - kwargs[name] = value - self.initialised = defer.Deferred() - self.connection_type = kwargs['connection_type'] - self.port = kwargs['port'] - self.port_https = kwargs['port_https'] - self.port_https_ext = kwargs['port_https_ext'] - if not self.port_https_ext: - self.port_https_ext = self.port_https - self.ssl_certificate = kwargs['ssl_certificate'] - self.redirect_to_https = kwargs['redirect_to_https'] - self.security_warning = kwargs['security_warning'] - self.passphrase = kwargs['passphrase'] - self._cleanup = [] - root = ProtectedFile(C.LIBERVIA_DIR) - self.signal_handler = SignalHandler(self) - _register = Register(self) - _upload_radiocol = UploadManagerRadioCol(self) - _upload_avatar = UploadManagerAvatar(self) - self.signal_handler.plugRegister(_register) - self.sessions = {} #key = session value = user - self.prof_connected = set() #Profiles connected - self.action_handler = SATActionIDHandler() - ## bridge ## - try: - self.bridge=DBusBridgeFrontend() - except BridgeExceptionNoService: - print(u"Can't connect to SàT backend, are you sure it's launched ?") - sys.exit(1) - def backendReady(dummy): - self.bridge.register("connected", self.signal_handler.connected) - self.bridge.register("disconnected", self.signal_handler.disconnected) - self.bridge.register("connectionError", self.signal_handler.connectionError) - self.bridge.register("actionResult", self.action_handler.actionResultCb) - #core - for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']: - self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name)) - #plugins - for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat', - 'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers', - 'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers', - 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']: - self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin") - self.media_dir = self.bridge.getConfig('', 'media_dir') - self.local_dir = self.bridge.getConfig('', 'local_dir') - root.putChild('', Redirect('libervia.html')) - root.putChild('json_signal_api', self.signal_handler) - root.putChild('json_api', MethodHandler(self)) - root.putChild('register_api', _register) - root.putChild('upload_radiocol', _upload_radiocol) - root.putChild('upload_avatar', _upload_avatar) - root.putChild('blog', MicroBlog(self)) - root.putChild('css', ProtectedFile("server_css/")) - root.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) - root.putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR))) - root.putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg")) #We cheat for PoC because we know we are on the same host, so we use directly upload dir - self.site = server.Site(root) - self.site.sessionFactory = LiberviaSession - - self.bridge.getReady(lambda: self.initialised.callback(None), - lambda failure: self.initialised.errback(Exception(failure))) - self.initialised.addCallback(backendReady) - self.initialised.addErrback(lambda failure: log.error("Init error: %s" % failure)) - - def addCleanup(self, callback, *args, **kwargs): - """Add cleaning method to call when service is stopped - cleaning method will be called in reverse order of they insertion - @param callback: callable to call on service stop - @param *args: list of arguments of the callback - @param **kwargs: list of keyword arguments of the callback""" - self._cleanup.insert(0, (callback, args, kwargs)) - - def startService(self): - """Connect the profile for Libervia and start the HTTP(S) server(s)""" - def eb(e): - log.error(_("Connection failed: %s") % e) - self.stop() - - def initOk(dummy): - if not self.bridge.isConnected(C.SERVICE_PROFILE): - self.bridge.asyncConnect(C.SERVICE_PROFILE, self.passphrase, - callback=self._startService, errback=eb) - - self.initialised.addCallback(initOk) - - def _startService(self, dummy): - """Actually start the HTTP(S) server(s) after the profile for Libervia is connected""" - if self.connection_type in ('https', 'both'): - if not ssl_available: - raise(ImportError(_("Python module pyOpenSSL is not installed!"))) - try: - with open(os.path.expanduser(self.ssl_certificate)) as keyAndCert: - try: - cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read()) - except OpenSSL.crypto.Error as e: - log.error(_("The file '%s' must contain both private and public parts of the certificate") % self.ssl_certificate) - raise e - except IOError as e: - log.error(_("The file '%s' doesn't exist") % self.ssl_certificate) - raise e - reactor.listenSSL(self.port_https, self.site, cert.options()) - if self.connection_type in ('http', 'both'): - if self.connection_type == 'both' and self.redirect_to_https: - reactor.listenTCP(self.port, server.Site(RedirectToHTTPS(self.port, self.port_https_ext))) - else: - reactor.listenTCP(self.port, self.site) - - def stopService(self): - print "launching cleaning methods" - for callback, args, kwargs in self._cleanup: - callback(*args, **kwargs) - self.bridge.disconnect(C.SERVICE_PROFILE) - - def run(self): - reactor.run() - - def stop(self): - reactor.stop() - - -class RedirectToHTTPS(Resource): - - def __init__(self, old_port, new_port): - Resource.__init__(self) - self.isLeaf = True - self.old_port = old_port - self.new_port = new_port - - def render(self, request): - netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port) - url = "https://" + netloc + request.uri - return redirectTo(url, request) - - -registerAdapter(SATSession, server.Session, ISATSession)
--- a/libervia_server/blog.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,224 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -from sat.core.i18n import _ -from sat_frontends.tools.strings import addURLToText -from libervia_server.html_tools import sanitizeHtml -from twisted.internet import defer -from twisted.web import server -from twisted.web.resource import Resource -from twisted.words.protocols.jabber.jid import JID -from datetime import datetime -from constants import Const as C -import uuid -import re - - -class MicroBlog(Resource): - isLeaf = True - - ERROR_TEMPLATE = """ - <html> - <head profile="http://www.w3.org/2005/10/profile"> - <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png"> - <title>MICROBLOG ERROR</title> - </head> - <body> - <h1 style='text-align: center; color: red;'>%(message)s</h1> - </body> - </html> - """ - - def __init__(self, host): - self.host = host - Resource.__init__(self) - - def render_GET(self, request): - if not request.postpath: - return MicroBlog.ERROR_TEMPLATE % {'root': '', - 'message': "You must indicate a nickname"} - else: - prof_requested = request.postpath[0] - #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here - prof_found = self.host.bridge.getProfileName(prof_requested) - if not prof_found or prof_found == 'libervia': - return MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath), - 'message': "Invalid nickname"} - else: - def got_jid(pub_jid_s): - pub_jid = JID(pub_jid_s) - d2 = defer.Deferred() - item_id = None - if len(request.postpath) > 1: - if request.postpath[1] == 'atom.xml': # return the atom feed - d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None) - self.host.bridge.getLastGroupBlogsAtom(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback) - return - try: # check if the given path is a valid UUID - uuid.UUID(request.postpath[1]) - item_id = request.postpath[1] - except ValueError: - pass - d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None) - if item_id: # display one message and its comments - self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], 'libervia', d2.callback, d2.errback) - else: # display the last messages without comment - self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback) - - d1 = defer.Deferred() - JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback)) - d1.addCallbacks(got_jid) - - return server.NOT_DONE_YET - - def render_html_blog(self, mblog_data, request, profile): - """Retrieve the user parameters before actually rendering the static blog - @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data) - @param request: HTTP request - @param profile - """ - d_list = [] - style = {} - - def getCallback(param_name): - d = defer.Deferred() - d.addCallback(lambda value: style.update({param_name: value})) - d_list.append(d) - return d.callback - - eb = lambda failure: self.render_error_blog(failure, request, profile) - - for param_name in (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION): - self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=getCallback(param_name), errback=eb) - - cb = lambda dummy: self.__render_html_blog(mblog_data, style, request, profile) - defer.DeferredList(d_list).addCallback(cb) - - def __render_html_blog(self, mblog_data, style, request, profile): - """Actually render the static blog. If mblog_data is a list of dict, we are missing - the comments items so we just display the main items. If mblog_data is a list of couple, - each couple is associating a main item data with the list of its comments, so we render all. - @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data) - @param style: dict defining the blog's rendering parameters - @param request: the HTTP request - @profile - """ - if not isinstance(style, dict): - style = {} - user = sanitizeHtml(profile).encode('utf-8') - root_url = '../' * len(request.postpath) - base_url = root_url + 'blog/' + user - - def getFromData(key): - return sanitizeHtml(style[key]).encode('utf-8') if key in style else '' - - def getImageFromData(key, alt): - """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/""" - url = style[key].encode('utf-8') if key in style else '' - regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$" - return "<img src='%(url)s' alt='%(alt)s'/>" % {'alt': alt, 'url': url} if re.match(regexp, url) else alt - - request.write(""" - <html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> - <meta name="keywords" content="%(keywords)s"> - <meta name="description" content="%(description)s"> - <link rel="alternate" type="application/atom+xml" href="%(base)s/atom.xml"/> - <link rel="stylesheet" type="text/css" href="%(root)scss/blog.css" /> - <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png"> - <title>%(title)s</title> - </head> - <body> - <div class="mblog_title"><a href="%(base)s">%(banner_elt)s</a></div> - """ % {'base': base_url, - 'root': root_url, - 'user': user, - 'keywords': getFromData(C.STATIC_BLOG_PARAM_KEYWORDS), - 'description': getFromData(C.STATIC_BLOG_PARAM_DESCRIPTION), - 'title': getFromData(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user, - 'banner_elt': getImageFromData(C.STATIC_BLOG_PARAM_BANNER, user)}) - mblog_data = [(entry if isinstance(entry, tuple) else (entry, [])) for entry in mblog_data] - mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('published', 0)))) - for entry in mblog_data: - self.__render_html_entry(entry[0], base_url, request) - comments = sorted(entry[1], key=lambda entry: (float(entry.get('published', 0)))) - for comment in comments: - self.__render_html_entry(comment, base_url, request) - request.write('</body></html>') - request.finish() - - def __render_html_entry(self, entry, base_url, request): - """Render one microblog entry. - @param entry: the microblog entry - @param base_url: the base url of the blog - @param request: the HTTP request - """ - timestamp = float(entry.get('published', 0)) - datetime_ = datetime.fromtimestamp(timestamp) - is_comment = entry['type'] == 'comment' - if is_comment: - author = (_("comment from %s") % entry['author']).encode('utf-8') - item_link = '' - else: - author = ' ' - item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8') - - def getText(key): - if ('%s_xhtml' % key) in entry: - return entry['%s_xhtml' % key].encode('utf-8') - elif key in entry: - processor = addURLToText if key.startswith('content') else sanitizeHtml - return processor(entry[key]).encode('utf-8') - return '' - - def addMainItemLink(elem): - if not item_link or not elem: - return elem - return """<a href="%(link)s" class="item_link">%(elem)s</a>""" % {'link': item_link, 'elem': elem} - - header = addMainItemLink("""<div class="mblog_header"> - <div class="mblog_metadata"> - <div class="mblog_author">%(author)s</div> - <div class="mblog_timestamp">%(date)s</div> - </div> - </div>""" % {'author': author, 'date': datetime_}) - - title = addMainItemLink(getText('title')) - body = getText('content') - if title: # insert the title within the body - body = """<h1>%(title)s</h1>\n%(body)s""" % {'title': title, 'body': body} - - request.write("""<div class="mblog_entry %(extra_style)s"> - %(header)s - <span class="mblog_content">%(content)s</span> - </div>""" % - {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '', - 'item_link': item_link, - 'header': header, - 'content': body}) - - def render_atom_feed(self, feed, request): - request.write(feed.encode('utf-8')) - request.finish() - - def render_error_blog(self, error, request, profile): - request.write(MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath), - 'message': "Can't access requested data"}) - request.finish()
--- a/libervia_server/html_tools.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 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/>. - -def sanitizeHtml(text): - """Sanitize HTML by escaping everything""" - #this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml - html_escape_table = { - "&": "&", - '"': """, - "'": "'", - ">": ">", - "<": "<", - } - - return "".join(html_escape_table.get(c,c) for c in text) -
--- a/libervia_server/libervia.sh Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -#!/bin/sh - -DEBUG="" -PYTHON="python2" - -kill_process() { - # $1 is the file containing the PID to kill, $2 is the process name - if [ -f $1 ]; then - PID=`cat $1` - if ps -p $PID > /dev/null; then - echo "Terminating $2... " - kill -INT $PID - else - echo "No running process of ID $PID... removing PID file" - rm -f $1 - fi - else - echo "$2 is probably not running (PID file doesn't exist)" - fi -} - -#We use python to parse config files -eval `"$PYTHON" << PYTHONEND - -from sat.core.constants import Const as C -from sat.memory.memory import fixLocalDir -from ConfigParser import SafeConfigParser -from os.path import expanduser, join -import sys - -fixLocalDir() # XXX: tmp update code, will be removed in the future - -config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) -try: - config.read(C.CONFIG_FILES) -except: - print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") - print ("exit 1") - sys.exit() - -env=[] -env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) -env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) -print ";".join(env) -PYTHONEND -` -APP_NAME="Libervia" # FIXME: the import from Python needs libervia module to be in PYTHONPATH -APP_NAME_FILE="libervia" -PID_FILE="$PID_DIR$APP_NAME_FILE.pid" -LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" - -# if there is one argument which is "stop", then we kill Libervia -if [ $# -eq 1 ];then - if [ $1 = "stop" ];then - kill_process $PID_FILE "$APP_NAME" - exit 0 - fi - if [ $1 = "debug" ];then - echo "Launching $APP_NAME in debug mode" - DEBUG="--debug" - fi -fi - -DAEMON="n" -MAIN_OPTIONS="-${DAEMON}o" - -#Don't change the next line -AUTO_OPTIONS="" -ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" - -log_dir=`dirname "$LOG_FILE"` -if [ ! -d $log_dir ] ; then - mkdir $log_dir -fi - -echo "Starting $APP_NAME..." -twistd $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE
--- a/public/contrat_social.html Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> -<html><head> - - <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"> - <title>Salut Toi: Contrat Social</title> - - -</head><body> -Le projet Salut Toi est n d'un besoin de protection de nos -liberts, de notre vie prive et de notre indpendance. Il se veut -garant des droits et liberts qu'un utilisateur a vis vis de ses -propres informations, des informations numriques sur sa vie ou celles -de ses connaissances, des donnes qu'il manipule; et se veut galement -un point de contact humain, ne se substituant pas aux rapports rels, -mais au contraire les facilitant.<br> - -Salut Toi lutte et luttera toujours contre toute forme de main mise -sur les technologies par des intrts privs. Le rseau global doit -appartenir tous, et tre un point d'expression et de libert pour -l'Humanit.<br> - -<br> - - ce titre, Salut Toi et ceux qui y participent se basent sur un -contrat social, un engagement vis vis de ceux qui l'utilisent. Ce -contrat consiste en les points suivants:<br> - -<ul> - - <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de -l'utilisateur, libert vis vis de ses donnes. Pour cela, Salut -Toi est un logiciel Libre - condition essentielle -, et son -infrastructure se base galement sur des logiciels Libres, c'est dire -des logiciels qui respectent ces 4 liberts fondamentales - <ul> - - <li>la libert d'excuter le programme, pour tous les usages,</li> - - </ul> - <ul> - - <li>la libert d'tudier le fonctionnement du programme et de -l'adapter ses besoins,</li> - - </ul> - <ul> - - <li>la libert de redistribuer des copies du programme,</li> - - </ul> - <ul> - - <li>la libert d'amliorer le programme et de distribuer ces -amliorations au public.<br> -</li> - - </ul> -</li> - - - - - -Vous avez ainsi la possibilit d'installer votre propre version de -Salut Toi sur votre propre machine, d'en vrifier - et de -comprendre - ainsi son fonctionnement, de l'adapter vos besoins, d'en -faire profiter vos amis. - - <li>Les informations vous concernant vous appartiennent, et nous -n'aurons pas la prtention - et l'indcence ! - de considrer le -contenu que vous produisez ou faites circuler via Salut Toi comme -nous appartenant. De mme, nous nous engageons ne jamais faire de -profit en revendant vos informations personnelles.</li> - <li>Nous incitons fortement la <span style="text-decoration: underline;">dcentralisation gnralise</span>. -Salut Toi tant bas sur un protocole dcentralis (XMPP), il l'est -lui-mme par nature. La dcentralisation est essentielle pour une -meilleure protection de vos informations, une meilleure rsistance la -censure ou aux pannes, et pour viter les drives autoritaires.</li> - <li>Luttant contre les tentatives de contrle priv et les abus -commerciaux du rseau global, et afin de garder notre indpendance, -nous nous refusons toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span> -de forme de rclame commerciale de notre fait.</li> - <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous -refusons toute forme de discrimination, que ce soit pour une zone -gographique, une catgorie de la population, ou tout autre raison.</li> - <li>Nous ferons tout notre possible pour lutter contre toute -tentative de censure. Le rseau global doit tre un moyen d'expression -pour tous.</li> - <li>Nous refusons toute ide d'autorit absolue en ce qui concerne -les dcisions prises pour Salut Toi et son fonctionnement, et le -choix de la dcentralisation et l'utilisation de logiciel Libre permet -de lutter contre toute forme de hirarchie.</li> - - <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi: - <ul> - <li>nous ferons notre -possible pour aider les utilisateurs, quel que soit leur niveau</li> - <li>de mme, des efforts seront fait quant -l'accessibilit aux personnes victimes d'un handicap</li> - <li> Salut Toi , -XMPP, et les technologies utilises facilitent les changes -lectroniques, mais nous dsirons mettre l'accent sur les rencontres -relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li> - </ul> -</li> - - -</ul> - -</body></html>
--- a/public/libervia.css Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1580 +0,0 @@ -/* -Libervia: a Salut à Toi frontend -Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> -Copyright (C) 2011 Adrien Vigneron <adrienvigneron@mailoo.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/>. -*/ - - -/* - * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/ - */ - -/* reset/default styles */ - -html, body, div, span, applet, object, iframe, -p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, font, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -b, u, i, center, dl, dt, dd, li, -fieldset, form, label, legend, table, caption, -tbody, tfoot, thead, tr, th, td { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; - color: #444; -} - -/* styles for displaying rich text - START */ -h1, h2, h3, h4, h5, h6 { - margin: 0; - padding: 0; - border: 0; - outline: 0; - vertical-align: baseline; - background: transparent; - color: #444; - border-bottom: 1px solid rgb(170, 170, 170); - margin-bottom: 0.6em; -} -ol, ul { - margin: 0; - border: 0; - outline: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; - color: #444; -} -a:link { - color: blue; -} -.bubble p { - margin: 0.4em 0em; -} -.bubble img { - /* /!\ setting a max-width percentage value affects the toolbar icons */ - max-width: 600px; -} - -/* styles for displaying rich text - END */ - -blockquote, q { quotes: none; } - -blockquote:before, blockquote:after, -q:before, q:after { - content: ''; - content: none; -} - -:focus { outline: 0; } -ins { text-decoration: none; } -del { text-decoration: line-through; } - -table { - border-collapse: collapse; - border-spacing: 0; -} - -/* pyjamas iframe hide */ -iframe { position: absolute; } - - -html, body { - width: 100%; - height: 100%; - min-height: 100%; - -} - -body { - line-height: 1em; - font-size: 1em; - overflow: auto; - -} - -.scrollpanel { - margin-bottom: -10000px; - -} - -.iescrollpanelfix { - position: relative; - top: 100%; - margin-bottom: -10000px; - -} - -/* undo part of the above (non-IE) */ -html>body .iescrollpanelfix { position: static; } - -/* CSS Reset END */ - -body { - background-color: #fff; - font: normal 0.8em/1.5em Arial, Helvetica, sans-serif; -} - -.header { - background-color: #eee; - border-bottom: 1px solid #ddd; -} - -/* Misc Pyjamas stuff */ - -.menuContainer { - margin: 0 32px 0 20px; -} - -.gwt-MenuBar,.gwt-MenuBar-horizontal { - /*background-color: #01FF78; - border: 1px solid #87B3FF; - cursor: default;*/ - width: 100%; - height: 28px; - margin: 0; - padding: 5px 5px 0 5px; - line-height: 100%; - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); - border: 1px solid #ddd; - border-radius: 0 0 1em 1em; - -webkit-border-radius: 0 0 1em 1em; - -moz-border-radius: 0 0 1em 1em; - background-color: #222; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’); - background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); - background: -moz-linear-gradient(top, #444444, #222222); - background-image: -o-linear-gradient(#444444,#222222); - display: inline-block; -} - -.gwt-MenuBar-horizontal .gwt-MenuItem { - text-decoration: none; - font-weight: bold; - height: 100%; - color: #e7e5e5; - padding: 3px 15px; - /*display: block;*/ - border-radius: 1em 1em 1em 1em; - -webkit-border-radius: 1em 1em 1em 1em; - -moz-border-radius: 1em 1em 1em 1em; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - -webkit-transition: color 0.2s linear; - -moz-transition: color 0.2s linear; - -o-transition: color 0.2s linear; -} - -.gwt-MenuItem img { - padding-right: 2px; -} - -.gwt-MenuBar-horizontal .gwt-MenuItem-selected { - background-color: #eee; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′); - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -moz-linear-gradient(top, #eee, #aaa); - background-image: -o-linear-gradient(#eee,#aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - cursor: pointer; -} - -.menuSeparator { - width: 100%; -} - -.menuSeparator.gwt-MenuItem-selected { - border: 0; - background: inherit; - cursor: default; -} - -.gwt-MenuBar { - background-color: #fff; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’); - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); - background: -moz-linear-gradient(top, #fff, #ccc); - background-image: -o-linear-gradient(#fff,#ccc); - /*display: none;*/ - height: 100%; - min-width: 148px; - margin: 0; - padding: 0; - /*min-width: 148px; - top: 28px;*/ - border: solid 1px #aaa; - -webkit-border-radius: 0 0 10px 10px; - -moz-border-radius: 0 0 10px 10px; - border-radius: 0 0 10px 10px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); - box-shadow: 0 1px 3px rgba(0, 0, 0, .3); -} - -.gwt-MenuBar table { - width: 100%; - display: inline-table; -} - -.gwt-MenuBar .gwt-MenuItem { - padding: 8px 15px; -} - - -.gwt-MenuBar .gwt-MenuItem-selected { - background: #cf2828 !important; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’) !important; - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; - background: -moz-linear-gradient(top, #cf2828, #981a1a) !important; - background-image: -o-linear-gradient(#cf2828,#981a1a) !important; - color: #fff !important; - -webkit-border-radius: 0 0 0 0; - -moz-border-radius: 0 0 0 0; - border-radius: 0 0 0 0; - text-shadow: 0 1px 1px rgba(0, 0, 0, .1); - transition: color 0.2s linear; - -webkit-transition: color 0.2s linear; - -moz-transition: color 0.2s linear; - -o-transition: color 0.2s linear; - cursor: pointer; -} - -/*.menuLastPopup div tr:first-child td{ - border-radius: 0 0 9px 9px !important; - -webkit-border-radius: 0 0 9px 9px !important; - -moz-border-radius: 0 0 9px 9px !important; -}*/ - -.gwt-MenuBar tr:last-child td { - border-radius: 0 0 9px 9px !important; - -webkit-border-radius: 0 0 9px 9px !important; - -moz-border-radius: 0 0 9px 9px !important; -} - - -.menuLastPopup .gwt-MenuBar { - border-top-right-radius: 9px 9px 9px 9px; - -webkit-border-top-right-radius: 9px 9px 9px 9px; - -moz-border-top-right-radius: 9px 9px 9px 9px; -} - -.gwt-AutoCompleteTextBox { - width: 80%; - border: 1px solid #87B3FF; - margin-top: 20px; -} -.gwt-DialogBox { - padding: 10px; - border: 1px solid #aaa; - background-color: #fff; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’); - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); - background: -moz-linear-gradient(top, #fff, #ccc); - background-image: -o-linear-gradient(#fff,#ccc); - border-radius: 9px 9px 9px 9px; - -webkit-border-radius: 9px 9px 9px 9px; - -moz-border-radius: 9px 9px 9px 9px; - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); -} - -.gwt-DialogBox .Caption { - height: 20px; - font-size: 1.3em !important; - background-color: #cf2828; - background: #cf2828 !important; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; - background: -moz-linear-gradient(top, #cf2828, #981a1a) !important; - background-image: -o-linear-gradient(#cf2828,#981a1a); - color: #fff; - padding: 3px 3px 4px 3px; - margin: -10px; - margin-bottom: 5px; - font-weight: bold; - cursor: default; - text-align: center; - border-radius: 7px 7px 0 0; - -webkit-border-radius: 7px 7px 0 0; - -moz-border-radius: 7px 7px 0 0; -} - -/*DIALOG: button, listbox, textbox, label */ - -.gwt-DialogBox .gwt-button { - background-color: #ccc; - border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); - -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444͸, endColorstr=’#222’); - background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); - background: -moz-linear-gradient(top, #444, #222); - background-image: -o-linear-gradient(#444,#222); - text-shadow: 1px 1px 1px rgba(0,0,0,0.2); - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - color: #fff; - font-weight: bold; - font-size: 1em; - border: none; - -webkit-transition: color 0.2s linear; - -moz-transition: color 0.2s linear; - -o-transition: color 0.2s linear; -} - -.gwt-DialogBox .gwt-button:hover { - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); - color: #fff; - text-shadow: 1px 1px 1px rgba(0,0,0,0.25); -} - -.gwt-DialogBox .gwt-TextBox { - background-color: #fff; - border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - box-shadow:inset 0px 1px 4px #000; - -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); - -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - color: #444; - font-size: 1em; - border: none; -} - -.gwt-DialogBox .gwt-ListBox { - overflow: auto; - width: 100%; - background-color: #fff; - border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - box-shadow:inset 0px 1px 4px #000; - -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); - -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - color: #444; - font-size: 1em; - border: none; -} - -.gwt-DialogBox .gwt-Label { - margin-top: 13px; -} - -/* Custom Dialogs */ - -.formWarning { /* used when a form is not valid and must be corrected before submission */ - font-weight: bold; - color: red !important; -} - -.contactsChooser { - text-align: center; - margin:auto; - cursor: pointer; -} - -.infoDialogBody { - width: 100%; - height: 100% -} -/* Contact List */ - -div.contactBox { - width: 100%; - /* We want the contact panel to not use all the available height when displayed - in the unibox panel (grey part), because the dialogs panels (white part) should - still be visible. The setting max-height: fit-content would be appropriate here - but it doesn't work with firefox 24.0. TODO: check if the current setting works - with other browsers... the panel should of course not be displayed on 100px - but exactly fit the contacts box. - */ - max-height: 100px; -} - -.contactTitle { - color: #cf2828; - font-size: 1.7em; - text-indent: 5px; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); - width: 200px; - height: 30px; -} - -.contactsSwitch { - /* Button used to switch contacts panel */ - background: none; - border: 0; - padding: 0; - font-size: large; -} - -.groupList { - width: 100%; -} - -.groupList tr:first-child td { - padding-top: 10px; -} - -.group { - padding: 2px 15px; - margin: 5px; - display: inline-block; - text-decoration: none; - font-weight: bold; - color: #e7e5e5; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - border-radius: 1em 1em 1em 1em; - -webkit-border-radius: 1em 1em 1em 1em; - -moz-border-radius: 1em 1em 1em 1em; - background-color: #eee; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa͸); - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -moz-linear-gradient(top, #eee, #aaa); - background-image: -o-linear-gradient(#eee,#aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - box-shadow: 0px 1px 1px #000; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); - -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); -} - -div.group:hover { - color: #fff; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); - -webkit-transition: color 0.1s linear; - -moz-transition: color 0.1s linear; - -o-transition: color 0.1s linear; -} -.contact { - font-size: 1em; - margin-top: 3px; - padding: 3px 10px 3px 10px; -} - -.contact-menu-selected { - font-size: 1em; - margin-top: 3px; - padding: 3px 10px 3px 10px; - border-radius: 5px; - background-color: rgb(175, 175, 175); -} - -/* START - contact presence status */ -.contact-connected { - color: #3c7e0c; - font-weight: bold; -} -.contact-unavailable { -} -.contact-chat { - color: #3c7e0c; - font-weight: bold; -} -.contact-away { - color: brown; - font-weight: bold; -} -.contact-dnd { - color: red; - font-weight: bold; -} -.contact-xa { - color: red; - font-weight: bold; -} -/* END - contact presence status */ - -.selected { - color: #fff; - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); - border-radius: 1em 1em 1em 1em; - -webkit-border-radius: 1em 1em 1em 1em; - -moz-border-radius: 1em 1em 1em 1em; - -webkit-transition: color 0.2s linear; - -moz-transition: color 0.2s linear; - -o-transition: color 0.2s linear; -} - -.messageBox { - width: 100%; - padding: 5px; - border: 1px solid #bbb; - color: #444; - background: #fff url('media/libervia/unibox_2.png') top bottom no-repeat; - box-shadow:inset 0 0 10px #ddd; - -webkit-box-shadow:inset 0 0 10px #ddd; - -moz-box-shadow:inset 0 0 10px #ddd; - border-radius: 0px 0px 10px 10px; - height: 25px; - margin: 0px; -} - -/* UniBox & Status */ - -.uniBoxPanel { - margin: 15px 22px 0 22px; -} - -.uniBox { - width: 100%; - height: 45px; - padding: 5px; - border: 1px solid #bbb; - color: #444; - background: #fff url('media/libervia/unibox_2.png') top right no-repeat; - box-shadow:inset 0 0 10px #ddd; - -webkit-box-shadow:inset 0 0 10px #ddd; - -moz-box-shadow:inset 0 0 10px #ddd; -} - -.uniBoxButton { - width:30px; - height:45px; -} - -.statusPanel { - margin: auto; - text-align: center; - width: 100%; - padding: 5px 0px; - text-shadow: 0 -1px 1px rgba(255,255,255,0.25); - font-size: 1.2em; - background-color: #eee; - font-style: italic; - font-weight: bold; - color: #666; - cursor: pointer; -} - -.presence-button { - font-size: x-large; - padding-right: 5px; - cursor: pointer; -} - -/* RegisterBox */ - -.registerPanel_main button { - margin: 0; - padding: 0; - border: 0; -} - -.registerPanel_main div, .registerPanel_main button { - color: #fff; - text-decoration: none; -} - -.registerPanel_main{ - height: 100%; - border: 5px solid #222; - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); -} - -.registerPanel_tabs .gwt-Label { - margin: 15px 7.5px 0px 7.5px; - cursor: pointer; - font-size: larger; -} - -.registerPanel_tabs .gwt-TabBarItem div { - color: #444; - padding: 5px 7.5px; - border-radius: 5px 5px 0px 5px; - box-shadow: inset 0px 0px 2px 1px #9F2828; -} - -.registerPanel_tabs .gwt-TabBarItem div:hover { - color: #fff; - box-shadow: inset 0px 0px 2px 2px #9F2828; -} - -.registerPanel_tabs .gwt-TabBarItem-selected div { - color: #fff; - box-shadow: inset 0px 0px 2px 2px #9F2828; -} - -.registerPanel_tabs .gwt-TabBarRest { - border-bottom: 1px #3F1818 dashed; -} - -.registerPanel_right_side { - background: #111 url('media/libervia/register_right.png'); - height: 100%; - width: 100%; -} -.registerPanel_content { - margin-left: 50px; - margin-top: 30px; -} - -.registerPanel_content div { - font-size: 1em; - margin-left: 10px; - margin-top: 15px; - font-style: bold; - color: #888; -} - -.registerPanel_content input { - height: 25px; - line-height: 25px; - width: 200px; - text-indent: 11px; - - background: #000; - color: #aaa; - border: 1px solid #222; - border-radius: 15px 15px 15px 15px; - -webkit-border-radius: 15px 15px 15px 15px; - -moz-border-radius: 15px 15px 15px 15px; -} - -.registerPanel_content input:focus { - border: 1px solid #444; -} - - -.registerPanel_content .button, .registerPanel_content .button:visited { - background: #222 url('media/libervia/gradient.png') repeat-x; - display: inline-block; - text-decoration: none; - border-radius: 6px 6px 6px 6px; - -moz-border-radius: 6px 6px 6px 6px; - -webkit-border-radius: 6px 6px 6px 6px; - -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.6); - -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.6); - border-bottom: 1px solid rgba(0,0,0,0.25); - cursor: pointer; - margin-top: 30px; -} - -/* Fix for Opera */ -.button, .button:visited { - border-radius: 6px 6px 6px 6px !important; -} - -.registerPanel_content .button:hover { background-color: #111; color: #fff; } -.registerPanel_content .button:active { top: 1px; } -.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; } -.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; } -.registerPanel_content .red.button:hover { background-color: #bc0000; } - -/* Widgets */ - -.widgetsPanel td { - vertical-align: top; -} - -.widgetsPanel > div > table { - border-collapse: separate !important; - border-spacing: 7px; -} - -.widgetHeader { - margin: auto; - height: 25px; - /*border: 1px solid #ddd;*/ - border-radius: 10px 10px 0 0; - -webkit-border-radius: 10px 10px 0 0; - -moz-border-radius: 10px 10px 0 0; - background-color: #222; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444͸, endColorstr=’#222’); - background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); - background: -moz-linear-gradient(top, #444, #222); - background-image: -o-linear-gradient(#444,#222); -} - -.widgetHeader_title { - color: #fff; - font-weight: bold; - text-align: left; - text-indent: 15px; - margin-top: 4px; -} - -.widgetHeader_buttonsWrapper { - position: absolute; - top: 0; - height: 100%; - width: 100%; -} - -.widgetHeader_buttonGroup { - float: right; -} - -.widgetHeader_buttonGroup img { - background-color: transparent; - width: 25px; - height: 20px; - padding-top: 2px; - padding-bottom: 3px; - border-left: 1px solid #666; - border-top: 0; - border-radius: 0 10px 0 0; - -webkit-border-radius: 0 10px 0 0; - -moz-border-radius: 0 10px 0 0; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#555͸, endColorstr=’#333’); - background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333)); - background: -moz-linear-gradient(top, #555, #333); - background-image: -o-linear-gradient(#555,#333); -} - -.widgetHeader_closeButton { - border-radius: 0 10px 0 0 !important; - -webkit-border-radius: 0 10px 0 0 !important; - -moz-border-radius: 0 10px 0 0 !important; -} - -.widgetHeader_settingButton { - border-radius: 0 0 0 0 !important; - -webkit-border-radius: 0 0 0 0 !important; - -moz-border-radius: 0 0 0 0 !important; -} - -.widgetHeader_buttonGroup img:hover { - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); -} - -.widgetBody { - border-radius: 0 0 10px 10px; - -webkit-border-radius: 0 0 10px 10px; - -moz-border-radius: 0 0 10px 10px; - background-color: #fff; - min-width: 200px; - min-height: 150px; - box-shadow:inset 0px 0 1px #444; - -webkit-box-shadow:inset 0 0 1px #444; - -moz-box-shadow:inset 0 0 1px #444; -} - -/* BorderWidgets */ - -.bottomBorderWidget { - height: 10px !important; -} - -.leftBorderWidget, .rightBorderWidget { - width: 10px !important; -} - -/* Microblog */ - -.microblogPanel { -/* margin: auto; - width: 95% !important;*/ - width: 100%; -} - -.microblogNewButton { - width: 100%; - height: 35px; -} - -.subPanel { -} - -.subpanel .mb_entry { - padding-left: 65px; -} - -.mb_entry { - min-height: 64px; -} - -.mb_entry_header -{ - cursor: pointer; -} - -.selected_widget .selected_entry .mb_entry_header -{ - background: #cf2828; -} - -.mb_entry_author { - font-weight: bold; - padding-left: 5px; -} - -.mb_entry_avatar { - float: left; -} - -.mb_entry_avatar img { - width: 48px; - height: 48px; - padding: 8px; -} - -.mb_entry_dialog { - float: left; - min-height: 54px; - padding: 5px 20px 5px 20px; - border-collapse: separate; # for the bubble queue since the entry dialog is now a HorizontalPanel -} - -.bubble { - position: relative; - padding: 15px; - margin: 2px; - -webkit-border-radius:10px; - -moz-border-radius:10px; - border-radius:10px; - background: #EDEDED; - border-color: #C1C1C1; - border-width: 1px; - border-style: solid; - display: block; - border-collapse: separate; - min-height: 15px; # for the bubble queue to be aligned when the bubble is empty -} - -.bubble:after { - background: transparent url('media/libervia/bubble_after.png') top right no-repeat; - border: none; - content: ""; - position: absolute; - bottom: auto; - left: -20px; - top: 16px; - display: block; - height: 20; - width: 20; -} - -.bubble textarea{ - width: 100%; -} - -.mb_entry_timestamp { - font-style: italic; -} - -.mb_entry_actions { - float: right; - margin: 5px; - cursor: pointer; - font-size: large; -} - -.mb_entry_action_larger { - font-size: x-large; -} - -.mb_entry_toggle_syntax { - cursor: pointer; - text-align: right; - display: block; - position: relative; - top: -20px: - left: -20px; -} - -/* Chat & MUC Room */ - -.chatPanel { - height: 100%; - width: 100%; -} - -.chatPanel_body { - height: 100%; - width: 100%; -} - -.chatContent { - overflow: auto; - padding: 5px 15px 5px 15px; -} - -.chatText { - margin-top: 7px; -} - -.chatTextInfo { - font-weight: bold; - font-style: italic; -} - -.chatTextInfo-link { - font-weight: bold; - font-style: italic; - cursor: pointer; - display: inline; -} - -.chatArea { - height:100%; - width:100%; -} - -.chat_text_timestamp { - font-style: italic; - margin-right: -4px; - padding: 1px 3px 1px 3px; - -moz-border-radius: 15px 0 0 15px; - -webkit-border-radius: 15px 0 0 15px; - border-radius: 15px 0 0 15px; - background-color: #eee; - color: #888; - border: 1px solid #ddd; - border-right: none; -} - -.chat_text_nick { - font-weight: bold; - padding: 1px 3px 1px 3px; - -moz-border-radius: 0 15px 15px 0; - -webkit-border-radius: 10 15px 15px 0; - border-radius: 0 15px 15px 0; - background-color: #eee; - color: #b01e1e; - border: 1px solid #ddd; - border-left: none; -} - -.chat_text_msg { - white-space: pre-wrap; -} - -.chat_text_mymess { - color: #006600; -} - -.occupant { - margin-top: 10px; - margin-right: 4px; - min-width: 120px; - padding: 5px 15px 5px 15px; - font-weight: bold; - background-color: #eee; - border: 1px solid #ddd; - white-space: nowrap; -} - -.occupantsList { - border-right: 2px dotted #ddd; - margin-left: 5px; - margin-right: 10px; - height: 100%; -} - -/* Games */ - -.cardPanel { - background: #02FE03; - margin: 0 auto; -} - -.cardGamePlayerNick { - font-weight: bold; -} - -/* Radiocol */ - -.radiocolPanel { - -} - -.radiocol_metadata_lbl { - font-weight: bold; - padding-right: 5px; -} - -.radiocol_next_song { - margin-right: 5px; - font-style:italic; -} - -.radiocol_status { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - color: black; -} - -.radiocol_upload_status_ok { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - color: #28F215; -} - -.radiocol_upload_status_ko { - margin-left: 10px; - margin-right: 10px; - font-weight: bold; - color: #B80000; -} - -/* Drag and drop */ - -.dragover { - background: #cf2828 !important; - border-radius: 1em 1em 1em 1em !important; - -webkit-border-radius: 1em 1em 1em 1em !important; - -moz-border-radius: 1em 1em 1em 1em !important; -} - -.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img { - background: #cf2828 !important; -} - -.dragover.widgetHeader { - border-radius: 1em 1em 0 0 !important; - -webkit-border-radius: 1em 1em 0 0 !important; - -moz-border-radius: 1em 1em 0 0 !important; -} - -.dragover.widgetBody { - border-radius: 0 0 1em 1em !important; - -webkit-border-radius: 0 0 1em 1em !important; - -moz-border-radius: 0 0 1em 1em !important; -} - -/* Warning message */ - -.warningPopup { - font-size: 1em; - width: 100%; - height: 26px; - text-align: center; - padding: 5px 0; - border-bottom: 1px solid #444; - /*background-color: #fff; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’); - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); - background: -moz-linear-gradient(top, #fff, #ccc); - background-image: -o-linear-gradient(#fff,#ccc); */ - -} - -.warningTarget { - font-weight: bold; - -} - -.targetPublic { - background-color: red; /*#cf2828;*/ - /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); */ -} - -.targetGroup { - background-color: #00FFFB; - /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’68ba0f′, endColorstr=’#40700d’); - background: -webkit-gradient(linear, left top, left bottom, from(#68ba0f), to(#40700d)); - background: -moz-linear-gradient(top, #68ba0f, #40700d); - background-image: -o-linear-gradient(#68ba0f,#40700d); */ -} - -.targetOne2One { - background-color: #66FF00; - /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’); - background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); - background: -moz-linear-gradient(top, #444444, #222222); - background-image: -o-linear-gradient(#444444,#222222);*/ -} - -.targetStatus { - background-color: #fff; - /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’); - background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); - background: -moz-linear-gradient(top, #fff, #ccc); - background-image: -o-linear-gradient(#fff,#ccc); */ -} - -/* Tab panel */ - -.liberviaTabPanel { -} - -.gwt-TabPanel { -} - -.gwt-TabPanelBottom { - height: 100%; -} - -.gwt-TabBar { - font-weight: bold; - text-decoration: none; - border-bottom: 3px solid #a01c1c; -} - -.mainTabPanel .gwt-TabBar { - z-index: 10; - position: fixed; - bottom: 0; - left: 0; -} - -.gwt-TabBar .gwt-TabBarFirst { - height: 100%; -} - -.gwt-TabBar .gwt-TabBarRest { -} - -.liberviaTabPanel .gwt-TabBar {; -} - -.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem { - cursor: pointer; - margin-right: 5px; -} - -.liberviaTabPanel .gwt-TabBarItem div { - color: #fff; -} - -.liberviaTabPanel .gwt-TabBarItem { - color: #444 !important; - background-color: #222; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444′, endColorstr=’#222’); - background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); - background: -moz-linear-gradient(top, #444, #222); - background-image: -o-linear-gradient(#444,#222); - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - padding: 4px 15px 4px 15px; - border-radius: 1em 1em 0 0; - -webkit-border-radius: 1em 1em 0 0; - -moz-border-radius: 1em 1em 0 0; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); -} - -.liberviaTabPanel .gwt-TabBarItem-selected { - color: #fff; - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - padding: 4px 15px 4px 15px; - border-radius: 1em 1em 0 0; - -webkit-border-radius: 1em 1em 0 0; - -moz-border-radius: 1em 1em 0 0; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); -} - -.liberviaTabPanel div.gwt-TabBarItem:hover { - color: #fff; - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); - padding: 4px 15px 4px 15px; - border-radius: 1em 1em 0 0; - -webkit-border-radius: 1em 1em 0 0; - -moz-border-radius: 1em 1em 0 0; - text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); -} - -.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem-selected { - cursor: default; -} - -.globalLeftArea { - margin-top: 9px; -} - - -/* Misc */ - -.selected_widget .widgetHeader { - /* this property is set when a widget is the current target of the uniBox - * (messages entered in unibox will be sent to this widget) - */ - background-color: #cf2828; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); - background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); - background: -moz-linear-gradient(top, #cf2828, #981a1a); - background-image: -o-linear-gradient(#cf2828,#981a1a); -} - -.infoFrame { - position: relative; - width: 100%; - height: 100%; -} - -.marginAuto { - margin: auto; -} - -.transparent { - opacity: 0; -} - -/* URLs */ - -a.url { - color: blue; - text-decoration: none -} - -a:hover.url { - text-decoration: underline -} - -/* Rich Text/Message Editor */ - -.richTextEditor { -} - -.richTextEditor tbody { - width: 100%; - display: table; -} - -.richMessageEditor { - width: 100%; - margin: 9px 18px; -} - -.richTextTitle { - margin-bottom: 5px; -} - -.richTextTitle textarea { - height: 23px; - width: 99%; - margin: auto; - display: block; -} - -.richTextToolbar { - white-space: nowrap; - width: 100%; -} - -.richTextArea { - width: 100%; -} - -.richMessageArea { - width: 100%; - height: 250px; -} - -.richTextWysiwyg { - min-height: 50px; - background-color: white; - border: 1px solid #a0a0a0; - border-radius: 5px; - display: block; - font-size: larger; - white-space: pre; -} - -.richTextSyntaxLabel { - text-align: right; - margin: 14px 0px 0px 14px; - font-size: 12px; -} - -.richTextToolButton { - cursor: pointer; - width:26px; - height:26px; - vertical-align: middle; - margin: 2px 1px; - border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - box-shadow: 0px 1px 4px #000; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); - border: none; - -webkit-transition: color 0.2s linear; - -moz-transition: color 0.2s linear; - -o-transition: color 0.2s linear; -} - -.richTextIcon { - width:16px; - height:16px; - vertical-align: middle; -} - -/* Recipients panel */ - -.recipientButtonCell { - width:55px; -} - -.recipientTypeMenu { -} - -.recipientTypeItem { - cursor: pointer; - border-radius: 5px; - width: 50px; -} - -.recipientPanel { -} - -.recipientTextBox { - cursor: pointer; - width: auto; - border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - box-shadow: inset 0px 1px 4px rgba(135, 179, 255, 0.6); - -webkit-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6); - -moz-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6); - padding: 2px 1px; - margin: 0px; - color: #444; - font-size: 1em; -} - -.recipientTextBox-invalid { - box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6); - -webkit-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6); - -moz-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6); - border: 1px solid rgb(255, 0, 0); -} - -.recipientRemoveButton { - margin: 0px 10px 0px 0px; - padding: 0px; - border: 1px dashed red; - border-radius: 5px 5px 5px 5px; -} - -.recipientRemoveIcon { - color: red; - width:15px; - height:15px; - vertical-align: baseline; -} - -.dragover-recipientPanel { - border-radius: 5px; - background: none repeat scroll 0% 0% rgb(135, 179, 255); - border: 1px dashed rgb(35,79,255); -} - -.recipientSpacer { - height: 15px; -} - -/* Popup (context) menu */ - -.popupMenuItem { - cursor: pointer; - border-radius: 5px; - width: 100%; -} - -/* Contact group manager */ - -.contactGroupEditor { - width: 800px; - max-width:800px; - min-width: 800px; - margin-top: 9px; - margin-left:18px; -} - -.contactGroupRemoveButton { - margin: 0px 10px 0px 0px; - padding: 0px; - border: 1px dashed red; - border-radius: 5px 5px 5px 5px; -} - -.addContactGroupPanel { - -} - -.contactGroupPanel { - vertical-align:middle; -} - -.toggleAssignedContacts { - white-space: nowrap; -} - -.contactGroupButtonCell { - vertical-align: baseline; - width: 55px; - white-space: nowrap; -} - -/* Room and contacts chooser */ - -.room-contact-chooser { - width:380px; -} - -/* StackPanel */ - -.gwt-StackPanel { -} - -.gwt-StackPanel .gwt-StackPanelItem { - background-color: #222; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’); - background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); - background: -moz-linear-gradient(top, #444444, #222222); - background-image: -o-linear-gradient(#444444,#222222); - text-decoration: none; - font-weight: bold; - height: 100%; - color: #e7e5e5; - padding: 3px 15px; - /*display: block;*/ - border-radius: 1em 1em 1em 1em; - -webkit-border-radius: 1em 1em 1em 1em; - -moz-border-radius: 1em 1em 1em 1em; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); - -webkit-transition: color 0.2s linear; - -moz-transition: color 0.2s linear; - -o-transition: color 0.2s linear; -} - -.gwt-StackPanel .gwt-StackPanelItem:hover { - background-color: #eee; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′); - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -moz-linear-gradient(top, #eee, #aaa); - background-image: -o-linear-gradient(#eee,#aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - cursor: pointer; -} - -.gwt-StackPanel .gwt-StackPanelItem-selected { - background-color: #eee; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′); - background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); - background: -moz-linear-gradient(top, #eee, #aaa); - background-image: -o-linear-gradient(#eee,#aaa); - color: #444; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); - cursor: pointer; -} - -/* Caption Panel */ - -.gwt-CaptionPanel { - overflow: auto; - background-color: #fff; - border-radius: 5px 5px 5px 5px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - padding: 3px 5px 3px 5px; - margin: 10px 5px 10px 5px; - color: #444; - font-size: 1em; - border: solid 1px gray; -} - -/* Radio buttons */ - -.gwt-RadioButton { - white-space: nowrap; -} - -[contenteditable="true"] { -} - -/* XMLUI styles */ - -.AdvancedListSelectable tr{ - cursor: pointer; -} - -.AdvancedListSelectable tr:hover{ - background: none repeat scroll 0 0 #EE0000; -} - -.line hr { - -} - -.dot hr { - height: 0px; - border-top: 1px dotted; - border-bottom: 0px; -} - -.dash hr { - height: 0px; - border-top: 1px dashed; - border-bottom: 0px; -} - -.plain hr { - height: 10px; - color: black; - background-color: black; -} - -.blank hr { - border: 0px; -}
--- a/public/libervia.html Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -<!-- -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/>. ---> - -<html> -<head profile="http://www.w3.org/2005/10/profile"> -<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> -<meta name="pygwt:module" content="libervia"> -<link rel='stylesheet' href='libervia.css'> -<link rel="icon" type="image/png" href="sat_logo_16.png"> - -<title>Libervia</title> -</head> -<body bgcolor="white"> -<script language="javascript" src="bootstrap.js"></script> -<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe> -</body> -</html>
--- a/setup.py Fri May 16 11:51:10 2014 +0200 +++ b/setup.py Tue May 20 06:41:16 2014 +0200 @@ -1,8 +1,9 @@ -#!/usr/bin/python +#!/usr/bin/env python2 # -*- coding: utf-8 -*- # Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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 @@ -26,6 +27,8 @@ import sys import subprocess from stat import ST_MODE +import shutil +from src.server.constants import Const as C # seen here: http://stackoverflow.com/questions/7275295 try: @@ -52,6 +55,12 @@ NAME = 'libervia' LAUNCH_DAEMON_COMMAND = 'libervia' +ENV_LIBERVIA_INSTALL = "LIBERVIA_INSTALL" # environment variable to customise installation +NO_PREINSTALL_OPT = 'nopreinstall' # skip all preinstallation checks +AUTO_DEB_OPT = 'autodeb' # automaticaly install debs +CLEAN_OPT = 'clean' # remove previous installation directories +PURGE_OPT = 'purge' # remove building and previous installation directories + class MercurialException(Exception): pass @@ -78,9 +87,9 @@ with open(self.sh_script_path, 'r') as sh_file: for ori_line in sh_file: if ori_line.startswith('DAEMON='): - dest_line = 'DAEMON=""\n' # we want to launch sat as a daemon - elif ori_line.startswith('TAP_PATH='): - dest_line = 'TAP_PATH="%s/"\n' % run_dir + dest_line = 'DAEMON=""\n' # we want to launch libervia as a daemon + elif ori_line.startswith('DATA_DIR='): + dest_line = 'DATA_DIR="%s"\n' % self.install_data_dir elif ori_line.startswith('PYTHON='): dest_line = 'PYTHON="%s"\n' % sys.executable else: @@ -93,36 +102,117 @@ def custom_create_links(self): """Create symbolic links to executables""" # the script which launch the daemon - links = [(self.sh_script_path, LAUNCH_DAEMON_COMMAND)] - for source, dest in links: - dest_name, copied = copy_file(source, os.path.join(self.install_scripts, dest), link='sym') + for source, dest in self.sh_script_links: + dest = os.path.join(self.install_scripts, dest) + if os.path.islink(dest) and os.readlink(dest) != source: + os.remove(dest) # copy_file doesn't force the link update + dest_name, copied = copy_file(source, dest, link='sym') assert (copied) # we change the perm in the same way as in the original install_scripts mode = ((os.stat(dest_name)[ST_MODE]) | 0555) & 07777 os.chmod(dest_name, mode) def pyjs_build(self): - return subprocess.call('pyjsbuild libervia --no-compile-inplace -m -I %s' % self.install_lib, shell=True) + """Build the browser side JS files from Python source.""" + cwd = os.getcwd() + os.chdir(os.path.join('src', 'browser')) + result = subprocess.call('pyjsbuild libervia_main --no-compile-inplace -I %s -o %s' % + (self.install_lib, self.pyjamas_output_dir), shell=True) + os.chdir(cwd) + return result + + def copy_data_files(self): + """To copy the JS files couldn't be done with the data_files parameter + of setuptools.setup because all the files to be copied must exist before + the call. Also, we need the value of self.install_lib to build the JS + files (it's not easily predictable as it may vary from one system to + another), so we can't call pyjsbuild before setuptools.setup. + """ + html = os.path.join(self.install_data_dir, C.HTML_DIR) + if os.path.isdir(html): + shutil.rmtree(html, ignore_errors=True) + shutil.copytree(self.pyjamas_output_dir, html) def run(self): + self.sh_script_path = os.path.join(self.install_lib, NAME, 'libervia.sh') + self.sh_script_links = [(self.sh_script_path, LAUNCH_DAEMON_COMMAND)] + self.install_data_dir = os.path.join(self.install_data, 'share', NAME) + self.pyjamas_output_dir = os.path.join(os.getcwd(), 'html') sys.stdout.write('running pre installation stuff\n') sys.stdout.flush() - build_result = self.pyjs_build() + if PURGE_OPT in install_opt: + self.purge() + elif CLEAN_OPT in install_opt: + self.clean() + install.run(self) + sys.stdout.write('running post installation stuff\n') + sys.stdout.flush() + build_result = self.pyjs_build() # build after libervia.common is accessible if build_result == 127: # TODO: remove magic string print "pyjsbuild is not installed or not accessible from the PATH of user '%s'" % os.getenv('USERNAME') return if build_result != 0: print "pyjsbuild failed to build libervia" return - install.run(self) - sys.stdout.write('running post installation stuff\n') - sys.stdout.flush() - self.sh_script_path = os.path.join(self.install_lib, 'libervia_server', 'libervia.sh') + self.copy_data_files() self.custom_auto_options() self.custom_create_links() + def confirm(self, message): + """Ask the user for a confirmation""" + message += 'Proceed' + while True: + res = raw_input("%s (y/n)? " % message) + if res not in ['y', 'Y', 'n', 'N']: + print "Your response ('%s') was not one of the expected responses: y, n" % res + message = 'Proceed' + continue + if res in ('y', 'Y'): + return True + return False -def preinstall_check(): + def clean(self, message=None, to_remove=None): + """Clean previous installation directories + + @param message (str): to use a non-default confirmation message + @param to_remove (str): extra files/directories to remove + """ + if message is None: + message = "Cleaning previous installation directories" + if to_remove is None: + to_remove = [] + to_remove.extend([os.path.join(self.install_lib, NAME), + self.install_data_dir, + os.path.join(self.install_data, 'share', 'doc', NAME), + os.path.join(self.install_lib, "%s-py%s.egg-info" % (self.config_vars['dist_fullname'], self.config_vars['py_version_short'])), + ]) + for source, dest in self.sh_script_links: + dest = os.path.join(self.install_scripts, dest) + if os.path.islink(dest): + to_remove.append(dest) + plugin_file = os.path.join(self.install_lib, 'twisted', 'plugins', NAME) + if os.path.isfile(plugin_file): + to_remove.append(plugin_file) + + message = "%s:\n%s\n" % (message, "\n".join([" %s" % path for path in to_remove])) + if not self.confirm(message): + return + sys.stdout.write('cleaning previous installation directories...\n') + sys.stdout.flush() + for path in to_remove: + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + else: + os.remove(path) + + def purge(self): + """Clean building and previous installation directories""" + message = "Cleaning building and previous installation directories" + to_remove = [os.path.join(os.getcwd(), 'build'), self.pyjamas_output_dir] + self.clean(message, to_remove) + + +def preinstall_check(install_opt): """Check presence of problematic dependencies, and try to install them with package manager This ugly stuff is necessary as distributions are not installed correctly with setuptools/distribute Hope to remove this at some point""" @@ -138,10 +228,18 @@ # which modules are not installed ? modules_toinstall = [mod for mod in modules_tocheck if not module_installed(mod)] + """# is mercurial available ? + hg_installed = subprocess.call('which hg', stdout=open('/dev/null', 'w'), shell=True) == 0 + if not hg_installed: + modules_toinstall.append('mercurial')""" # hg can be installed from pypi if modules_toinstall: - # are we on a distribution using apt ? - apt_path = subprocess.Popen('which apt-get', stdout=subprocess.PIPE, shell=True).communicate()[0][:-1] + if AUTO_DEB_OPT in install_opt: # auto debian installation is requested + # are we on a distribution using apt ? + apt_path = subprocess.Popen('which apt-get', stdout=subprocess.PIPE, shell=True).communicate()[0][:-1] + else: + apt_path = None + not_installed = set() if apt_path: # we have apt, we'll try to use it @@ -164,9 +262,9 @@ if sys.argv[1].lower() in ['egg_info', 'install']: # we only check dependencies if egg_info or install is used - install_opt = os.environ.get("LIBERVIA_INSTALL", "") - if not "nopreinstall" in install_opt: # user can force preinstall skipping - preinstall_check() + install_opt = os.environ.get(ENV_LIBERVIA_INSTALL, "").split() + if not NO_PREINSTALL_OPT in install_opt: # user can force preinstall skipping + preinstall_check(install_opt) setup(name=NAME, version='0.4.0', @@ -181,14 +279,15 @@ 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 'Operating System :: POSIX :: Linux', 'Topic :: Communications :: Chat'], - package_dir={'libervia': '.', 'libervia_server': 'libervia_server'}, - packages=['libervia', 'libervia.output', 'libervia_server', 'twisted.plugins'], - package_data={'libervia': ['libervia.py'], 'libervia.output': ['**/*.*'], 'libervia_server': ['libervia.sh']}, - data_files=[('share/doc/%s' % NAME, ['COPYING', 'README'])], + package_dir={'libervia': 'src', 'twisted.plugins': 'src/twisted/plugins'}, + packages=['libervia', 'libervia.common', 'libervia.server', 'twisted.plugins'], + package_data={'libervia': ['libervia.sh']}, + data_files=[(os.path.join('share', 'doc', NAME), ['COPYING', 'README', 'INSTALL']), + (os.path.join('share', NAME, C.SERVER_CSS_DIR), [os.path.join(C.SERVER_CSS_DIR, filename) for filename in os.listdir(C.SERVER_CSS_DIR)]), + ], scripts=[], zip_safe=False, dependency_links=['http://www.blarg.net/%7Esteveha/pyfeed-0.7.4.tar.gz', 'http://www.blarg.net/%7Esteveha/xe-0.7.4.tar.gz'], install_requires=['sat', 'twisted', 'pyfeed', 'xe', 'txJSON-RPC', 'zope.interface', 'pyopenssl'], cmdclass={'install': CustomInstall}, ) -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/base_panels.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,609 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ +from sat_frontends.tools.strings import addURLToText, addURLToImage + +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.Button import Button +from pyjamas.ui.HTML import HTML +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.StackPanel import StackPanel +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT +from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas import DOM + +from datetime import datetime +from time import time + +from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML +from constants import Const as C + + +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, state=None, special=""): + """ + @param nick: the user nickname + @param state: the user chate state (XEP-0085) + @param special: a string of symbols (e.g: for activities) + """ + HTML.__init__(self) + self.nick = nick + self._state = state + self.special = special + self._refresh() + + def __str__(self): + return self.nick + + def setState(self, state): + self._state = state + self._refresh() + + def addSpecial(self, special): + """@param special: unicode""" + if special not in self.special: + self.special += special + self._refresh() + + def removeSpecials(self, special): + """@param special: unicode or list""" + if not isinstance(special, list): + special = [special] + for symbol in special: + self.special = self.special.replace(symbol, "") + self._refresh() + + def _refresh(self): + state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' + special = "" if len(self.special) == 0 else " %s" % self.special + self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state)) + + +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: + log.error("trying to remove an unexisting nick") + + def clear(self): + self.occupants_list.clear() + AbsolutePanel.clear(self) + + def updateSpecials(self, occupants=[], html=""): + """Set the specified html "symbol" to the listed occupants, + and eventually remove it from the others (if they got it). + This is used for example to visualize who is playing a game. + @param occupants: list of the occupants that need the symbol + @param html: unicode symbol (actually one character or more) + or a list to assign different symbols of the same family. + """ + index = 0 + special = html + for occupant in self.occupants_list.keys(): + if occupant in occupants: + if isinstance(html, list): + special = html[index] + index = (index + 1) % len(html) + self.occupants_list[occupant].addSpecial(special) + else: + self.occupants_list[occupant].removeSpecials(html) + + +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=None, **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"} + if isinstance(style, dict): + 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) + + +class TitlePanel(ToggleStackPanel): + """A toggle panel to set the message title""" + def __init__(self): + ToggleStackPanel.__init__(self, Width="100%") + self.text_area = TextArea() + self.add(self.text_area, _("Title")) + self.addStackChangeListener(self) + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + text = self.text_area.getText() + suffix = "" if (visible or not text) else (": %s" % text) + sender.setStackText(index, _("Title") + suffix) + + def getText(self): + return self.text_area.getText() + + def setText(self, text): + self.text_area.setText(text) + + +class BaseTextEditor(object): + """Basic definition of a text editor. The method edit gets a boolean parameter which + should be set to True when you want to edit the text and False to only display it.""" + + def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): + """ + Remark when inheriting this class: since the setContent method could be + overwritten by the child class, you should consider calling this __init__ + after all the parameters affecting this setContent method have been set. + @param content: dict with at least a 'text' key + @param strproc: method to be applied on strings to clean the content + @param modifiedCb: method to be called when the text has been modified. + If this method returns: + - True: the modification will be saved and afterEditCb called; + - False: the modification won't be saved and afterEditCb called; + - None: the modification won't be saved and afterEditCb not called. + @param afterEditCb: method to be called when the edition is done + """ + if content is None: + content = {'text': ''} + assert('text' in content) + if strproc is None: + def strproc(text): + try: + return text.strip() + except (TypeError, AttributeError): + return text + self.strproc = strproc + self.__modifiedCb = modifiedCb + self._afterEditCb = afterEditCb + self.initialized = False + self.edit_listeners = [] + self.setContent(content) + + def setContent(self, content=None): + """Set the editable content. The displayed content, which is set from the child class, could differ. + @param content: dict with at least a 'text' key + """ + if content is None: + content = {'text': ''} + elif not isinstance(content, dict): + content = {'text': content} + assert('text' in content) + self._original_content = {} + for key in content: + self._original_content[key] = self.strproc(content[key]) + + def getContent(self): + """Get the current edited or editable content. + @return: dict with at least a 'text' key + """ + raise NotImplementedError + + def setOriginalContent(self, content): + """Use this method with care! Content initialization should normally be + done with self.setContent. This method exists to let you trick the editor, + e.g. for self.modified to return True also when nothing has been modified. + @param content: dict + """ + self._original_content = content + + def getOriginalContent(self): + """ + @return the original content before modification (dict) + """ + return self._original_content + + def modified(self, content=None): + """Check if the content has been modified. + Remark: we don't use the direct comparison because we want to ignore empty elements + @content: content to be check against the original content or None to use the current content + @return: True if the content has been modified. + """ + if content is None: + content = self.getContent() + # the following method returns True if one non empty element exists in a but not in b + diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] + # the following method returns True if the values for the common keys are not equals + diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] + # finally the combination of both to return True if a difference is found + diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) + + return diff(content, self._original_content) + + def edit(self, edit, abort=False, sync=False): + """ + Remark: the editor must be visible before you call this method. + @param edit: set to True to edit the content or False to only display it + @param abort: set to True to cancel the edition and loose the changes. + If edit and abort are both True, self.abortEdition can be used to ask for a + confirmation. When edit is False and abort is True, abortion is actually done. + @param sync: set to True to cancel the edition after the content has been saved somewhere else + """ + if edit: + if not self.initialized: + self.syncToEditor() # e.g.: use the selected target and unibox content + self.setFocus(True) + if abort: + content = self.getContent() + if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation + self.edit(False, True, sync) + return + if sync: + self.syncFromEditor(content) # e.g.: save the content to unibox + return + else: + if not self.initialized: + return + content = self.getContent() + if abort: + self._afterEditCb(content) + return + if self.__modifiedCb and self.modified(content): + result = self.__modifiedCb(content) # e.g.: send a message or update something + if result is not None: + if self._afterEditCb: + self._afterEditCb(content) # e.g.: restore the display mode + if result is True: + self.setContent(content) + elif self._afterEditCb: + self._afterEditCb(content) + + self.initialized = True + + def setFocus(self, focus): + """ + @param focus: set to True to focus the editor + """ + raise NotImplementedError + + def syncToEditor(self): + pass + + def syncFromEditor(self, content): + pass + + def abortEdition(self, content): + return True + + def addEditListener(self, listener): + """Add a method to be called whenever the text is edited. + @param listener: method taking two arguments: sender, keycode""" + self.edit_listeners.append(listener) + + +class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): + """Base class for manage a simple text editor.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + """ + @param content + @param modifiedCb + @param afterEditCb + @param options: dict with the following value: + - no_xhtml: set to True to clean any xhtml content. + - enhance_display: if True, the display text will be enhanced with addURLToText + - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. + - listen_focus: set to True to terminate the edition when the focus is lost. + - listen_click: set to True to start the edition when you click on the widget. + """ + self.options = {'no_xhtml': False, + 'enhance_display': True, + 'listen_keyboard': True, + 'listen_focus': False, + 'listen_click': False + } + if options: + self.options.update(options) + self.__shift_down = False + if self.options['listen_focus']: + FocusHandler.__init__(self) + if self.options['listen_click']: + ClickHandler.__init__(self) + KeyboardHandler.__init__(self) + strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text) + BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) + self.textarea = self.display = None + + def setContent(self, content=None): + BaseTextEditor.setContent(self, content) + + def getContent(self): + raise NotImplementedError + + def edit(self, edit, abort=False, sync=False): + BaseTextEditor.edit(self, edit) + if edit: + if self.options['listen_focus'] and self not in self.textarea._focusListeners: + self.textarea.addFocusListener(self) + if self.options['listen_click']: + self.display.clearClickListener() + if self not in self.textarea._keyboardListeners: + self.textarea.addKeyboardListener(self) + else: + self.setDisplayContent() + if self.options['listen_focus']: + try: + self.textarea.removeFocusListener(self) + except ValueError: + pass + if self.options['listen_click'] and self not in self.display._clickListeners: + self.display.addClickListener(self) + try: + self.textarea.removeKeyboardListener(self) + except ValueError: + pass + + def setDisplayContent(self): + text = self._original_content['text'] + if not self.options['no_xhtml']: + text = addURLToImage(text) + if self.options['enhance_display']: + text = addURLToText(text) + self.display.setHTML(convertNewLinesToXHTML(text)) + + def setFocus(self, focus): + raise NotImplementedError + + def onKeyDown(self, sender, keycode, modifiers): + for listener in self.edit_listeners: + listener(self.textarea, keycode) + if not self.options['listen_keyboard']: + return + if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> + self.__shift_down = True + return + if keycode == KEY_ENTER: # finish the edition + self.textarea.setFocus(False) + if not self.options['listen_focus']: + self.edit(False) + + def onKeyUp(self, sender, keycode, modifiers): + if keycode == KEY_SHIFT: + self.__shift_down = False + + def onLostFocus(self, sender): + """Finish the edition when focus is lost""" + if self.options['listen_focus']: + self.edit(False) + + def onClick(self, sender=None): + """Start the edition when the widget is clicked""" + if self.options['listen_click']: + self.edit(True) + + def onBrowserEvent(self, event): + if self.options['listen_focus']: + FocusHandler.onBrowserEvent(self, event) + if self.options['listen_click']: + ClickHandler.onBrowserEvent(self, event) + KeyboardHandler.onBrowserEvent(self, event) + + +class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): + """Manage a simple text editor with the HTML 5 "contenteditable" property.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + HTML.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = self.display = self + + def getContent(self): + text = DOM.getInnerHTML(self.getElement()) + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setHTML(self._original_content['text']) + self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.getElement().focus() + else: + self.getElement().blur() + + +class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): + """Manage a simple text editor with a TextArea for editing, HTML for display.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + SimplePanel.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = TextArea() + self.display = HTML() + + def getContent(self): + text = self.textarea.getText() + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setText(self._original_content['text']) + self.setWidget(self.textarea if edit else self.display) + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.textarea.setCursorPos(len(self.textarea.getText())) + self.textarea.setFocus(focus)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/base_widget.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,730 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) +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.ScrollPanel import ScrollPanel +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.TabPanel import TabPanel +from pyjamas.ui.HTMLPanel import HTMLPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.Button import Button +from pyjamas.ui.Image import Image +from pyjamas.ui.Widget import Widget +from pyjamas.ui.DragWidget import DragWidget +from pyjamas.ui.DropWidget import DropWidget +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui import HasAlignment +from pyjamas import DOM +from pyjamas import Window +from __pyjamas__ import doc + +import dialog + + +class DragLabel(DragWidget): + + def __init__(self, text, _type): + DragWidget.__init__(self) + self._text = text + self._type = _type + + def onDragStart(self, event): + dt = event.dataTransfer + dt.setData('text/plain', "%s\n%s" % (self._text, self._type)) + dt.setDragImage(self.getElement(), 15, 15) + + +class LiberviaDragWidget(DragLabel): + """ A DragLabel which keep the widget being dragged as class value """ + current = None # widget currently dragged + + def __init__(self, text, _type, widget): + DragLabel.__init__(self, text, _type) + self.widget = widget + + def onDragStart(self, event): + LiberviaDragWidget.current = self.widget + DragLabel.onDragStart(self, event) + + def onDragEnd(self, event): + LiberviaDragWidget.current = None + + +class DropCell(DropWidget): + """Cell in the middle grid which replace itself with the dropped widget on DnD""" + drop_keys = {} + + def __init__(self, host): + DropWidget.__init__(self) + self.host = host + self.setStyleName('dropCell') + + @classmethod + def addDropKey(cls, key, callback): + DropCell.drop_keys[key] = callback + + def onDragEnter(self, event): + if self == LiberviaDragWidget.current: + return + self.addStyleName('dragover') + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + 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): + self.removeStyleName('dragover') + DOM.eventPreventDefault(event) + dt = event.dataTransfer + # 'text', 'text/plain', and 'Text' are equivalent. + try: + item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed + if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and + item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report + # item_type = dt.getData("type") + log.debug("message: %s" % item) + log.debug("type: %s" % item_type) + except: + log.debug("no message found") + item = ' ' + item_type = None + if item_type == "WIDGET": + if not LiberviaDragWidget.current: + log.error("No widget registered in LiberviaDragWidget !") + return + _new_panel = LiberviaDragWidget.current + if self == _new_panel: # We can't drop on ourself + return + # we need to remove the widget from the panel as it will be inserted elsewhere + widgets_panel = _new_panel.getWidgetsPanel() + wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] + row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) + if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: + # the dropped widget is the only one in the same row + # as the target widget (self), we don't do anything + return + widgets_panel.removeWidget(_new_panel) + elif item_type in self.drop_keys: + _new_panel = self.drop_keys[item_type](self.host, item) + else: + log.warning("unmanaged item type") + return + if isinstance(self, LiberviaWidget): + self.host.unregisterWidget(self) + self.onQuit() + if not isinstance(_new_panel, LiberviaWidget): + log.warning("droping an object which is not a class of LiberviaWidget") + _flextable = self.getParent() + _widgetspanel = _flextable.getParent().getParent() + row_idx, cell_idx = self._getCellAndRow(_flextable, event) + if self.host.getSelected == self: + self.host.setSelected(None) + _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) + """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) + _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 WidgetHeader(AbsolutePanel, LiberviaDragWidget): + + def __init__(self, parent, title): + AbsolutePanel.__init__(self) + self.add(title) + button_group_wrapper = SimplePanel() + button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') + button_group = HorizontalPanel() + button_group.setStyleName('widgetHeader_buttonGroup') + setting_button = Image("media/icons/misc/settings.png") + setting_button.setStyleName('widgetHeader_settingButton') + setting_button.addClickListener(parent.onSetting) + close_button = Image("media/icons/misc/close.png") + close_button.setStyleName('widgetHeader_closeButton') + close_button.addClickListener(parent.onClose) + button_group.add(setting_button) + button_group.add(close_button) + button_group_wrapper.setWidget(button_group) + self.add(button_group_wrapper) + self.addStyleName('widgetHeader') + LiberviaDragWidget.__init__(self, "", "WIDGET", parent) + + +class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): + """Libervia's widget which can replace itself with a dropped widget on DnD""" + + def __init__(self, host, title='', selectable=False): + """Init the widget + @param host: SatWebFrontend object + @param title: title show in the header of the widget + @param selectable: True is widget can be selected by user""" + VerticalPanel.__init__(self) + DropCell.__init__(self, host) + ClickHandler.__init__(self) + self.__selectable = selectable + self.__title_id = HTMLPanel.createUniqueId() + self.__setting_button_id = HTMLPanel.createUniqueId() + self.__close_button_id = HTMLPanel.createUniqueId() + self.__title = Label(title) + self.__title.setStyleName('widgetHeader_title') + self._close_listeners = [] + header = WidgetHeader(self, self.__title) + self.add(header) + self.setSize('100%', '100%') + self.addStyleName('widget') + if self.__selectable: + self.addClickListener(self) + + def onClose(sender): + """Check dynamically if the unibox is enable or not""" + if self.host.uni_box: + self.host.uni_box.onWidgetClosed(sender) + + self.addCloseListener(onClose) + self.host.registerWidget(self) + + def getDebugName(self): + return "%s (%s)" % (self, self.__title.getText()) + + def getWidgetsPanel(self, verbose=True): + return self.getParent(WidgetsPanel, verbose) + + def getParent(self, class_=None, verbose=True): + """ + Note: this method overrides pyjamas.ui.Widget.getParent + @param class_: class of the ancestor to look for or None to return the first parent + @param verbose: set to True to log error messages # FIXME: must be removed + @return: the parent/ancestor or None if it has not been found + """ + current = Widget.getParent(self) + if class_ is None: + return current # this is the default behavior + while current is not None and not isinstance(current, class_): + current = Widget.getParent(current) + if current is None and verbose: + log.debug("Can't find parent %s for %s" % (class_, self)) + return current + + def onClick(self, sender): + self.host.setSelected(self) + + def onClose(self, sender): + """ Called when the close button is pushed """ + _widgetspanel = self.getWidgetsPanel() + _widgetspanel.removeWidget(self) + for callback in self._close_listeners: + callback(self) + self.onQuit() + + def onQuit(self): + """ Called when the widget is actually ending """ + pass + + def addCloseListener(self, callback): + """Add a close listener to this widget + @param callback: function to be called from self.onClose""" + self._close_listeners.append(callback) + + def refresh(self): + """This can be overwritten by a child class to refresh the display when, + instead of creating a new one, an existing widget is found and reused. + """ + pass + + def onSetting(self, sender): + widpanel = self.getWidgetsPanel() + row, col = widpanel.getIndex(self) + body = VerticalPanel() + + # colspan & rowspan + colspan = widpanel.getColSpan(row, col) + rowspan = widpanel.getRowSpan(row, col) + + def onColSpanChange(value): + widpanel.setColSpan(row, col, value) + + def onRowSpanChange(value): + widpanel.setRowSpan(row, col, value) + colspan_setter = dialog.IntSetter("Columns span", colspan) + colspan_setter.addValueChangeListener(onColSpanChange) + colspan_setter.setWidth('100%') + rowspan_setter = dialog.IntSetter("Rows span", rowspan) + rowspan_setter.addValueChangeListener(onRowSpanChange) + rowspan_setter.setWidth('100%') + body.add(colspan_setter) + body.add(rowspan_setter) + + # size + width_str = self.getWidth() + if width_str.endswith('px'): + width = int(width_str[:-2]) + else: + width = 0 + height_str = self.getHeight() + if height_str.endswith('px'): + height = int(height_str[:-2]) + else: + height = 0 + + def onWidthChange(value): + if not value: + self.setWidth('100%') + else: + self.setWidth('%dpx' % value) + + def onHeightChange(value): + if not value: + self.setHeight('100%') + else: + self.setHeight('%dpx' % value) + width_setter = dialog.IntSetter("width (0=auto)", width) + width_setter.addValueChangeListener(onWidthChange) + width_setter.setWidth('100%') + height_setter = dialog.IntSetter("height (0=auto)", height) + height_setter.addValueChangeListener(onHeightChange) + height_setter.setHeight('100%') + body.add(width_setter) + body.add(height_setter) + + # reset + def onReset(sender): + colspan_setter.setValue(1) + rowspan_setter.setValue(1) + width_setter.setValue(0) + height_setter.setValue(0) + + reset_bt = Button("Reset", onReset) + body.add(reset_bt) + body.setCellHorizontalAlignment(reset_bt, HasAlignment.ALIGN_CENTER) + + _dialog = dialog.GenericDialog("Widget setting", body) + _dialog.show() + + def setTitle(self, text): + """change the title in the header of the widget + @param text: text of the new title""" + self.__title.setText(text) + + def isSelectable(self): + return self.__selectable + + def setSelectable(self, selectable): + if not self.__selectable: + try: + self.removeClickListener(self) + except ValueError: + pass + if self.selectable and not self in self._clickListeners: + self.addClickListener(self) + self.__selectable = selectable + + def getWarningData(self): + """ Return exposition warning level when this widget is selected and something is sent to it + This method should be overriden by children + @return: tuple (warning level type/HTML msg). Type can be one of: + - PUBLIC + - GROUP + - ONE2ONE + - MISC + - NONE + """ + if not self.__selectable: + log.error("getWarningLevel must not be called for an unselectable widget") + raise Exception + # TODO: cleaner warning types (more general constants) + return ("NONE", None) + + def setWidget(self, widget, scrollable=True): + """Set the widget that will be in the body of the LiberviaWidget + @param widget: widget to put in the body + @param scrollable: if true, the widget will be in a ScrollPanelWrapper""" + if scrollable: + _scrollpanelwrapper = ScrollPanelWrapper() + _scrollpanelwrapper.setStyleName('widgetBody') + _scrollpanelwrapper.setWidget(widget) + body_wid = _scrollpanelwrapper + else: + body_wid = widget + self.add(body_wid) + self.setCellHeight(body_wid, '100%') + + def doDetachChildren(self): + # We need to force the use of a panel subclass method here, + # for the same reason as doAttachChildren + VerticalPanel.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 + VerticalPanel.doAttachChildren(self) + + def matchEntity(self, entity): + """This method should be overwritten by child classes.""" + raise NotImplementedError + + +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 EmptyWidget(DropCell, SimplePanel): + """Empty dropable panel""" + + def __init__(self, host): + SimplePanel.__init__(self) + DropCell.__init__(self, host) + #self.setWidget(HTML('')) + self.setSize('100%', '100%') + + +class BorderWidget(EmptyWidget): + def __init__(self, host): + EmptyWidget.__init__(self, host) + self.addStyleName('borderPanel') + + +class LeftBorderWidget(BorderWidget): + def __init__(self, host): + BorderWidget.__init__(self, host) + self.addStyleName('leftBorderWidget') + + +class RightBorderWidget(BorderWidget): + def __init__(self, host): + BorderWidget.__init__(self, host) + self.addStyleName('rightBorderWidget') + + +class BottomBorderWidget(BorderWidget): + def __init__(self, host): + BorderWidget.__init__(self, host) + self.addStyleName('bottomBorderWidget') + + +class WidgetsPanel(ScrollPanelWrapper): + + def __init__(self, host, locked=False): + ScrollPanelWrapper.__init__(self) + self.setSize('100%', '100%') + self.host = host + self.locked = locked # if True: tab will not be removed when there are no more widgets inside + self.selected = None + self.flextable = FlexTable() + self.flextable.setSize('100%', '100%') + self.setWidget(self.flextable) + self.setStyleName('widgetsPanel') + _bottom = BottomBorderWidget(self.host) + self.flextable.setWidget(0, 0, _bottom) # There will be always an Empty widget on the last row, + # dropping a widget there will add a new row + td_elt = _bottom.getElement().parentNode + DOM.setStyleAttribute(td_elt, "height", "1px") # needed so the cell adapt to the size of the border (specially in webkit) + self._max_cols = 1 # give the maximum number of columns i a raw + + def isLocked(self): + return self.locked + + def changeWidget(self, row, col, wid): + """Change the widget in the given location, add row or columns when necessary""" + log.debug("changing widget: %s %s %s" % (wid.getDebugName(), row, col)) + last_row = max(0, self.flextable.getRowCount() - 1) + try: + prev_wid = self.flextable.getWidget(row, col) + except: + log.error("Trying to change an unexisting widget !") + return + + cellFormatter = self.flextable.getFlexCellFormatter() + + if isinstance(prev_wid, BorderWidget): + # We are on a border, we must create a row and/or columns + log.debug("BORDER WIDGET") + prev_wid.removeStyleName('dragover') + + if isinstance(prev_wid, BottomBorderWidget): + # We are on the bottom border, we create a new row + self.flextable.insertRow(last_row) + self.flextable.setWidget(last_row, 0, LeftBorderWidget(self.host)) + self.flextable.setWidget(last_row, 1, wid) + self.flextable.setWidget(last_row, 2, RightBorderWidget(self.host)) + cellFormatter.setHorizontalAlignment(last_row, 2, HasAlignment.ALIGN_RIGHT) + row = last_row + + elif isinstance(prev_wid, LeftBorderWidget): + if col != 0: + log.error("LeftBorderWidget must be on the first column !") + return + self.flextable.insertCell(row, col + 1) + self.flextable.setWidget(row, 1, wid) + + elif isinstance(prev_wid, RightBorderWidget): + if col != self.flextable.getCellCount(row) - 1: + log.error("RightBorderWidget must be on the last column !") + return + self.flextable.insertCell(row, col) + self.flextable.setWidget(row, col, wid) + + else: + prev_wid.removeFromParent() + self.flextable.setWidget(row, col, wid) + + _max_cols = max(self._max_cols, self.flextable.getCellCount(row)) + if _max_cols != self._max_cols: + self._max_cols = _max_cols + self._sizesAdjust() + + def _sizesAdjust(self): + cellFormatter = self.flextable.getFlexCellFormatter() + width = 100.0 / max(1, self._max_cols - 2) # we don't count the borders + + for row_idx in xrange(self.flextable.getRowCount()): + for col_idx in xrange(self.flextable.getCellCount(row_idx)): + _widget = self.flextable.getWidget(row_idx, col_idx) + if not isinstance(_widget, BorderWidget): + td_elt = _widget.getElement().parentNode + DOM.setStyleAttribute(td_elt, "width", "%.2f%%" % width) + + last_row = max(0, self.flextable.getRowCount() - 1) + cellFormatter.setColSpan(last_row, 0, self._max_cols) + + def addWidget(self, wid): + """Add a widget to a new cell on the next to last row""" + last_row = max(0, self.flextable.getRowCount() - 1) + log.debug("putting widget %s at %d, %d" % (wid.getDebugName(), last_row, 0)) + self.changeWidget(last_row, 0, wid) + + def removeWidget(self, wid): + """Remove a widget and the cell where it is""" + _row, _col = self.flextable.getIndex(wid) + self.flextable.remove(wid) + self.flextable.removeCell(_row, _col) + if not self.getLiberviaRowWidgets(_row): # we have no more widgets, we remove the row + self.flextable.removeRow(_row) + _max_cols = 1 + for row_idx in xrange(self.flextable.getRowCount()): + _max_cols = max(_max_cols, self.flextable.getCellCount(row_idx)) + if _max_cols != self._max_cols: + self._max_cols = _max_cols + self._sizesAdjust() + current = self + + blank_page = self.getLiberviaWidgetsCount() == 0 # do we still have widgets on the page ? + + if blank_page and not self.isLocked(): + # we now notice the MainTabPanel that the WidgetsPanel is empty and need to be removed + while current is not None: + if isinstance(current, MainTabPanel): + current.onWidgetPanelRemove(self) + return + current = current.getParent() + log.error("no MainTabPanel found !") + + def getWidgetCoords(self, wid): + return self.flextable.getIndex(wid) + + def getLiberviaRowWidgets(self, row): + """ Return all the LiberviaWidget in the row """ + return [wid for wid in self.getRowWidgets(row) if isinstance(wid, LiberviaWidget)] + + def getRowWidgets(self, row): + """ Return all the widgets in the row """ + widgets = [] + cols = self.flextable.getCellCount(row) + for col in xrange(cols): + widgets.append(self.flextable.getWidget(row, col)) + return widgets + + def getLiberviaWidgetsCount(self): + """ Get count of contained widgets """ + return len([wid for wid in self.flextable if isinstance(wid, LiberviaWidget)]) + + def getIndex(self, wid): + return self.flextable.getIndex(wid) + + def getColSpan(self, row, col): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.getColSpan(row, col) + + def setColSpan(self, row, col, value): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.setColSpan(row, col, value) + + def getRowSpan(self, row, col): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.getRowSpan(row, col) + + def setRowSpan(self, row, col, value): + cellFormatter = self.flextable.getFlexCellFormatter() + return cellFormatter.setRowSpan(row, col, value) + + +class DropTab(Label, DropWidget): + + def __init__(self, tab_panel, text): + Label.__init__(self, text) + DropWidget.__init__(self, tab_panel) + self.tab_panel = tab_panel + self.setStyleName('dropCell') + self.setWordWrap(False) + DOM.setStyleAttribute(self.getElement(), "min-width", "30px") + + def _getIndex(self): + """ get current index of the DropTab """ + # XXX: awful hack, but seems the only way to get index + return self.tab_panel.tabBar.panel.getWidgetIndex(self.getParent().getParent()) - 1 + + def onDragEnter(self, event): + #if self == LiberviaDragWidget.current: + # return + self.addStyleName('dragover') + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + self.removeStyleName('dragover') + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def onDrop(self, event): + DOM.eventPreventDefault(event) + self.removeStyleName('dragover') + if self._getIndex() == self.tab_panel.tabBar.getSelectedTab(): + # the widget come from the DragTab, so nothing to do, we let it there + return + + # FIXME: quite the same stuff as in DropCell, need some factorisation + dt = event.dataTransfer + # 'text', 'text/plain', and 'Text' are equivalent. + try: + item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed + if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and + item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report + # item_type = dt.getData("type") + log.debug("message: %s" % item) + log.debug("type: %s" % item_type) + except: + log.debug("no message found") + item = ' ' + item_type = None + if item_type == "WIDGET": + if not LiberviaDragWidget.current: + log.error("No widget registered in LiberviaDragWidget !") + return + _new_panel = LiberviaDragWidget.current + _new_panel.getWidgetsPanel().removeWidget(_new_panel) + elif item_type in DropCell.drop_keys: + _new_panel = DropCell.drop_keys[item_type](self.tab_panel.host, item) + else: + log.warning("unmanaged item type") + return + + widgets_panel = self.tab_panel.getWidget(self._getIndex()) + widgets_panel.addWidget(_new_panel) + + +class MainTabPanel(TabPanel): + + def __init__(self, host): + TabPanel.__init__(self) + self.host = host + self.tabBar.setVisible(False) + self.setStyleName('liberviaTabPanel') + self.addStyleName('mainTabPanel') + Window.addWindowResizeListener(self) + + def getCurrentPanel(self): + """ Get the panel of the currently selected tab """ + return self.deck.visibleWidget + + def onWindowResized(self, width, height): + tab_panel_elt = self.getElement() + _elts = doc().getElementsByClassName('gwt-TabBar') + if not _elts.length: + log.error("no TabBar found, it should exist !") + tab_bar_h = 0 + else: + tab_bar_h = _elts.item(0).offsetHeight + ideal_height = height - DOM.getAbsoluteTop(tab_panel_elt) - tab_bar_h - 5 + ideal_width = width - DOM.getAbsoluteLeft(tab_panel_elt) - 5 + self.setWidth("%s%s" % (ideal_width, "px")) + self.setHeight("%s%s" % (ideal_height, "px")) + + def add(self, widget, text=''): + tab = DropTab(self, text) + TabPanel.add(self, widget, tab, False) + if self.getWidgetCount() > 1: + self.tabBar.setVisible(True) + self.host.resize() + + def onWidgetPanelRemove(self, panel): + """ Called when a child WidgetsPanel is empty and need to be removed """ + self.remove(panel) + widgets_count = self.getWidgetCount() + if widgets_count == 1: + self.tabBar.setVisible(False) + self.host.resize() + self.selectTab(0) + else: + self.selectTab(widgets_count - 1)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/card_game.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,387 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools.games import TarotCard +from sat.core.i18n import _ + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.DockPanel import DockPanel +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.Image import Image +from pyjamas.ui.Label import Label +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.ui import HasAlignment +from pyjamas import Window +from pyjamas import DOM + +from dialog import ConfirmDialog, GenericDialog +from xmlui import XMLUI + + +CARD_WIDTH = 74 +CARD_HEIGHT = 136 +CARD_DELTA_Y = 30 +MIN_WIDTH = 950 # Minimum size of the panel +MIN_HEIGHT = 500 + + +class CardWidget(TarotCard, Image, MouseHandler): + """This class is used to represent a card, graphically and logically""" + + def __init__(self, parent, file_): + """@param file: path of the PNG file""" + self._parent = parent + Image.__init__(self, file_) + root_name = file_[file_.rfind("/") + 1:-4] + suit, value = root_name.split('_') + TarotCard.__init__(self, (suit, value)) + MouseHandler.__init__(self) + self.addMouseListener(self) + + def onMouseEnter(self, sender): + if self._parent.state == "ecart" or self._parent.state == "play": + DOM.setStyleAttribute(self.getElement(), "top", "0px") + + def onMouseLeave(self, sender): + if not self in self._parent.hand: + return + if not self in list(self._parent.selected): # FIXME: Workaround pyjs bug, must report it + DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y) + + def onMouseUp(self, sender, x, y): + if self._parent.state == "ecart": + if self not in list(self._parent.selected): + self._parent.addToSelection(self) + else: + self._parent.removeFromSelection(self) + elif self._parent.state == "play": + self._parent.playCard(self) + + +class CardPanel(DockPanel, ClickHandler): + + def __init__(self, parent, referee, player_nick, players): + DockPanel.__init__(self) + ClickHandler.__init__(self) + self._parent = parent + self._autoplay = None # XXX: use 0 to activate fake play, None else + self.referee = referee + self.players = players + self.player_nick = player_nick + self.bottom_nick = self.player_nick + idx = self.players.index(self.player_nick) + idx = (idx + 1) % len(self.players) + self.right_nick = self.players[idx] + idx = (idx + 1) % len(self.players) + self.top_nick = self.players[idx] + idx = (idx + 1) % len(self.players) + self.left_nick = self.players[idx] + self.bottom_nick = player_nick + self.selected = set() # Card choosed by the player (e.g. during ecart) + self.hand_size = 13 # number of cards in a hand + self.hand = [] + self.to_show = [] + self.state = None + self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT) + self.setStyleName("cardPanel") + + # Now we set up the layout + _label = Label(self.top_nick) + _label.setStyleName('cardGamePlayerNick') + self.add(_label, DockPanel.NORTH) + self.setCellWidth(_label, '100%') + self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER) + + self.hand_panel = AbsolutePanel() + self.add(self.hand_panel, DockPanel.SOUTH) + self.setCellWidth(self.hand_panel, '100%') + self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER) + + _label = Label(self.left_nick) + _label.setStyleName('cardGamePlayerNick') + self.add(_label, DockPanel.WEST) + self.setCellHeight(_label, '100%') + self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) + + _label = Label(self.right_nick) + _label.setStyleName('cardGamePlayerNick') + self.add(_label, DockPanel.EAST) + self.setCellHeight(_label, '100%') + self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT) + self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) + + self.center_panel = DockPanel() + self.inner_left = SimplePanel() + self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_left, DockPanel.WEST) + self.center_panel.setCellHeight(self.inner_left, '100%') + self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT) + self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE) + + self.inner_right = SimplePanel() + self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_right, DockPanel.EAST) + self.center_panel.setCellHeight(self.inner_right, '100%') + self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE) + + self.inner_top = SimplePanel() + self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_top, DockPanel.NORTH) + self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER) + self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM) + + self.inner_bottom = SimplePanel() + self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) + self.center_panel.add(self.inner_bottom, DockPanel.SOUTH) + self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER) + self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP) + + self.inner_center = SimplePanel() + self.center_panel.add(self.inner_center, DockPanel.CENTER) + self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER) + self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE) + + self.add(self.center_panel, DockPanel.CENTER) + self.setCellWidth(self.center_panel, '100%') + self.setCellHeight(self.center_panel, '100%') + self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE) + self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER) + + self.loadCards() + self.mouse_over_card = None # contain the card to highlight + self.visible_size = CARD_WIDTH / 2 # number of pixels visible for cards + self.addClickListener(self) + + def loadCards(self): + """Load all the cards in memory""" + def _getTarotCardsPathsCb(paths): + log.debug("_getTarotCardsPathsCb") + for file_ in paths: + log.debug("path:", file_) + card = CardWidget(self, file_) + log.debug("card:", card) + self.cards[(card.suit, card.value)] = card + self.deck.append(card) + self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) + self.cards = {} + self.deck = [] + self.cards["atout"] = {} # As Tarot is a french game, it's more handy & logical to keep french names + self.cards["pique"] = {} # spade + self.cards["coeur"] = {} # heart + self.cards["carreau"] = {} # diamond + self.cards["trefle"] = {} # club + self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb) + + def onClick(self, sender): + if self.state == "chien": + self.to_show = [] + self.state = "wait" + self.updateToShow() + elif self.state == "wait_for_ecart": + self.state = "ecart" + self.hand.extend(self.to_show) + self.hand.sort() + self.to_show = [] + self.updateToShow() + self.updateHand() + + def tarotGameNew(self, hand): + """Start a new game, with given hand""" + if hand is []: # reset the display after the scores have been showed + self.selected.clear() + del self.hand[:] + del self.to_show[:] + self.state = None + #empty hand + self.updateHand() + #nothing on the table + self.updateToShow() + for pos in ['top', 'left', 'bottom', 'right']: + getattr(self, "inner_%s" % pos).setWidget(None) + self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) + return + for suit, value in hand: + self.hand.append(self.cards[(suit, value)]) + self.hand.sort() + self.state = "init" + self.updateHand() + + def updateHand(self): + """Show the cards in the hand in the hand_panel (SOUTH panel)""" + self.hand_panel.clear() + self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10)) + x_pos = 0 + y_pos = CARD_DELTA_Y + for card in self.hand: + self.hand_panel.add(card, x_pos, y_pos) + x_pos += self.visible_size + + def updateToShow(self): + """Show cards in the center panel""" + if not self.to_show: + _widget = self.inner_center.getWidget() + if _widget: + self.inner_center.remove(_widget) + return + panel = AbsolutePanel() + panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT)) + x_pos = 0 + y_pos = 0 + for card in self.to_show: + panel.add(card, x_pos, y_pos) + x_pos += CARD_WIDTH + 5 + self.inner_center.setWidget(panel) + + def _ecartConfirm(self, confirm): + if not confirm: + return + ecart = [] + for card in self.selected: + ecart.append((card.suit, card.value)) + self.hand.remove(card) + self.selected.clear() + self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart) + self.state = "wait" + self.updateHand() + + def addToSelection(self, card): + self.selected.add(card) + if len(self.selected) == 6: + ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() + + def tarotGameInvalidCards(self, phase, played_cards, invalid_cards): + """Invalid cards have been played + @param phase: phase of the game + @param played_cards: all the cards played + @param invalid_cards: cards which are invalid""" + + if phase == "play": + self.state = "play" + elif phase == "ecart": + self.state = "ecart" + else: + log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here + + for suit, value in played_cards: + self.hand.append(self.cards[(suit, value)]) + + self.hand.sort() + self.updateHand() + if self._autoplay == None: # No dialog if there is autoplay + Window.alert('Cards played are invalid !') + self.__fakePlay() + + def removeFromSelection(self, card): + self.selected.remove(card) + if len(self.selected) == 6: + ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() + + def tarotGameChooseContrat(self, xml_data): + """Called when the player has to select his contrat + @param xml_data: SàT xml representation of the form""" + body = XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL']) + _dialog = GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() + + def tarotGameShowCards(self, game_stage, cards, data): + """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" + self.to_show = [] + for suit, value in cards: + self.to_show.append(self.cards[(suit, value)]) + self.updateToShow() + if game_stage == "chien" and data['attaquant'] == self.player_nick: + self.state = "wait_for_ecart" + else: + self.state = "chien" + + def getPlayerLocation(self, nick): + """return player location (top,bottom,left or right)""" + for location in ['top', 'left', 'bottom', 'right']: + if getattr(self, '%s_nick' % location) == nick: + return location + log.error("This line should not be reached") + + def tarotGameCardsPlayed(self, player, cards): + """A card has been played by player""" + if not len(cards): + log.warning("cards should not be empty") + return + if len(cards) > 1: + log.error("can't manage several cards played") + if self.to_show: + self.to_show = [] + self.updateToShow() + suit, value = cards[0] + player_pos = self.getPlayerLocation(player) + player_panel = getattr(self, "inner_%s" % player_pos) + + if player_panel.getWidget() != None: + #We have already cards on the table, we remove them + for pos in ['top', 'left', 'bottom', 'right']: + getattr(self, "inner_%s" % pos).setWidget(None) + + card = self.cards[(suit, value)] + DOM.setElemAttribute(card.getElement(), "style", "") + player_panel.setWidget(card) + + def tarotGameYourTurn(self): + """Called when we have to play :)""" + if self.state == "chien": + self.to_show = [] + self.updateToShow() + self.state = "play" + self.__fakePlay() + + def __fakePlay(self): + """Convenience method for stupid autoplay + /!\ don't forgot to comment any interactive dialog for invalid card""" + if self._autoplay == None: + return + if self._autoplay >= len(self.hand): + self._autoplay = 0 + card = self.hand[self._autoplay] + self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) + del self.hand[self._autoplay] + self.state = "wait" + self._autoplay += 1 + + def playCard(self, card): + self.hand.remove(card) + self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) + self.state = "wait" + self.updateHand() + + def tarotGameScore(self, xml_data, winners, loosers): + """Show score at the end of a round""" + if not winners and not loosers: + title = "Draw game" + else: + if self.player_nick in winners: + title = "You <b>win</b> !" + else: + title = "You <b>loose</b> :(" + body = XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) + _dialog = GenericDialog(title, body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/constants.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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/>. + +from libervia.common import constants + + +class Const(constants.Const): + """Add here the constants that are only used by the browser side."""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/contact.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,415 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas import Window +from pyjamas import DOM +from __pyjamas__ import doc + +from jid import JID + +from base_panels import PopupMenuPanel +from base_widget import DragLabel +from panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel +from html_tools import html_sanitize + + +def setPresenceStyle(element, presence, base_style="contact"): + """ + Set the CSS style of a contact's element according to its presence. + @param item: the UI element of the contact + @param presence: a value in ("", "chat", "away", "dnd", "xa"). + @param base_style: the base name of the style to apply + """ + if not hasattr(element, 'presence_style'): + element.presence_style = None + style = '%s-%s' % (base_style, presence or 'connected') + if style == element.presence_style: + return + if element.presence_style is not None: + element.removeStyleName(element.presence_style) + element.addStyleName(style) + element.presence_style = style + + +class GroupLabel(DragLabel, Label, ClickHandler): + def __init__(self, host, group): + self.group = group + self.host = host + Label.__init__(self, group) #, Element=DOM.createElement('div') + self.setStyleName('group') + DragLabel.__init__(self, group, "GROUP") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group) + + +class ContactLabel(DragLabel, HTML, ClickHandler): + def __init__(self, host, jid, name=None, handleClick=True): + HTML.__init__(self) + self.host = host + self.name = name or jid + self.waiting = False + self.jid = jid + self._fill() + self.setStyleName('contact') + DragLabel.__init__(self, jid, "CONTACT") + if handleClick: + ClickHandler.__init__(self) + self.addClickListener(self) + + def _fill(self): + if self.waiting: + _wait_html = "<b>(*)</b> " + self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html, + 'name': html_sanitize(self.name)}) + + def setMessageWaiting(self, waiting): + """Show a visual indicator if message are waiting + @param waiting: True if message are waiting""" + self.waiting = waiting + self._fill() + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(ChatPanel, self.jid) + + +class GroupList(VerticalPanel): + + def __init__(self, parent): + VerticalPanel.__init__(self) + self.setStyleName('groupList') + self._parent = parent + + def add(self, group): + _item = GroupLabel(self._parent.host, group) + _item.addMouseListener(self._parent) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + index = 0 + for group_ in [group.group for group in self.getChildren()]: + if group_ > group: + break + index += 1 + VerticalPanel.insert(self, _item, index) + + def remove(self, group): + for wid in self: + if isinstance(wid, GroupLabel) and wid.group == group: + VerticalPanel.remove(self, wid) + + +class GenericContactList(VerticalPanel): + """Class that can be used to represent a contact list, but not necessarily + the one that is displayed on the left side. Special features like popup menu + panel or changing the contact states must be done in a sub-class.""" + + def __init__(self, host, handleClick=False): + VerticalPanel.__init__(self) + self.host = host + self.contacts = [] + self.handleClick = handleClick + + def add(self, jid, name=None, item_cb=None): + if jid in self.contacts: + return + index = 0 + for contact_ in self.contacts: + if contact_ > jid: + break + index += 1 + self.contacts.insert(index, jid) + _item = ContactLabel(self.host, jid, name, handleClick=self.handleClick) + DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") + VerticalPanel.insert(self, _item, index) + if item_cb is not None: + item_cb(_item) + + def remove(self, jid): + wid = self.getContactLabel(jid) + if not wid: + return + VerticalPanel.remove(self, wid) + self.contacts.remove(jid) + + def isContactPresent(self, contact_jid): + """Return True if a contact is present in the panel""" + return contact_jid in self.contacts + + def getContacts(self): + return self.contacts + + def getContactLabel(self, contact_jid): + """get contactList widget of a contact + @return: ContactLabel item if present, else None""" + for wid in self: + if isinstance(wid, ContactLabel) and wid.jid == contact_jid: + return wid + return None + + +class ContactList(GenericContactList): + """The contact list that is displayed on the left side.""" + + def __init__(self, host): + GenericContactList.__init__(self, host, handleClick=True) + self.menu_entries = {"blog": {"title": "Public blog..."}} + self.context_menu = PopupMenuPanel(entries=self.menu_entries, + hide=self.contextMenuHide, + callback=self.contextMenuCallback, + vertical=False, style={"selected": "menu-selected"}) + + def contextMenuHide(self, sender, key): + """Return True if the item for that sender should be hidden.""" + # TODO: enable the blogs of users that are on another server + return JID(sender.jid).domain != self.host._defaultDomain + + def contextMenuCallback(self, sender, key): + if key == "blog": + # TODO: use the bare when all blogs can be retrieved + node = JID(sender.jid).node + web_panel = WebPanel(self.host, "/blog/%s" % node) + self.host.addTab("%s's blog" % node, web_panel) + else: + sender.onClick(sender) + + def add(self, jid, name=None): + def item_cb(item): + self.context_menu.registerRightClickSender(item) + GenericContactList.add(self, jid, name, item_cb) + + def setState(self, jid, type_, state): + """Change the appearance of the contact, according to the state + @param jid: jid which need to change state + @param type_: one of availability, messageWaiting + @param state: + - for messageWaiting type: + True if message are waiting + - for availability type: + 'unavailable' if not connected, else presence like RFC6121 #4.7.2.1""" + _item = self.getContactLabel(jid) + if _item: + if type_ == 'availability': + setPresenceStyle(_item, state) + elif type_ == 'messageWaiting': + _item.setMessageWaiting(state) + + +class ContactTitleLabel(DragLabel, Label, ClickHandler): + def __init__(self, host, text): + Label.__init__(self, text) #, Element=DOM.createElement('div') + self.host = host + self.setStyleName('contactTitle') + DragLabel.__init__(self, text, "CONTACT_TITLE") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(MicroblogPanel, None) + + +class ContactPanel(SimplePanel): + """Manage the contacts and groups""" + + def __init__(self, host): + SimplePanel.__init__(self) + + self.scroll_panel = ScrollPanel() + + self.host = host + self.groups = {} + self.connected = {} # jid connected as key and their status + + self.vPanel = VerticalPanel() + _title = ContactTitleLabel(host, 'Contacts') + DOM.setStyleAttribute(_title.getElement(), "cursor", "pointer") + + self._contact_list = ContactList(host) + self._contact_list.setStyleName('contactList') + self._groupList = GroupList(self) + self._groupList.setStyleName('groupList') + + self.vPanel.add(_title) + self.vPanel.add(self._groupList) + self.vPanel.add(self._contact_list) + self.scroll_panel.add(self.vPanel) + self.add(self.scroll_panel) + self.setStyleName('contactBox') + Window.addWindowResizeListener(self) + + def onWindowResized(self, width, height): + contact_panel_elt = self.getElement() + classname = 'widgetsPanel' if isinstance(self.getParent().getParent(), UniBoxPanel) else'gwt-TabBar' + _elts = doc().getElementsByClassName(classname) + if not _elts.length: + log.error("no element of class %s found, it should exist !" % classname) + tab_bar_h = height + else: + tab_bar_h = DOM.getAbsoluteTop(_elts.item(0)) or height # getAbsoluteTop can be 0 if tabBar is hidden + + ideal_height = tab_bar_h - DOM.getAbsoluteTop(contact_panel_elt) - 5 + self.scroll_panel.setHeight("%s%s" % (ideal_height, "px")) + + def updateContact(self, jid, attributes, groups): + """Add a contact to the panel if it doesn't exist, update it else + @param jid: jid userhost as unicode + @attributes: cf SàT Bridge API's newContact + @param groups: list of groups""" + _current_groups = self.getContactGroups(jid) + _new_groups = set(groups) + _key = "@%s: " + + for group in _current_groups.difference(_new_groups): + # We remove the contact from the groups where he isn't anymore + self.groups[group].remove(jid) + if not self.groups[group]: + # The group is now empty, we must remove it + del self.groups[group] + self._groupList.remove(group) + if self.host.uni_box: + self.host.uni_box.removeKey(_key % group) + + for group in _new_groups.difference(_current_groups): + # We add the contact to the groups he joined + if not group in self.groups.keys(): + self.groups[group] = set() + self._groupList.add(group) + if self.host.uni_box: + self.host.uni_box.addKey(_key % group) + self.groups[group].add(jid) + + # We add the contact to contact list, it will check if contact already exists + self._contact_list.add(jid) + + def removeContact(self, jid): + """Remove contacts from groups where he is and contact list""" + self.updateContact(jid, {}, []) # we remove contact from every group + self._contact_list.remove(jid) + + def setConnected(self, jid, resource, availability, priority, statuses): + """Set connection status + @param jid: JID userhost as unicode + """ + if availability == 'unavailable': + if jid in self.connected: + if resource in self.connected[jid]: + del self.connected[jid][resource] + if not self.connected[jid]: + del self.connected[jid] + else: + if not jid in self.connected: + self.connected[jid] = {} + self.connected[jid][resource] = (availability, priority, statuses) + + # check if the contact is connected with another resource, use the one with highest priority + if jid in self.connected: + max_resource = max_priority = None + for tmp_resource in self.connected[jid]: + if max_priority is None or self.connected[jid][tmp_resource][1] >= max_priority: + max_resource = tmp_resource + max_priority = self.connected[jid][tmp_resource][1] + if availability == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one + priority = max_priority + availability = self.connected[jid][max_resource][0] + if jid not in self.connected or priority >= max_priority: + # case 1: jid not in self.connected means all resources are disconnected, update with 'unavailable' + # case 2: update (or confirm) with the values of the resource which takes precedence + self._contact_list.setState(jid, "availability", availability) + + # update the connected contacts chooser live + if hasattr(self.host, "room_contacts_chooser") and self.host.room_contacts_chooser is not None: + self.host.room_contacts_chooser.resetContacts() + + def setContactMessageWaiting(self, jid, waiting): + """Show an visual indicator that contact has send a message + @param jid: jid of the contact + @param waiting: True if message are waiting""" + self._contact_list.setState(jid, "messageWaiting", waiting) + + def getConnected(self, filter_muc=False): + """return a list of all jid (bare jid) connected + @param filter_muc: if True, remove the groups from the list + """ + contacts = self.connected.keys() + contacts.sort() + return contacts if not filter_muc else list(set(contacts).intersection(set(self.getContacts()))) + + def getContactGroups(self, contact_jid): + """Get groups where contact is + @param group: string of single group, or list of string + @param contact_jid: jid to test + """ + result = set() + for group in self.groups: + if self.isContactInGroup(group, contact_jid): + result.add(group) + return result + + def isContactInGroup(self, group, contact_jid): + """Test if the contact_jid is in the group + @param group: string of single group, or list of string + @param contact_jid: jid to test + @return: True if contact_jid is in on of the groups""" + if group in self.groups and contact_jid in self.groups[group]: + return True + return False + + def isContactInRoster(self, contact_jid): + """Test if the contact is in our roster list""" + for _contact_label in self._contact_list: + if contact_jid == _contact_label.jid: + return True + return False + + def getContacts(self): + return self._contact_list.getContacts() + + def getGroups(self): + return self.groups.keys() + + def onMouseMove(self, sender, x, y): + pass + + def onMouseDown(self, sender, x, y): + pass + + def onMouseUp(self, sender, x, y): + pass + + def onMouseEnter(self, sender): + if isinstance(sender, GroupLabel): + for contact in self._contact_list: + if contact.jid in self.groups[sender.group]: + contact.addStyleName("selected") + + def onMouseLeave(self, sender): + if isinstance(sender, GroupLabel): + for contact in self._contact_list: + if contact.jid in self.groups[sender.group]: + contact.removeStyleName("selected") +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/contact_group.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. + +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.DockPanel import DockPanel +from pyjamas.Timer import Timer +from pyjamas.ui.Button import Button +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui import HasAlignment + +from dialog import ConfirmDialog, InfoDialog +from list_manager import ListManager +import dialog +import contact + + +class ContactGroupManager(ListManager): + """A manager for sub-panels to assign contacts to each group.""" + + def __init__(self, parent, keys_dict, contact_list, offsets, style): + ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style) + self.registerPopupMenuPanel(entries={"Remove group": {}}, + callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key))) + + def removeContactKey(self, sender, key): + key = sender.getText() + + def confirm_cb(answer): + if answer: + ListManager.removeContactKey(self, key) + self._parent.removeKeyFromAddGroupPanel(key) + + _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key) + _dialog.show() + + def removeFromRemainingList(self, contacts): + ListManager.removeFromRemainingList(self, contacts) + self._parent.updateContactList(contacts=contacts) + + def addToRemainingList(self, contacts, ignore_key=None): + ListManager.addToRemainingList(self, contacts, ignore_key) + self._parent.updateContactList(contacts=contacts) + + +class ContactGroupEditor(DockPanel): + """Panel for the contact groups manager.""" + + def __init__(self, host, parent=None, onCloseCallback=None): + DockPanel.__init__(self) + self.host = host + + # eventually display in a popup + if parent is None: + parent = DialogBox(autoHide=False, centered=True) + parent.setHTML("Manage contact groups") + self._parent = parent + self._on_close_callback = onCloseCallback + self.all_contacts = self.host.contact_panel.getContacts() + + groups_list = self.host.contact_panel.groups.keys() + groups_list.sort() + + self.add_group_panel = self.getAddGroupPanel(groups_list) + south_panel = self.getCloseSaveButtons() + center_panel = self.getContactGroupManager(groups_list) + east_panel = self.getContactList() + + self.add(self.add_group_panel, DockPanel.CENTER) + self.add(east_panel, DockPanel.EAST) + self.add(center_panel, DockPanel.NORTH) + self.add(south_panel, DockPanel.SOUTH) + + self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT) + self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP) + self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT) + self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP) + self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM) + self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT) + self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM) + self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) + + # need to be done after the contact list has been initialized + self.groups.setContacts(self.host.contact_panel.groups) + self.toggleContacts(showAll=True) + + # Hide the contacts list from the main panel to not confuse the user + self.restore_contact_panel = False + if self.host.contact_panel.getVisible(): + self.restore_contact_panel = True + self.host.panel._contactsSwitch() + + parent.add(self) + parent.setVisible(True) + if isinstance(parent, DialogBox): + parent.center() + + def getContactGroupManager(self, groups_list): + """Set the list manager for the groups""" + flex_table = FlexTable(len(groups_list), 2) + flex_table.addStyleName('contactGroupEditor') + # overwrite the default style which has been set for rich text editor + style = { + "keyItem": "group", + "popupMenuItem": "popupMenuItem", + "removeButton": "contactGroupRemoveButton", + "buttonCell": "contactGroupButtonCell", + "keyPanel": "contactGroupPanel" + } + self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style) + self.groups.createWidgets() # widgets are automatically added to FlexTable + # FIXME: clean that part which is dangerous + flex_table.updateContactList = self.updateContactList + flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove + return flex_table + + def getAddGroupPanel(self, groups_list): + """Add the 'Add group' panel to the FlexTable""" + + def add_group_cb(text): + self.groups.addContactKey(text) + self.add_group_panel.textbox.setFocus(True) + + add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb) + add_group_panel.addStyleName("addContactGroupPanel") + return add_group_panel + + def getCloseSaveButtons(self): + """Add the buttons to close the dialog / save the groups""" + buttons = HorizontalPanel() + buttons.addStyleName("marginAuto") + buttons.add(Button("Save", listener=self.closeAndSave)) + buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) + return buttons + + def getContactList(self): + """Add the contact list to the DockPanel""" + self.toggle = Button("", self.toggleContacts) + self.toggle.addStyleName("toggleAssignedContacts") + self.contacts = contact.GenericContactList(self.host) + for contact_ in self.all_contacts: + self.contacts.add(contact_) + contact_panel = VerticalPanel() + contact_panel.add(self.toggle) + contact_panel.add(self.contacts) + return contact_panel + + def toggleContacts(self, sender=None, showAll=None): + """Callback for the toggle button""" + if sender is None: + sender = self.toggle + sender.showAll = showAll if showAll is not None else not sender.showAll + if sender.showAll: + sender.setText("Hide assigned") + else: + sender.setText("Show assigned") + self.updateContactList(sender) + + def updateContactList(self, sender=None, contacts=None): + """Update the contact list regarding the toggle button""" + if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"): + return + sender = self.toggle + if contacts is not None: + if not isinstance(contacts, list): + contacts = [contacts] + for contact_ in contacts: + if contact_ not in self.all_contacts: + contacts.remove(contact_) + else: + contacts = self.all_contacts + for contact_ in contacts: + if sender.showAll: + self.contacts.getContactLabel(contact_).setVisible(True) + else: + if contact_ in self.groups.remaining_list: + self.contacts.getContactLabel(contact_).setVisible(True) + else: + self.contacts.getContactLabel(contact_).setVisible(False) + + def __close(self): + """Remove the widget from parent or close the popup.""" + if isinstance(self._parent, DialogBox): + self._parent.hide() + self._parent.remove(self) + if self._on_close_callback is not None: + self._on_close_callback() + if self.restore_contact_panel: + self.host.panel._contactsSwitch() + + def cancelWithoutSaving(self): + """Ask for confirmation before closing the dialog.""" + def confirm_cb(answer): + if answer: + self.__close() + + _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?") + _dialog.show() + + def closeAndSave(self): + """Call bridge methods to save the changes and close the dialog""" + map_ = {} + for contact_ in self.all_contacts: + map_[contact_] = set() + contacts = self.groups.getContacts() + for group in contacts.keys(): + for contact_ in contacts[group]: + try: + map_[contact_].add(group) + except KeyError: + InfoDialog("Invalid contact", + "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) + + "Your changes could not be saved: please check your assignments and save again.", Width="400px").center() + return + for contact_ in map_.keys(): + groups = map_[contact_] + current_groups = self.host.contact_panel.getContactGroups(contact_) + if groups != current_groups: + self.host.bridge.call('updateContact', None, contact_, '', list(groups)) + self.__close()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/dialog.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,547 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools.misc import DEFAULT_MUC + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.Grid import Grid +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.Button import Button +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.RadioButton import RadioButton +from pyjamas.ui import HasAlignment +from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER +from pyjamas.ui.MouseListener import MouseWheelHandler +from pyjamas import Window + +from base_panels import ToggleStackPanel + + +# List here the patterns that are not allowed in contact group names +FORBIDDEN_PATTERNS_IN_GROUP = () + + +class RoomChooser(Grid): + """Select a room from the rooms you already joined, or create a new one""" + + GENERATE_MUC = "<use random name>" + + def __init__(self, host, default_room=DEFAULT_MUC): + Grid.__init__(self, 2, 2, Width='100%') + self.host = host + + self.new_radio = RadioButton("room", "Discussion room:") + self.new_radio.setChecked(True) + self.box = TextBox(Width='95%') + self.box.setText(self.GENERATE_MUC if default_room == "" else default_room) + self.exist_radio = RadioButton("room", "Already joined:") + self.rooms_list = ListBox(Width='95%') + + self.add(self.new_radio, 0, 0) + self.add(self.box, 0, 1) + self.add(self.exist_radio, 1, 0) + self.add(self.rooms_list, 1, 1) + + self.box.addFocusListener(self) + self.rooms_list.addFocusListener(self) + + self.exist_radio.setVisible(False) + self.rooms_list.setVisible(False) + self.setRooms() + + def onFocus(self, sender): + if sender == self.rooms_list: + self.exist_radio.setChecked(True) + elif sender == self.box: + if self.box.getText() == self.GENERATE_MUC: + self.box.setText("") + self.new_radio.setChecked(True) + + def onLostFocus(self, sender): + if sender == self.box: + if self.box.getText() == "": + self.box.setText(self.GENERATE_MUC) + + def setRooms(self): + for room in self.host.room_list: + self.rooms_list.addItem(room.bare) + if len(self.host.room_list) > 0: + self.exist_radio.setVisible(True) + self.rooms_list.setVisible(True) + self.exist_radio.setChecked(True) + + def getRoom(self): + if self.exist_radio.getChecked(): + values = self.rooms_list.getSelectedValues() + return "" if values == [] else values[0] + value = self.box.getText() + return "" if value == self.GENERATE_MUC else value + + +class ContactsChooser(VerticalPanel): + """Select one or several connected contacts""" + + def __init__(self, host, nb_contact=None, ok_button=None): + """ + @param host: SatWebFrontend instance + @param nb_contact: number of contacts that have to be selected, None for no limit + If a tuple is given instead of an integer, nb_contact[0] is the minimal and + nb_contact[1] is the maximal number of contacts to be chosen. + """ + self.host = host + if isinstance(nb_contact, tuple): + if len(nb_contact) == 0: + nb_contact = None + elif len(nb_contact) == 1: + nb_contact = (nb_contact[0], nb_contact[0]) + elif nb_contact is not None: + nb_contact = (nb_contact, nb_contact) + if nb_contact is None: + log.warning("Need to select as many contacts as you want") + else: + log.warning("Need to select between %d and %d contacts" % nb_contact) + self.nb_contact = nb_contact + self.ok_button = ok_button + VerticalPanel.__init__(self, Width='100%') + self.contacts_list = ListBox() + self.contacts_list.setMultipleSelect(True) + self.contacts_list.setWidth("95%") + self.contacts_list.addStyleName('contactsChooser') + self.contacts_list.addChangeListener(self.onChange) + self.add(self.contacts_list) + self.setContacts() + self.onChange() + + def onChange(self, sender=None): + if self.ok_button is None: + return + if self.nb_contact: + selected = len(self.contacts_list.getSelectedValues(True)) + if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: + self.ok_button.setEnabled(True) + else: + self.ok_button.setEnabled(False) + + def setContacts(self, selected=[]): + """Fill the list with the connected contacts + @param select: list of the contacts to select by default + """ + self.contacts_list.clear() + contacts = self.host.contact_panel.getConnected(filter_muc=True) + self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) + self.contacts_list.addItem("") + for contact in contacts: + if contact not in [room.bare for room in self.host.room_list]: + self.contacts_list.addItem(contact) + self.contacts_list.setItemTextSelection(selected) + + def getContacts(self): + return self.contacts_list.getSelectedValues(True) + + +class RoomAndContactsChooser(DialogBox): + """Select a room and some users to invite in""" + + def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", + title_room="Join room", title_invite="Invite contacts", visible=(True, True)): + DialogBox.__init__(self, centered=True) + self.host = host + self.callback = callback + self.title_room = title_room + self.title_invite = title_invite + + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + ok_button = Button("OK", self.onOK) + button_panel.add(ok_button) + button_panel.add(Button("Cancel", self.onCancel)) + + self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) + self.contact_panel = ContactsChooser(host, nb_contact, ok_button) + + self.stack_panel = ToggleStackPanel(Width="100%") + self.stack_panel.add(self.room_panel, visible=visible[0]) + self.stack_panel.add(self.contact_panel, visible=visible[1]) + self.stack_panel.addStackChangeListener(self) + self.onStackChanged(self.stack_panel, 0, visible[0]) + self.onStackChanged(self.stack_panel, 1, visible[1]) + + main_panel = VerticalPanel() + main_panel.setStyleName("room-contact-chooser") + main_panel.add(self.stack_panel) + main_panel.add(button_panel) + + self.setWidget(main_panel) + self.setHTML(title) + self.show() + + # needed to update the contacts list when someone logged in/out + self.host.room_contacts_chooser = self + + def getRoom(self, asSuffix=False): + room = self.room_panel.getRoom() + if asSuffix: + return room if room == "" else ": %s" % room + else: + return room + + def getContacts(self, asSuffix=False): + contacts = self.contact_panel.getContacts() + if asSuffix: + return "" if contacts == [] else ": %s" % ", ".join(contacts) + else: + return contacts + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + if index == 0: + sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True))) + elif index == 1: + sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True))) + + def resetContacts(self): + """Called when someone log in/out to update the list""" + self.contact_panel.setContacts(self.getContacts()) + + def onOK(self, sender): + room_jid = self.getRoom() + if room_jid != "" and "@" not in room_jid: + Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain) + return + self.hide() + self.callback(room_jid, self.getContacts()) + + def onCancel(self, sender): + self.hide() + + def hide(self): + self.host.room_contacts_chooser = None + DialogBox.hide(self, autoClosed=True) + + +class GenericConfirmDialog(DialogBox): + + def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs): + """ + Dialog to confirm an action + @param widgets: widgets to attach + @param callback: method to call when a button is clicked + @param title: title of the dialog + @param prompt: textbox from which to retrieve the string value to be passed to the callback when + OK button is pressed. If None, OK button will return "True". Cancel button always returns "False". + """ + self.callback = callback + DialogBox.__init__(self, centered=True, **kwargs) + + content = VerticalPanel() + content.setWidth('100%') + for wid in widgets: + content.add(wid) + if wid == prompt: + wid.setWidth('100%') + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + self.confirm_button = Button("OK", self.onConfirm) + button_panel.add(self.confirm_button) + self.cancel_button = Button("Cancel", self.onCancel) + button_panel.add(self.cancel_button) + content.add(button_panel) + self.setHTML(title) + self.setWidget(content) + self.prompt = prompt + + def onConfirm(self, sender): + self.hide() + self.callback(self.prompt.getText() if self.prompt else True) + + def onCancel(self, sender): + self.hide() + self.callback(False) + + def show(self): + DialogBox.show(self) + if self.prompt: + self.prompt.setFocus(True) + + +class ConfirmDialog(GenericConfirmDialog): + + def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): + GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) + + +class GenericDialog(DialogBox): + """Dialog which just show a widget and a close button""" + + def __init__(self, title, main_widget, callback=None, options=None, **kwargs): + """Simple notice dialog box + @param title: HTML put in the header + @param main_widget: widget put in the body + @param callback: method to call on closing + @param options: one or more of the following options: + - NO_CLOSE: don't add a close button""" + DialogBox.__init__(self, centered=True, **kwargs) + self.callback = callback + if not options: + options = [] + _body = VerticalPanel() + _body.setSize('100%','100%') + _body.setSpacing(4) + _body.add(main_widget) + _body.setCellWidth(main_widget, '100%') + _body.setCellHeight(main_widget, '100%') + if not 'NO_CLOSE' in options: + _close_button = Button("Close", self.onClose) + _body.add(_close_button) + _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) + self.setHTML(title) + self.setWidget(_body) + self.panel.setSize('100%', '100%') #Need this hack to have correct size in Gecko & Webkit + + def close(self): + """Same effect as clicking the close button""" + self.onClose(None) + + def onClose(self, sender): + self.hide() + if self.callback: + self.callback() + + +class InfoDialog(GenericDialog): + + def __init__(self, title, body, callback=None, options=None, **kwargs): + GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) + + +class PromptDialog(GenericConfirmDialog): + + def __init__(self, callback, text='', title='User input', **kwargs): + prompt = TextBox() + prompt.setText(text) + GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs) + + +class PopupPanelWrapper(PopupPanel): + """This wrapper catch Escape event to avoid request cancellation by Firefox""" + + def onEventPreview(self, event): + if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: + #needed to prevent request cancellation in Firefox + event.preventDefault() + return PopupPanel.onEventPreview(self, event) + + +class ExtTextBox(TextBox): + """Extended TextBox""" + + def __init__(self, *args, **kwargs): + if 'enter_cb' in kwargs: + self.enter_cb = kwargs['enter_cb'] + del kwargs['enter_cb'] + TextBox.__init__(self, *args, **kwargs) + self.addKeyboardListener(self) + + def onKeyUp(self, sender, keycode, modifiers): + pass + + def onKeyDown(self, sender, keycode, modifiers): + pass + + def onKeyPress(self, sender, keycode, modifiers): + if self.enter_cb and keycode == KEY_ENTER: + self.enter_cb(self) + + +class GroupSelector(DialogBox): + + def __init__(self, top_widgets, initial_groups, selected_groups, + ok_title="OK", ok_cb=None, cancel_cb=None): + DialogBox.__init__(self, centered=True) + main_panel = VerticalPanel() + self.ok_cb = ok_cb + self.cancel_cb = cancel_cb + + for wid in top_widgets: + main_panel.add(wid) + + main_panel.add(Label('Select in which groups your contact is:')) + self.list_box = ListBox() + self.list_box.setMultipleSelect(True) + self.list_box.setVisibleItemCount(5) + self.setAvailableGroups(initial_groups) + self.setGroupsSelected(selected_groups) + main_panel.add(self.list_box) + + def cb(text): + self.list_box.addItem(text) + self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") + + main_panel.add(AddGroupPanel(initial_groups, cb)) + + button_panel = HorizontalPanel() + button_panel.addStyleName("marginAuto") + button_panel.add(Button(ok_title, self.onOK)) + button_panel.add(Button("Cancel", self.onCancel)) + main_panel.add(button_panel) + + self.setWidget(main_panel) + + def getSelectedGroups(self): + """Return a list of selected groups""" + return self.list_box.getSelectedValues() + + def setAvailableGroups(self, groups): + _groups = list(set(groups)) + _groups.sort() + self.list_box.clear() + for group in _groups: + self.list_box.addItem(group) + + def setGroupsSelected(self, selected_groups): + self.list_box.setItemTextSelection(selected_groups) + + def onOK(self, sender): + self.hide() + if self.ok_cb: + self.ok_cb(self) + + def onCancel(self, sender): + self.hide() + if self.cancel_cb: + self.cancel_cb(self) + + +class AddGroupPanel(HorizontalPanel): + def __init__(self, groups, cb=None): + """ + @param groups: list of the already existing groups + """ + HorizontalPanel.__init__(self) + self.groups = groups + self.add(Label('Add group:')) + self.textbox = ExtTextBox(enter_cb=self.onGroupInput) + self.add(self.textbox) + self.add(Button("add", lambda sender: self.onGroupInput(self.textbox))) + self.cb = cb + + def onGroupInput(self, sender): + text = sender.getText() + if text == "": + return + for group in self.groups: + if text == group: + Window.alert("The group '%s' already exists." % text) + return + for pattern in FORBIDDEN_PATTERNS_IN_GROUP: + if pattern in text: + Window.alert("The pattern '%s' is not allowed in group names." % pattern) + return + sender.setText('') + self.groups.append(text) + if self.cb is not None: + self.cb(text) + + +class WheelTextBox(TextBox, MouseWheelHandler): + + def __init__(self, *args, **kwargs): + TextBox.__init__(self, *args, **kwargs) + MouseWheelHandler.__init__(self) + + +class IntSetter(HorizontalPanel): + """This class show a bar with button to set an int value""" + + def __init__(self, label, value=0, value_max=None, visible_len=3): + """initialize the intSetter + @param label: text shown in front of the setter + @param value: initial value + @param value_max: limit value, None or 0 for unlimited""" + HorizontalPanel.__init__(self) + self.value = value + self.value_max = value_max + _label = Label(label) + self.add(_label) + self.setCellWidth(_label, "100%") + minus_button = Button("-", self.onMinus) + self.box = WheelTextBox() + self.box.setVisibleLength(visible_len) + self.box.setText(str(value)) + self.box.addInputListener(self) + self.box.addMouseWheelListener(self) + plus_button = Button("+", self.onPlus) + self.add(minus_button) + self.add(self.box) + self.add(plus_button) + self.valueChangedListener = [] + + def addValueChangeListener(self, listener): + self.valueChangedListener.append(listener) + + def removeValueChangeListener(self, listener): + if listener in self.valueChangedListener: + self.valueChangedListener.remove(listener) + + def _callListeners(self): + for listener in self.valueChangedListener: + listener(self.value) + + def setValue(self, value): + """Change the value and fire valueChange listeners""" + self.value = value + self.box.setText(str(value)) + self._callListeners() + + def onMinus(self, sender, step=1): + self.value = max(0, self.value - step) + self.box.setText(str(self.value)) + self._callListeners() + + def onPlus(self, sender, step=1): + self.value += step + if self.value_max: + self.value = min(self.value, self.value_max) + self.box.setText(str(self.value)) + self._callListeners() + + def onInput(self, sender): + """Accept only valid integer && normalize print (no leading 0)""" + try: + self.value = int(self.box.getText()) if self.box.getText() else 0 + except ValueError: + pass + if self.value_max: + self.value = min(self.value, self.value_max) + self.box.setText(str(self.value)) + self._callListeners() + + def onMouseWheel(self, sender, velocity): + if velocity > 0: + self.onMinus(sender, 10) + else: + self.onPlus(sender, 10)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/file_tools.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,148 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from pyjamas.ui.FileUpload import FileUpload +from pyjamas.ui.FormPanel import FormPanel +from pyjamas import Window +from pyjamas import DOM +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HTML import HTML +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.Label import Label + + +class FilterFileUpload(FileUpload): + + def __init__(self, name, max_size, types=None): + """ + @param name: the input element name and id + @param max_size: maximum file size in MB + @param types: allowed types as a list of couples (x, y, z): + - x: MIME content type e.g. "audio/ogg" + - y: file extension e.g. "*.ogg" + - z: description for the user e.g. "Ogg Vorbis Audio" + If types is None, all file format are accepted + """ + FileUpload.__init__(self) + self.setName(name) + while DOM.getElementById(name): + name = "%s_" % name + self.setID(name) + self._id = name + self.max_size = max_size + self.types = types + + def getFileInfo(self): + from __pyjamas__ import JS + JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]") + + def check(self): + if self.getFilename() == "": + return False + (size, filetype) = self.getFileInfo() + if self.types and filetype not in [x for (x, y, z) in self.types]: + types = [] + for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]: + if type_ not in types: + types.append(type_) + Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types)) + return False + if size > self.max_size * pow(2, 20): + Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size) + return False + return True + + +class FileUploadPanel(FormPanel): + + def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None): + """Build a form panel to upload a file. + @param action_url: the form action URL + @param input_id: the input element name and id + @param max_size: maximum file size in MB + @param texts: a dict to ovewrite the default textual values + @param close_cb: the close button callback method + """ + FormPanel.__init__(self) + self.texts = {'ok_button': 'Upload file', + 'cancel_button': 'Cancel', + 'body': 'Please select a file.', + 'submitting': '<strong>Submitting, please wait...</strong>', + 'errback': "Your file has been rejected...", + 'body_errback': 'Please select another file.', + 'callback': "Your file has been accepted!"} + if isinstance(texts, dict): + self.texts.update(texts) + self.close_cb = close_cb + self.setEncoding(FormPanel.ENCODING_MULTIPART) + self.setMethod(FormPanel.METHOD_POST) + self.setAction(action_url) + self.vPanel = VerticalPanel() + self.message = HTML(self.texts['body']) + self.vPanel.add(self.message) + + hPanel = HorizontalPanel() + hPanel.setSpacing(5) + hPanel.setStyleName('marginAuto') + self.file_upload = FilterFileUpload(input_id, max_size) + self.vPanel.add(self.file_upload) + + self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick")) + hPanel.add(self.upload_btn) + hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick"))) + + self.status = Label() + hPanel.add(self.status) + + self.vPanel.add(hPanel) + + self.add(self.vPanel) + self.addFormHandler(self) + + def setCloseCb(self, close_cb): + self.close_cb = close_cb + + def onCloseBtnClick(self): + if self.close_cb: + self.close_cb() + else: + log.warning("no close method defined") + + def onSubmitBtnClick(self): + if not self.file_upload.check(): + return + self.message.setHTML(self.texts['submitting']) + self.upload_btn.setEnabled(False) + self.submit() + + def onSubmit(self, event): + pass + + def onSubmitComplete(self, event): + result = event.getResults() + if result != "OK": + Window.alert(self.texts['errback']) + self.message.setHTML(self.texts['body_errback']) + self.upload_btn.setEnabled(True) + else: + Window.alert(self.texts['callback']) + self.close_cb()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/html_tools.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +from sat_frontends.tools import xmltools + +from nativedom import NativeDOM +import re + +dom = NativeDOM() + + +def html_sanitize(html): + """Naive sanitization of HTML""" + return html.replace('<', '<').replace('>', '>') + + +def html_strip(html): + """Strip leading/trailing white spaces, HTML line breaks and sequences.""" + cleaned = re.sub(r"^(<br/?>| |\s)+", "", html) + cleaned = re.sub(r"(<br/?>| |\s)+$", "", cleaned) + return cleaned + + +def inlineRoot(xhtml): + """ make root element inline """ + doc = dom.parseString(xhtml) + return xmltools.inlineRoot(doc) + + +def convertNewLinesToXHTML(text): + return text.replace('\n', '<br/>')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/jid.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + + +class JID(object): + """This class help manage JID (Node@Domaine/Resource)""" + + def __init__(self, jid): + self.__raw = str(jid) + self.__parse() + + def __parse(self): + """find node domaine and resource""" + node_end = self.__raw.find('@') + if node_end < 0: + node_end = 0 + domain_end = self.__raw.find('/') + if domain_end < 1: + domain_end = len(self.__raw) + self.node = self.__raw[:node_end] + self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end] + self.resource = self.__raw[domain_end + 1:] + if not node_end: + self.bare = self.__raw + else: + self.bare = self.node + '@' + self.domain + + def __str__(self): + return self.__raw.__str__() + + def is_valid(self): + """return True if the jid is xmpp compliant""" + #FIXME: always return True for the moment + return True + + def __eq__(self, other): + """Redefine equality operator to implement the naturally expected test""" + return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/libervia_main.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,903 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 + +### logging configuration ### +import logging +logging.configure() +from sat.core.log import getLogger +log = getLogger(__name__) +### + +from sat_frontends.tools.misc import InputHistory +from sat_frontends.tools.strings import getURLParams +from sat.core.i18n import _ + +from pyjamas.ui.RootPanel import RootPanel +from pyjamas.ui.HTML import HTML +from pyjamas.ui.KeyboardListener import KEY_ESCAPE +from pyjamas.Timer import Timer +from pyjamas import Window, DOM +from pyjamas.JSONService import JSONProxy + +from register import RegisterBox +from contact import ContactPanel +from base_widget import WidgetsPanel +from panels import MicroblogItem +import panels, dialog +from jid import JID +from xmlui import XMLUI +from html_tools import html_sanitize +from notification import Notification + +from constants import Const as C + + +MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories + +# Set to true to not create a new LiberviaWidget when a similar one +# already exist (i.e. a chat panel with the same target). Instead +# the existing widget will be eventually removed from its parent +# and added to new WidgetsPanel, or replaced to the expected +# position if the previous and the new parent are the same. +REUSE_EXISTING_LIBERVIA_WIDGETS = True + + +class LiberviaJsonProxy(JSONProxy): + def __init__(self, *args, **kwargs): + JSONProxy.__init__(self, *args, **kwargs) + self.handler = self + self.cb = {} + self.eb = {} + + def call(self, method, cb, *args): + _id = self.callMethod(method, args) + if cb: + if isinstance(cb, tuple): + if len(cb) != 2: + log.error("tuple syntax for bridge.call is (callback, errback), aborting") + return + if cb[0] is not None: + self.cb[_id] = cb[0] + self.eb[_id] = cb[1] + else: + self.cb[_id] = cb + + def onRemoteResponse(self, response, request_info): + if request_info.id in self.cb: + _cb = self.cb[request_info.id] + # if isinstance(_cb, tuple): + # #we have arguments attached to the callback + # #we send them after the answer + # callback, args = _cb + # callback(response, *args) + # else: + # #No additional argument, we call directly the callback + _cb(response) + del self.cb[request_info.id] + if request_info.id in self.eb: + del self.eb[request_info.id] + + def onRemoteError(self, code, errobj, request_info): + """def dump(obj): + print "\n\nDUMPING %s\n\n" % obj + for i in dir(obj): + print "%s: %s" % (i, getattr(obj,i))""" + if request_info.id in self.eb: + _eb = self.eb[request_info.id] + _eb((code, errobj)) + del self.cb[request_info.id] + del self.eb[request_info.id] + else: + if code != 0: + log.error("Internal server error") + """for o in code, error, request_info: + dump(o)""" + else: + if isinstance(errobj['message'], dict): + log.error("Error %s: %s" % (errobj['message']['faultCode'], errobj['message']['faultString'])) + else: + log.error("%s" % errobj['message']) + + +class RegisterCall(LiberviaJsonProxy): + def __init__(self): + LiberviaJsonProxy.__init__(self, "/register_api", + ["isRegistered", "isConnected", "connect", "registerParams", "getMenus"]) + + +class BridgeCall(LiberviaJsonProxy): + def __init__(self): + LiberviaJsonProxy.__init__(self, "/json_api", + ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", + "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid", + "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", + "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", + "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", + "getWaitingSub", "subscription", "delContact", "updateContact", "getCard", + "getEntityData", "getParamsUI", "asyncGetParamA", "setParam", "launchAction", + "disconnect", "chatStateComposing", "getNewAccountDomain", "confirmationAnswer", + "syntaxConvert", "getAccountDialogUI", + ]) + + +class BridgeSignals(LiberviaJsonProxy): + RETRY_BASE_DELAY = 1000 + + def __init__(self, host): + self.host = host + self.retry_delay = self.RETRY_BASE_DELAY + LiberviaJsonProxy.__init__(self, "/json_signal_api", + ["getSignals"]) + + def onRemoteResponse(self, response, request_info): + self.retry_delay = self.RETRY_BASE_DELAY + LiberviaJsonProxy.onRemoteResponse(self, response, request_info) + + def onRemoteError(self, code, errobj, request_info): + if errobj['message'] == 'Empty Response': + Window.getLocation().reload() # XXX: reset page in case of session ended. + # FIXME: Should be done more properly without hard reload + LiberviaJsonProxy.onRemoteError(self, code, errobj, request_info) + #we now try to reconnect + if isinstance(errobj['message'], dict) and errobj['message']['faultCode'] == 0: + Window.alert('You are not allowed to connect to server') + else: + def _timerCb(timer): + self.host.bridge_signals.call('getSignals', self.host._getSignalsCB) + Timer(notify=_timerCb).schedule(self.retry_delay) + self.retry_delay *= 2 + + +class SatWebFrontend(InputHistory): + def onModuleLoad(self): + log.info("============ onModuleLoad ==============") + panels.ChatPanel.registerClass() + panels.MicroblogPanel.registerClass() + self.whoami = None + self._selected_listeners = set() + self.bridge = BridgeCall() + self.bridge_signals = BridgeSignals(self) + self.uni_box = None + self.status_panel = HTML('<br />') + self.contact_panel = ContactPanel(self) + self.panel = panels.MainPanel(self) + self.discuss_panel = self.panel.discuss_panel + self.tab_panel = self.panel.tab_panel + self.tab_panel.addTabListener(self) + self.libervia_widgets = set() # keep track of all actives LiberviaWidgets + self.room_list = [] # list of rooms + self.mblog_cache = [] # used to keep our own blog entries in memory, to show them in new mblog panel + self.avatars_cache = {} # keep track of jid's avatar hash (key=jid, value=file) + self._register_box = None + RootPanel().add(self.panel) + self.notification = Notification() + DOM.addEventPreview(self) + self._register = RegisterCall() + self._register.call('getMenus', self.panel.menu.createMenus) + self._register.call('registerParams', None) + self._register.call('isRegistered', self._isRegisteredCB) + self.initialised = False + self.init_cache = [] # used to cache events until initialisation is done + # define here the parameters that have an incidende to UI refresh + self.params_ui = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM, + "category": C.ENABLE_UNIBOX_KEY, + "cast": lambda value: value == 'true', + "value": None + } + } + + def addSelectedListener(self, callback): + self._selected_listeners.add(callback) + + def getSelected(self): + wid = self.tab_panel.getCurrentPanel() + if not isinstance(wid, WidgetsPanel): + log.error("Tab widget is not a WidgetsPanel, can't get selected widget") + return None + return wid.selected + + def setSelected(self, widget): + """Define the selected widget""" + widgets_panel = self.tab_panel.getCurrentPanel() + if not isinstance(widgets_panel, WidgetsPanel): + return + + selected = widgets_panel.selected + + if selected == widget: + return + + if selected: + selected.removeStyleName('selected_widget') + + widgets_panel.selected = widget + + if widget: + widgets_panel.selected.addStyleName('selected_widget') + + for callback in self._selected_listeners: + callback(widget) + + def resize(self): + """Resize elements""" + Window.onResize() + + def onBeforeTabSelected(self, sender, tab_index): + return True + + def onTabSelected(self, sender, tab_index): + selected = self.getSelected() + for callback in self._selected_listeners: + callback(selected) + + def onEventPreview(self, event): + if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: + #needed to prevent request cancellation in Firefox + event.preventDefault() + return True + + def getAvatar(self, jid_str): + """Return avatar of a jid if in cache, else ask for it""" + def dataReceived(result): + if 'avatar' in result: + self._entityDataUpdatedCb(jid_str, 'avatar', result['avatar']) + else: + self.bridge.call("getCard", None, jid_str) + + def avatarError(error_data): + # The jid is maybe not in our roster, we ask for the VCard + self.bridge.call("getCard", None, jid_str) + + if jid_str not in self.avatars_cache: + self.bridge.call('getEntityData', (dataReceived, avatarError), jid_str, ['avatar']) + self.avatars_cache[jid_str] = "/media/icons/tango/emotes/64/face-plain.png" + return self.avatars_cache[jid_str] + + def registerWidget(self, wid): + log.debug("Registering %s" % wid.getDebugName()) + self.libervia_widgets.add(wid) + + def unregisterWidget(self, wid): + try: + self.libervia_widgets.remove(wid) + except KeyError: + log.warning('trying to remove a non registered Widget: %s' % wid.getDebugName()) + + def refresh(self): + """Refresh the general display.""" + self.panel.refresh() + if self.params_ui['unibox']['value']: + self.uni_box = self.panel.unibox_panel.unibox + else: + self.uni_box = None + for lib_wid in self.libervia_widgets: + lib_wid.refresh() + self.resize() + + def addTab(self, label, wid, select=True): + """Create a new tab and eventually add a widget in + @param label: label of the tab + @param wid: LiberviaWidget to add + @param select: True to select the added tab + """ + widgets_panel = WidgetsPanel(self) + self.tab_panel.add(widgets_panel, label) + widgets_panel.addWidget(wid) + if select: + self.tab_panel.selectTab(self.tab_panel.getWidgetCount() - 1) + return widgets_panel + + def addWidget(self, wid, tab_index=None): + """ Add a widget at the bottom of the current or specified tab + @param wid: LiberviaWidget to add + @param tab_index: index of the tab to add the widget to""" + if tab_index is None or tab_index < 0 or tab_index >= self.tab_panel.getWidgetCount(): + panel = self.tab_panel.getCurrentPanel() + else: + panel = self.tab_panel.tabBar.getTabWidget(tab_index) + panel.addWidget(wid) + + def displayNotification(self, title, body): + self.notification.notify(title, body) + + def _isRegisteredCB(self, result): + registered, warning = result + if not registered: + self._register_box = RegisterBox(self.logged) + self._register_box.centerBox() + self._register_box.show() + if warning: + dialog.InfoDialog(_('Security warning'), warning).show() + self._tryAutoConnect(skip_validation=not not warning) + else: + self._register.call('isConnected', self._isConnectedCB) + + def _isConnectedCB(self, connected): + if not connected: + self._register.call('connect', lambda x: self.logged()) + else: + self.logged() + + def logged(self): + if self._register_box: + self._register_box.hide() + del self._register_box # don't work if self._register_box is None + + # display the real presence status panel + self.panel.header.remove(self.status_panel) + self.status_panel = panels.PresenceStatusPanel(self) + self.panel.header.add(self.status_panel) + + #it's time to fill the page + self.bridge.call('getContacts', self._getContactsCB) + self.bridge.call('getParamsUI', self._getParamsUICB) + self.bridge_signals.call('getSignals', self._getSignalsCB) + #We want to know our own jid + self.bridge.call('getProfileJid', self._getProfileJidCB) + + def domain_cb(value): + self._defaultDomain = value + log.info("new account domain: %s" % value) + + def domain_eb(value): + self._defaultDomain = "libervia.org" + + self.bridge.call("getNewAccountDomain", (domain_cb, domain_eb)) + self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) + + # get ui params and refresh the display + count = 0 # used to do something similar to DeferredList + + def params_ui_cb(param, value=None): + count += 1 + refresh = count == len(self.params_ui) + self._paramUpdate(param['name'], value, param['category'], refresh) + for param in self.params_ui: + self.bridge.call('asyncGetParamA', lambda value: params_ui_cb(self.params_ui[param], value), + self.params_ui[param]['name'], self.params_ui[param]['category']) + + def _tryAutoConnect(self, skip_validation=False): + """This method retrieve the eventual URL parameters to auto-connect the user. + @param skip_validation: if True, set the form values but do not validate it + """ + params = getURLParams(Window.getLocation().getSearch()) + if "login" in params: + self._register_box._form.login_box.setText(params["login"]) + self._register_box._form.login_pass_box.setFocus(True) + if "passwd" in params: + # try to connect + self._register_box._form.login_pass_box.setText(params["passwd"]) + if not skip_validation: + self._register_box._form.onLogin(None) + return True + else: + # this would eventually set the browser saved password + Timer(5, lambda: self._register_box._form.login_pass_box.setFocus(True)) + + def _actionCb(self, data): + if not data: + # action was a one shot, nothing to do + pass + elif "xmlui" in data: + ui = XMLUI(self, xml_data = data['xmlui']) + options = ['NO_CLOSE'] if ui.type == 'form' else [] + _dialog = dialog.GenericDialog(ui.title, ui, options=options) + ui.setCloseCb(_dialog.close) + _dialog.show() + else: + dialog.InfoDialog("Error", + "Unmanaged action result", Width="400px").center() + + def _actionEb(self, err_data): + err_code, err_obj = err_data + dialog.InfoDialog("Error", + str(err_obj), Width="400px").center() + + def launchAction(self, callback_id, data): + """ Launch a dynamic action + @param callback_id: id of the action to launch + @param data: data needed only for certain actions + + """ + if data is None: + data = {} + self.bridge.call('launchAction', (self._actionCb, self._actionEb), callback_id, data) + + def _getContactsCB(self, contacts_data): + for contact in contacts_data: + jid, attributes, groups = contact + self._newContactCb(jid, attributes, groups) + + def _getSignalsCB(self, signal_data): + self.bridge_signals.call('getSignals', self._getSignalsCB) + log.debug("Got signal ==> name: %s, params: %s" % (signal_data[0], signal_data[1])) + name, args = signal_data + if name == 'personalEvent': + self._personalEventCb(*args) + elif name == 'newMessage': + self._newMessageCb(*args) + elif name == 'presenceUpdate': + self._presenceUpdateCb(*args) + elif name == 'paramUpdate': + self._paramUpdate(*args) + elif name == 'roomJoined': + self._roomJoinedCb(*args) + elif name == 'roomLeft': + self._roomLeftCb(*args) + elif name == 'roomUserJoined': + self._roomUserJoinedCb(*args) + elif name == 'roomUserLeft': + self._roomUserLeftCb(*args) + elif name == 'roomUserChangedNick': + self._roomUserChangedNickCb(*args) + elif name == 'askConfirmation': + self._askConfirmation(*args) + elif name == 'newAlert': + self._newAlert(*args) + elif name == 'tarotGamePlayers': + self._tarotGameStartedCb(True, *args) + elif name == 'tarotGameStarted': + self._tarotGameStartedCb(False, *args) + elif name == 'tarotGameNew' or \ + name == 'tarotGameChooseContrat' or \ + name == 'tarotGameShowCards' or \ + name == 'tarotGameInvalidCards' or \ + name == 'tarotGameCardsPlayed' or \ + name == 'tarotGameYourTurn' or \ + name == 'tarotGameScore': + self._tarotGameGenericCb(name, args[0], args[1:]) + elif name == 'radiocolPlayers': + self._radioColStartedCb(True, *args) + elif name == 'radiocolStarted': + self._radioColStartedCb(False, *args) + elif name == 'radiocolPreload': + self._radioColGenericCb(name, args[0], args[1:]) + elif name == 'radiocolPlay': + self._radioColGenericCb(name, args[0], args[1:]) + elif name == 'radiocolNoUpload': + self._radioColGenericCb(name, args[0], args[1:]) + elif name == 'radiocolUploadOk': + self._radioColGenericCb(name, args[0], args[1:]) + elif name == 'radiocolSongRejected': + self._radioColGenericCb(name, args[0], args[1:]) + elif name == 'subscribe': + self._subscribeCb(*args) + elif name == 'contactDeleted': + self._contactDeletedCb(*args) + elif name == 'newContact': + self._newContactCb(*args) + elif name == 'entityDataUpdated': + self._entityDataUpdatedCb(*args) + elif name == 'chatStateReceived': + self._chatStateReceivedCb(*args) + + def _getParamsUICB(self, xmlui): + """Hide the parameters item if there's nothing to display""" + if not xmlui: + self.panel.menu.removeItemParams() + + def _ownBlogsFills(self, mblogs): + #put our own microblogs in cache, then fill all panels with them + for publisher in mblogs: + for mblog in mblogs[publisher]: + if not mblog.has_key('content'): + log.warning("No content found in microblog [%s]" % mblog) + continue + if mblog.has_key('groups'): + _groups = set(mblog['groups'].split() if mblog['groups'] else []) + else: + _groups = None + mblog_entry = MicroblogItem(mblog) + self.mblog_cache.append((_groups, mblog_entry)) + + if len(self.mblog_cache) > MAX_MBLOG_CACHE: + del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + self.FillMicroblogPanel(lib_wid) + self.initialised = True # initialisation phase is finished here + for event_data in self.init_cache: # so we have to send all the cached events + self._personalEventCb(*event_data) + del self.init_cache + + def _getProfileJidCB(self, jid): + self.whoami = JID(jid) + #we can now ask our status + self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb) + #the rooms where we are + self.bridge.call('getRoomsJoined', self._getRoomsJoinedCb) + #and if there is any subscription request waiting for us + self.bridge.call('getWaitingSub', self._getWaitingSubCb) + #we fill the panels already here + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + if lib_wid.accept_all(): + self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10) + else: + self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10) + + #we ask for our own microblogs: + self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10) + + ## Signals callbacks ## + + def _personalEventCb(self, sender, event_type, data): + if not self.initialised: + self.init_cache.append((sender, event_type, data)) + return + sender = JID(sender).bare + if event_type == "MICROBLOG": + if not 'content' in data: + log.warning("No content found in microblog data") + return + if 'groups' in data: + _groups = set(data['groups'].split() if data['groups'] else []) + else: + _groups = None + mblog_entry = MicroblogItem(data) + + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + self.addBlogEntry(lib_wid, sender, _groups, mblog_entry) + + if sender == self.whoami.bare: + found = False + for index in xrange(0, len(self.mblog_cache)): + entry = self.mblog_cache[index] + if entry[1].id == mblog_entry.id: + # replace existing entry + self.mblog_cache.remove(entry) + self.mblog_cache.insert(index, (_groups, mblog_entry)) + found = True + break + if not found: + self.mblog_cache.append((_groups, mblog_entry)) + if len(self.mblog_cache) > MAX_MBLOG_CACHE: + del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] + elif event_type == 'MICROBLOG_DELETE': + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + lib_wid.removeEntry(data['type'], data['id']) + log.debug("%s %s %s" % (self.whoami.bare, sender, data['type'])) + + if sender == self.whoami.bare and data['type'] == 'main_item': + for index in xrange(0, len(self.mblog_cache)): + entry = self.mblog_cache[index] + if entry[1].id == data['id']: + self.mblog_cache.remove(entry) + break + + def addBlogEntry(self, mblog_panel, sender, _groups, mblog_entry): + """Check if an entry can go in MicroblogPanel and add to it + @param mblog_panel: MicroblogPanel instance + @param sender: jid of the entry sender + @param _groups: groups which can receive this entry + @param mblog_entry: MicroblogItem instance""" + if mblog_entry.type == "comment" or mblog_panel.isJidAccepted(sender) or (_groups == None and self.whoami and sender == self.whoami.bare) \ + or (_groups and _groups.intersection(mblog_panel.accepted_groups)): + mblog_panel.addEntry(mblog_entry) + + def FillMicroblogPanel(self, mblog_panel): + """Fill a microblog panel with entries in cache + @param mblog_panel: MicroblogPanel instance + """ + #XXX: only our own entries are cached + for cache_entry in self.mblog_cache: + _groups, mblog_entry = cache_entry + self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry) + + def getEntityMBlog(self, entity): + log.info("geting mblog for entity [%s]" % (entity,)) + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + if lib_wid.isJidAccepted(entity): + self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10) + + def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True): + """Get the corresponding panel if it exists. + @param class_: class of the panel (ChatPanel, MicroblogPanel...) + @param entity: polymorphic parameter, see class_.matchEntity. + @param ignoreOtherTabs: if True, the widgets that are not + contained by the currently selected tab will be ignored + @return: the existing widget that has been found or None.""" + selected_tab = self.tab_panel.getCurrentPanel() + for lib_wid in self.libervia_widgets: + parent = lib_wid.getWidgetsPanel(verbose=False) + if parent is None or (ignoreOtherTabs and parent != selected_tab): + # do not return a widget that is not in the currently selected tab + continue + if isinstance(lib_wid, class_): + try: + if lib_wid.matchEntity(entity): + log.debug("existing widget found: %s" % lib_wid.getDebugName()) + return lib_wid + except AttributeError as e: + e.stack_list() + return None + return None + + def getOrCreateLiberviaWidget(self, class_, entity, select=True, new_tab=None): + """Get the matching LiberviaWidget if it exists, or create a new one. + @param class_: class of the panel (ChatPanel, MicroblogPanel...) + @param entity: polymorphic parameter, see class_.matchEntity. + @param select: if True, select the widget that has been found or created + @param new_tab: if not None, a widget which is created is created in + a new tab. In that case new_tab is a unicode to label that new tab. + If new_tab is not None and a widget is found, no tab is created. + @return: the newly created wigdet if REUSE_EXISTING_LIBERVIA_WIDGETS + is set to False or if the widget has not been found, the existing + widget that has been found otherwise.""" + lib_wid = None + tab = None + if REUSE_EXISTING_LIBERVIA_WIDGETS: + lib_wid = self.getLiberviaWidget(class_, entity, new_tab is None) + if lib_wid is None: # create a new widget + lib_wid = class_.createPanel(self, entity[0] if isinstance(entity, tuple) else entity) + if new_tab is None: + self.addWidget(lib_wid) + else: + tab = self.addTab(new_tab, lib_wid, False) + else: # reuse existing widget + tab = lib_wid.getWidgetsPanel(verbose=False) + if new_tab is None: + if tab is not None: + tab.removeWidget(lib_wid) + self.addWidget(lib_wid) + if select: + if new_tab is not None: + self.tab_panel.selectTab(tab) + # must be done after the widget is added, + # for example to scroll to the bottom + self.setSelected(lib_wid) + lib_wid.refresh() + return lib_wid + + def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): + _from = JID(from_jid) + _to = JID(to_jid) + other = _to if _from.bare == self.whoami.bare else _from + lib_wid = self.getLiberviaWidget(panels.ChatPanel, other, ignoreOtherTabs=False) + self.displayNotification(_from, msg) + if lib_wid is not None: + lib_wid.printMessage(_from, msg, extra) + else: + # The message has not been shown, we must indicate it + self.contact_panel.setContactMessageWaiting(other.bare, True) + + def _presenceUpdateCb(self, entity, show, priority, statuses): + entity_jid = JID(entity) + if self.whoami and self.whoami == entity_jid: # XXX: QnD way to get our presence/status + self.status_panel.setPresence(show) + if statuses: + self.status_panel.setStatus(statuses.values()[0]) + else: + self.contact_panel.setConnected(entity_jid.bare, entity_jid.resource, show, priority, statuses) + + def _roomJoinedCb(self, room_jid, room_nicks, user_nick): + _target = JID(room_jid) + if _target not in self.room_list: + self.room_list.append(_target) + chat_panel = panels.ChatPanel(self, _target, type_='group') + chat_panel.setUserNick(user_nick) + if _target.node.startswith('sat_tarot_'): #XXX: it's not really beautiful, but it works :) + self.addTab("Tarot", chat_panel) + elif _target.node.startswith('sat_radiocol_'): + self.addTab("Radio collective", chat_panel) + else: + self.addTab(_target.node, chat_panel) + chat_panel.setPresents(room_nicks) + chat_panel.historyPrint() + chat_panel.refresh() + + def _roomLeftCb(self, room_jid, room_nicks, user_nick): + # FIXME: room_list contains JID instances so why MUST we do + # 'remove(room_jid)' and not 'remove(JID(room_jid))' ????!! + # This looks like a pyjamas bug --> check/report + try: + self.room_list.remove(room_jid) + except KeyError: + pass + + def _roomUserJoinedCb(self, room_jid_s, user_nick, user_data): + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + lib_wid.userJoined(user_nick, user_data) + + def _roomUserLeftCb(self, room_jid_s, user_nick, user_data): + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + lib_wid.userLeft(user_nick, user_data) + + def _roomUserChangedNickCb(self, room_jid_s, old_nick, new_nick): + """Called when an user joined a MUC room""" + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + lib_wid.changeUserNick(old_nick, new_nick) + + def _tarotGameStartedCb(self, waiting, room_jid_s, referee, players): + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + lib_wid.startGame("Tarot", waiting, referee, players) + + def _tarotGameGenericCb(self, event_name, room_jid_s, args): + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + getattr(lib_wid.getGame("Tarot"), event_name)(*args) + + def _radioColStartedCb(self, waiting, room_jid_s, referee, players, queue_data): + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + lib_wid.startGame("RadioCol", waiting, referee, players, queue_data) + + def _radioColGenericCb(self, event_name, room_jid_s, args): + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel) and lib_wid.type == 'group' and lib_wid.target.bare == room_jid_s: + getattr(lib_wid.getGame("RadioCol"), event_name)(*args) + + def _getPresenceStatusesCb(self, presence_data): + for entity in presence_data: + for resource in presence_data[entity]: + args = presence_data[entity][resource] + self._presenceUpdateCb("%s/%s" % (entity, resource), *args) + + def _getRoomsJoinedCb(self, room_data): + for room in room_data: + self._roomJoinedCb(*room) + + def _getWaitingSubCb(self, waiting_sub): + for sub in waiting_sub: + self._subscribeCb(waiting_sub[sub], sub) + + def _subscribeCb(self, sub_type, entity): + if sub_type == 'subscribed': + dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show() + self.getEntityMBlog(entity) + + elif sub_type == 'unsubscribed': + dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show() + #TODO: remove microblogs from panels + + elif sub_type == 'subscribe': + #The user want to subscribe to our presence + _dialog = None + msg = HTML('The contact <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity)) + + def ok_cb(ignore): + self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups()) + def cancel_cb(ignore): + self.bridge.call('subscription', None, "unsubscribed", entity, '', '') + + _dialog = dialog.GroupSelector([msg], self.contact_panel.getGroups(), [], "Add", ok_cb, cancel_cb) + _dialog.setHTML('<b>Add contact request</b>') + _dialog.show() + + def _contactDeletedCb(self, entity): + self.contact_panel.removeContact(entity) + + def _newContactCb(self, contact, attributes, groups): + self.contact_panel.updateContact(contact, attributes, groups) + + def _entityDataUpdatedCb(self, entity_jid_s, key, value): + if key == "avatar": + avatar = '/avatars/%s' % value + + self.avatars_cache[entity_jid_s] = avatar + + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.MicroblogPanel): + if lib_wid.isJidAccepted(entity_jid_s) or (self.whoami and entity_jid_s == self.whoami.bare): + lib_wid.updateValue('avatar', entity_jid_s, avatar) + + def _chatStateReceivedCb(self, from_jid_s, state): + """Callback when a new chat state is received. + @param from_jid_s: JID of the contact who sent his state, or '@ALL@' + @param state: new state (string) + """ + if from_jid_s == '@ALL@': + target = '@ALL@' + nick = C.ALL_OCCUPANTS + else: + from_jid = JID(from_jid_s) + target = from_jid.bare + nick = from_jid.resource + + for lib_wid in self.libervia_widgets: + if isinstance(lib_wid, panels.ChatPanel): + if target == '@ALL' or target == lib_wid.target.bare: + if lib_wid.type == 'one2one': + lib_wid.setState(state) + elif lib_wid.type == 'group': + lib_wid.setState(state, nick=nick) + + def _askConfirmation(self, confirmation_id, confirmation_type, data): + answer_data = {} + + def confirm_cb(result): + self.bridge.call('confirmationAnswer', None, confirmation_id, result, answer_data) + + if confirmation_type == "YES/NO": + dialog.ConfirmDialog(confirm_cb, text=data["message"], title=data["title"]).show() + + def _newAlert(self, message, title, alert_type): + dialog.InfoDialog(title, message).show() + + def _paramUpdate(self, name, value, category, refresh=True): + """This is called when the paramUpdate signal is received, but also + during initialization when the UI parameters values are retrieved. + @param refresh: set to True to refresh the general UI + """ + for param in self.params_ui: + if name == self.params_ui[param]['name']: + self.params_ui[param]['value'] = self.params_ui[param]['cast'](value) + if refresh: + self.refresh() + break + + def sendError(self, errorData): + dialog.InfoDialog("Error while sending message", + "Your message can't be sent", Width="400px").center() + log.error("sendError: %s" % str(errorData)) + + def send(self, targets, text, extra={}): + """Send a message to any target type. + @param targets: list of tuples (type, entities, addr) with: + - type in ("PUBLIC", "GROUP", "COMMENT", "STATUS" , "groupchat" , "chat") + - entities could be a JID, a list groups, a node hash... depending the target + - addr in ("To", "Cc", "Bcc") - ignore case + @param text: the message content + @param extra: options + """ + # FIXME: too many magic strings, we should use constants instead + addresses = [] + for target in targets: + type_, entities, addr = target[0], target[1], 'to' if len(target) < 3 else target[2].lower() + if type_ in ("PUBLIC", "GROUP"): + self.bridge.call("sendMblog", None, type_, entities if type_ == "GROUP" else None, text, extra) + elif type_ == "COMMENT": + self.bridge.call("sendMblogComment", None, entities, text, extra) + elif type_ == "STATUS": + self.bridge.call('setStatus', None, self.status_panel.presence, text) + elif type_ in ("groupchat", "chat"): + addresses.append((addr, entities)) + else: + log.error("Unknown target type") + if addresses: + if len(addresses) == 1 and addresses[0][0] == 'to': + self.bridge.call('sendMessage', (None, self.sendError), addresses[0][1], text, '', type_, extra) + else: + extra.update({'address': '\n'.join([('%s:%s' % entry) for entry in addresses])}) + self.bridge.call('sendMessage', (None, self.sendError), self.whoami.domain, text, '', type_, extra) + + def showWarning(self, type_=None, msg=None): + """Display a popup information message, e.g. to notify the recipient of a message being composed. + If type_ is None, a popup being currently displayed will be hidden. + @type_: a type determining the CSS style to be applied (see WarningPopup.showWarning) + @msg: message to be displayed + """ + if not hasattr(self, "warning_popup"): + self.warning_popup = panels.WarningPopup() + self.warning_popup.showWarning(type_, msg) + + +if __name__ == '__main__': + app = SatWebFrontend() + app.onModuleLoad() + pyjd.run()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/list_manager.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,608 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from pyjamas.ui.Grid import Grid +from pyjamas.ui.Button import Button +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.FlowPanel import FlowPanel +from pyjamas.ui.AutoComplete import AutoCompleteTextBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.KeyboardListener import KEY_ENTER +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.DropWidget import DropWidget +from pyjamas.Timer import Timer +from pyjamas import DOM + +from base_panels import PopupMenuPanel +from base_widget import DragLabel + +# HTML content for the removal button (image or text) +REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>' + +# Item to be considered for an empty list box selection. +# Could be whatever which doesn't look like a JID or a group name. +EMPTY_SELECTION_ITEM = "" + + +class ListManager(): + """A manager for sub-panels to assign elements to lists.""" + + def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}): + """ + @param parent: FlexTable parent widget for the manager + @param keys_dict: dict with the contact keys mapped to data + @param contact_list: list of string (the contact JID userhosts) + @param offsets: dict to set widget positions offset within parent + - "x_first": the x offset for the first widget's row on the grid + - "x": the x offset for all widgets rows, except the first one if "x_first" is defined + - "y": the y offset for all widgets columns on the grid + """ + self._parent = parent + if isinstance(keys_dict, set) or isinstance(keys_dict, list): + tmp = {} + for key in keys_dict: + tmp[key] = {} + keys_dict = tmp + self.__keys_dict = keys_dict + if isinstance(contact_list, set): + contact_list = list(contact_list) + self.__list = contact_list + self.__list.sort() + # store the list of contacts that are not assigned yet + self.__remaining_list = [] + self.__remaining_list.extend(self.__list) + # mark a change to sort the list before it's used + self.__remaining_list_sorted = True + + self.offsets = {"x_first": 0, "x": 0, "y": 0} + if "x" in offsets and not "x_first" in offsets: + offsets["x_first"] = offsets["x"] + self.offsets.update(offsets) + + self.style = { + "keyItem": "recipientTypeItem", + "popupMenuItem": "recipientTypeItem", + "buttonCell": "recipientButtonCell", + "dragoverPanel": "dragover-recipientPanel", + "keyPanel": "recipientPanel", + "textBox": "recipientTextBox", + "textBox-invalid": "recipientTextBox-invalid", + "removeButton": "recipientRemoveButton", + } + self.style.update(style) + + def createWidgets(self, title_format="%s"): + """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" + self.__children = {} + for key in self.__keys_dict: + self.addContactKey(key, title_format=title_format) + + def addContactKey(self, key, dict_={}, title_format="%s"): + if key not in self.__keys_dict: + self.__keys_dict[key] = dict_ + # copy the key to its associated sub-map + self.__keys_dict[key]["title"] = key + self._addChild(self.__keys_dict[key], title_format) + + def removeContactKey(self, key): + """Remove a list panel and all its associated data.""" + contacts = self.__children[key]["panel"].getContacts() + (y, x) = self._parent.getIndex(self.__children[key]["button"]) + self._parent.removeRow(y) + del self.__children[key] + del self.__keys_dict[key] + self.addToRemainingList(contacts) + + def _addChild(self, entry, title_format): + """Add a button and FlowPanel for the corresponding map entry.""" + button = Button(title_format % entry["title"]) + button.setStyleName(self.style["keyItem"]) + if hasattr(entry, "desc"): + button.setTitle(entry["desc"]) + if not "optional" in entry: + entry["optional"] = False + button.setVisible(not entry["optional"]) + y = len(self.__children) + self.offsets["y"] + x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] + + self._parent.insertRow(y) + self._parent.setWidget(y, x, button) + self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) + + _child = ListPanel(self, entry, self.style) + self._parent.setWidget(y, x + 1, _child) + + self.__children[entry["title"]] = {} + self.__children[entry["title"]]["button"] = button + self.__children[entry["title"]]["panel"] = _child + + if hasattr(self, "popup_menu"): + # this is done if self.registerPopupMenuPanel has been called yet + self.popup_menu.registerClickSender(button) + + def _refresh(self, visible=True): + """Set visible the sub-panels that are non optional or non empty, hide the rest.""" + for key in self.__children: + self.setContactPanelVisible(key, False) + if not visible: + return + _map = self.getContacts() + for key in _map: + if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: + self.setContactPanelVisible(key, True) + + def setVisible(self, visible): + self._refresh(visible) + + def setContactPanelVisible(self, key, visible=True, sender=None): + """Do not remove the "sender" param as it is needed for the context menu.""" + self.__children[key]["button"].setVisible(visible) + self.__children[key]["panel"].setVisible(visible) + + @property + def list(self): + """Return the full list of potential contacts.""" + return self.__list + + @property + def keys(self): + return self.__keys_dict.keys() + + @property + def keys_dict(self): + return self.__keys_dict + + @property + def remaining_list(self): + """Return the contacts that have not been selected yet.""" + if not self.__remaining_list_sorted: + self.__remaining_list_sorted = True + self.__remaining_list.sort() + return self.__remaining_list + + def setRemainingListUnsorted(self): + """Mark a change (deletion) so the list will be sorted before it's used.""" + self.__remaining_list_sorted = False + + def removeFromRemainingList(self, contacts): + """Remove contacts after they have been added to a sub-panel.""" + if not isinstance(contacts, list): + contacts = [contacts] + for contact_ in contacts: + if contact_ in self.__remaining_list: + self.__remaining_list.remove(contact_) + + def addToRemainingList(self, contacts, ignore_key=None): + """Add contacts after they have been removed from a sub-panel.""" + if not isinstance(contacts, list): + contacts = [contacts] + assigned_contacts = set() + assigned_map = self.getContacts() + for key_ in assigned_map.keys(): + if ignore_key is not None and key_ == ignore_key: + continue + assigned_contacts.update(assigned_map[key_]) + for contact_ in contacts: + if contact_ not in self.__list or contact_ in self.__remaining_list: + continue + if contact_ in assigned_contacts: + continue # the contact is assigned somewhere else + self.__remaining_list.append(contact_) + self.setRemainingListUnsorted() + + def setContacts(self, _map={}): + """Set the contacts for each contact key.""" + for key in self.__keys_dict: + if key in _map: + self.__children[key]["panel"].setContacts(_map[key]) + else: + self.__children[key]["panel"].setContacts([]) + self._refresh() + + def getContacts(self): + """Get the contacts for all the lists. + @return: a mapping between keys and contact lists.""" + _map = {} + for key in self.__children: + _map[key] = self.__children[key]["panel"].getContacts() + return _map + + @property + def target_drop_cell(self): + """@return: the panel where something has been dropped.""" + return self._target_drop_cell + + def setTargetDropCell(self, target_drop_cell): + """@param: target_drop_cell: the panel where something has been dropped.""" + self._target_drop_cell = target_drop_cell + + def registerPopupMenuPanel(self, entries, hide, callback): + "Register a popup menu panel that will be bound to all contact keys elements." + self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) + + +class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler): + """A draggable AutoCompleteTextBox which is used for representing a contact. + This class is NOT generic because of the onDragEnd method which call methods + from ListPanel. It's probably not reusable for another scenario. + """ + + def __init__(self, parent, event_cbs, style): + AutoCompleteTextBox.__init__(self) + DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type + self._parent = parent + self.event_cbs = event_cbs + self.style = style + self.addMouseListener(self) + self.addFocusListener(self) + self.addChangeListener(self) + self.addStyleName(style["textBox"]) + self.reset() + + def reset(self): + self.setText("") + self.setValid() + + def setValid(self, valid=True): + if self.getText() == "": + valid = True + if valid: + self.removeStyleName(self.style["textBox-invalid"]) + else: + self.addStyleName(self.style["textBox-invalid"]) + self.valid = valid + + def onDragStart(self, event): + self._text = self.getText() + DragLabel.onDragStart(self, event) + self._parent.setTargetDropCell(None) + self.setSelectionRange(len(self.getText()), 0) + + def onDragEnd(self, event): + target = self._parent.target_drop_cell # parent or another ListPanel + if self.getText() == "" or target is None: + return + self.event_cbs["drop"](self, target) + + def setRemoveButton(self): + + def remove_cb(sender): + """Callback for the button to remove this contact.""" + self._parent.remove(self) + self._parent.remove(self.remove_btn) + self.event_cbs["remove"](self) + + self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) + self.remove_btn.setStyleName(self.style["removeButton"]) + self._parent.add(self.remove_btn) + + def removeOrReset(self): + if hasattr(self, "remove_btn"): + self.remove_btn.click() + else: + self.reset() + + def onMouseMove(self, sender): + """Mouse enters the area of a DragAutoCompleteTextBox.""" + if hasattr(sender, "remove_btn"): + sender.remove_btn.setVisible(True) + + def onMouseLeave(self, sender): + """Mouse leaves the area of a DragAutoCompleteTextBox.""" + if hasattr(sender, "remove_btn"): + Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) + + def onFocus(self, sender): + sender.setSelectionRange(0, len(self.getText())) + self.event_cbs["focus"](sender) + + def validate(self): + self.setSelectionRange(len(self.getText()), 0) + self.event_cbs["validate"](self) + + def onChange(self, sender): + """The textbox or list selection is changed""" + if isinstance(sender, ListBox): + AutoCompleteTextBox.onChange(self, sender) + self.validate() + + def onClick(self, sender): + """The list is clicked""" + AutoCompleteTextBox.onClick(self, sender) + self.validate() + + def onKeyUp(self, sender, keycode, modifiers): + """Listen for ENTER key stroke""" + AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) + if keycode == KEY_ENTER: + self.validate() + + +class DropCell(DropWidget): + """A cell where you can drop widgets. This class is NOT generic because of + onDrop which uses methods from ListPanel. It has been created to + separate the drag and drop methods from the others and add a bit of + lisibility, but it's probably not reusable for another scenario. + """ + + def __init__(self, drop_cbs): + DropWidget.__init__(self) + self.drop_cbs = drop_cbs + + def onDragEnter(self, event): + self.addStyleName(self.style["dragoverPanel"]) + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + 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, we don't want the style to be removed + self.removeStyleName(self.style["dragoverPanel"]) + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def onDrop(self, event): + DOM.eventPreventDefault(event) + dt = event.dataTransfer + # 'text', 'text/plain', and 'Text' are equivalent. + item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed + if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and + item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report + if item_type in self.drop_cbs.keys(): + self.drop_cbs[item_type](self, item) + self.removeStyleName(self.style["dragoverPanel"]) + + +VALID = 1 +INVALID = 2 +DELETE = 3 + + +class ListPanel(FlowPanel, DropCell): + """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel + is not fully implemented yet and can not be used with pyjamas.ui.Label.""" + + def __init__(self, parent, entry, style={}): + """Initialization with a button and a DragAutoCompleteTextBox.""" + FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) + drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), + "CONTACT": lambda panel, item: self.addContact(item), + "CONTACT_TITLE": lambda panel, item: self.addContact('@@'), + "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) + } + DropCell.__init__(self, drop_cbs) + self.style = style + self.addStyleName(self.style["keyPanel"]) + self._parent = parent + self.key = entry["title"] + self._addTextBox() + + def _addTextBox(self, switchPrevious=False): + """Add a text box to the last position. If switchPrevious is True, simulate + an insertion before the current last textbox by copying the text and valid state. + @return: the created textbox or the previous one if switchPrevious is True. + """ + if hasattr(self, "_last_textbox"): + if self._last_textbox.getText() == "": + return + self._last_textbox.setRemoveButton() + else: + switchPrevious = False + + def focus_cb(sender): + if sender != self._last_textbox: + # save the current value before it's being modified + self._parent.addToRemainingList(sender.getText(), ignore_key=self.key) + sender.setCompletionItems(self._parent.remaining_list) + + def remove_cb(sender): + """Callback for the button to remove this contact.""" + self._parent.addToRemainingList(sender.getText()) + self._parent.setRemainingListUnsorted() + self._last_textbox.setFocus(True) + + def drop_cb(sender, target): + """Callback when the textbox is drag-n-dropped.""" + parent = sender._parent + if target != parent and target.addContact(sender.getText()): + sender.removeOrReset() + else: + parent._parent.removeFromRemainingList(sender.getText()) + + events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} + textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) + self.add(textbox) + if switchPrevious: + textbox.setText(self._last_textbox.getText()) + textbox.setValid(self._last_textbox.valid) + self._last_textbox.reset() + previous = self._last_textbox + self._last_textbox = textbox + return previous if switchPrevious else textbox + + def _checkContact(self, contact, modify): + """ + @param contact: the contact to check + @param modify: True if the contact is being modified + @return: + - VALID if the contact is valid + - INVALID if the contact is not valid but can be displayed + - DELETE if the contact should not be displayed at all + """ + def countItemInList(list_, item): + """For some reason the built-in count function doesn't work...""" + count = 0 + for elem in list_: + if elem == item: + count += 1 + return count + if contact is None or contact == "": + return DELETE + if countItemInList(self.getContacts(), contact) > (1 if modify else 0): + return DELETE + return VALID if contact in self._parent.list else INVALID + + def addContact(self, contact, sender=None): + """The first parameter type is checked, so it is also possible to call addContact(sender). + If contact is not defined, sender.getText() is used. If sender is not defined, contact will + be written to the last textbox and a new textbox is added afterward. + @param contact: unicode + @param sender: DragAutoCompleteTextBox instance + """ + if isinstance(contact, DragAutoCompleteTextBox): + sender = contact + contact = sender.getText() + valid = self._checkContact(contact, sender is not None) + if sender is None: + # method has been called to modify but to add a contact + if valid == VALID: + # eventually insert before the last textbox if it's not empty + sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox + sender.setText(contact) + else: + sender.setValid(valid == VALID) + if valid != VALID: + if sender is not None and valid == DELETE: + sender.removeOrReset() + return False + if sender == self._last_textbox: + self._addTextBox() + try: + sender.setVisibleLength(len(contact)) + except: + # IndexSizeError: Index or size is negative or greater than the allowed amount + log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact))) + self._parent.removeFromRemainingList(contact) + self._last_textbox.setFocus(True) + return True + + def emptyContacts(self): + """Empty the list of contacts.""" + for child in self.getChildren(): + if hasattr(child, "remove_btn"): + child.remove_btn.click() + + def setContacts(self, tab): + """Set the contacts.""" + self.emptyContacts() + if isinstance(tab, set): + tab = list(tab) + tab.sort() + for contact in tab: + self.addContact(contact) + + def getContacts(self): + """Get the contacts + @return: an array of string""" + tab = [] + for widget in self.getChildren(): + if isinstance(widget, DragAutoCompleteTextBox): + # not to be mixed with EMPTY_SELECTION_ITEM + if widget.getText() != "": + tab.append(widget.getText()) + return tab + + @property + def target_drop_cell(self): + """@return: the panel where something has been dropped.""" + return self._parent.target_drop_cell + + def setTargetDropCell(self, target_drop_cell): + """ + XXX: Property setter here would not make it, you need a proper method! + @param target_drop_cell: the panel where something has been dropped.""" + self._parent.setTargetDropCell(target_drop_cell) + + +class ContactChooserPanel(DialogBox): + """Display the contacts chooser dialog. This has been implemented while + prototyping and is currently not used. Left for an eventual later use. + Replaced by the popup menu which allows to add a panel for Cc or Bcc. + """ + + def __init__(self, manager, **kwargs): + """Display a listbox for each contact key""" + DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) + self.setHTML("Select contacts") + self.manager = manager + self.listboxes = {} + self.contacts = manager.getContacts() + + container = VerticalPanel(Visible=True) + container.addStyleName("marginAuto") + + grid = Grid(2, len(self.manager.keys_dict)) + index = -1 + for key in self.manager.keys_dict: + index += 1 + grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index) + listbox = ListBox() + listbox.setMultipleSelect(True) + listbox.setVisibleItemCount(15) + listbox.addItem(EMPTY_SELECTION_ITEM) + for element in manager.list: + listbox.addItem(element) + self.listboxes[key] = listbox + grid.add(listbox, 1, index) + self._reset() + + buttons = HorizontalPanel() + buttons.addStyleName("marginAuto") + btn_close = Button("Cancel", self.hide) + buttons.add(btn_close) + btn_reset = Button("Reset", self._reset) + buttons.add(btn_reset) + btn_ok = Button("OK", self._validate) + buttons.add(btn_ok) + + container.add(grid) + container.add(buttons) + + self.add(container) + self.center() + + def _reset(self): + """Reset the selections.""" + for key in self.manager.keys_dict: + listbox = self.listboxes[key] + for i in xrange(0, listbox.getItemCount()): + if listbox.getItemText(i) in self.contacts[key]: + listbox.setItemSelected(i, "selected") + else: + listbox.setItemSelected(i, "") + + def _validate(self): + """Sets back the selected contacts to the good sub-panels.""" + _map = {} + for key in self.manager.keys_dict: + selections = self.listboxes[key].getSelectedItemText() + if EMPTY_SELECTION_ITEM in selections: + selections.remove(EMPTY_SELECTION_ITEM) + _map[key] = selections + self.manager.setContacts(_map) + self.hide()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/logging.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. +"""This module configure logs for Libervia browser side""" + +from __pyjamas__ import console +from constants import Const as C +from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas + + +class LiberviaLogger(log.Logger): + + def out(self, message, level=None): + if level == C.LOG_LVL_DEBUG: + console.debug(message) + elif level == C.LOG_LVL_INFO: + console.info(message) + elif level == C.LOG_LVL_WARNING: + console.warn(message) + else: + console.error(message) + + +def configure(): + fmt = '[%(name)s] %(message)s' + log.configure(C.LOG_BACKEND_CUSTOM, + logger_class = LiberviaLogger, + level = C.LOG_LVL_DEBUG, + fmt = fmt, + output = None, + logger = None, + colors = False, + force_colors = False) + # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed + LiberviaLogger.fmt = fmt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/menu.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _ + +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.MenuBar import MenuBar +from pyjamas.ui.MenuItem import MenuItem +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Frame import Frame +from pyjamas import Window + +from jid import JID + +from file_tools import FileUploadPanel +from xmlui import XMLUI +import panels +import dialog +from contact_group import ContactGroupEditor + + +class MenuCmd: + + def __init__(self, object_, handler): + self._object = object_ + self._handler = handler + + def execute(self): + handler = getattr(self._object, self._handler) + handler() + + +class PluginMenuCmd: + + def __init__(self, host, action_id): + self.host = host + self.action_id = action_id + + def execute(self): + self.host.launchAction(self.action_id, None) + + +class LiberviaMenuBar(MenuBar): + + def __init__(self): + MenuBar.__init__(self, vertical=False) + self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal") + # TODO: properly adapt CSS to the new class name + + def doItemAction(self, item, fireCommand): + MenuBar.doItemAction(self, item, fireCommand) + if item == self.items[-1] and self.popup: + self.popup.setPopupPosition(Window.getClientWidth() - + self.popup.getOffsetWidth() - 22, + self.getAbsoluteTop() + + self.getOffsetHeight() - 1) + self.popup.addStyleName('menuLastPopup') + + +class AvatarUpload(FileUploadPanel): + def __init__(self): + texts = {'ok_button': 'Upload avatar', + 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.', + 'errback': "Can't open image... did you actually submit an image?", + 'body_errback': 'Please select another image file.', + 'callback': "Your new profile picture has been set!"} + FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts) + + +class Menu(SimplePanel): + + def __init__(self, host): + self.host = host + SimplePanel.__init__(self) + self.setStyleName('menuContainer') + + def createMenus(self, add_menus): + _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s" + menus_dict = {} + menus_order = [] + + def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd): + """ add a menu to menu_dict """ + log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd)) + try: + menu_bar = menus_dict[menu_name] + except KeyError: + menu_bar = menus_dict[menu_name] = MenuBar(vertical=True) + menus_order.append((menu_name, menu_name_i18n, icon)) + if item_name_i18n and menu_cmd: + menu_bar.addItem(item_name_i18n, menu_cmd) + + addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget")) + addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect")) + addMenu("Contacts", _("Contacts"), None, 'social', None) + addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom")) + addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio")) + addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame")) + addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame")) + + # additional menus + for action_id, type_, path, path_i18n in add_menus: + if not path: + log.warning("skipping menu without path") + continue + if len(path) != len(path_i18n): + log.error("inconsistency between menu paths") + continue + menu_name = path[0] + menu_name_i18n = path_i18n[0] + item_name = path[1:] + if not item_name: + log.warning("skipping menu with a path of lenght 1 [%s]" % path[0]) + continue + item_name_i18n = ' | '.join(path_i18n[1:]) + addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id)) + + # menu items that should be displayed after the automatically added ones + addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups")) + + menus_order.append(None) # we add separator + + addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract")) + addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout")) + addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount")) + addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters")) + + # XXX: temporary, will change when a full profile will be managed in SàT + addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload")) + + menubar = LiberviaMenuBar() + + for menu_data in menus_order: + if menu_data is None: + _separator = MenuItem('', None) + _separator.setStyleName('menuSeparator') + menubar.addItem(_separator, None) + else: + menu_name, menu_name_i18n, icon = menu_data + menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name])) + + self.add(menubar) + + #General menu + def onWebWidget(self): + web_panel = panels.WebPanel(self.host, "http://www.goffi.org") + self.host.addWidget(web_panel) + self.host.setSelected(web_panel) + + def onDisconnect(self): + def confirm_cb(answer): + if answer: + log.info("disconnection") + self.host.bridge.call('disconnect', None) + _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?") + _dialog.show() + + def onSocialContract(self): + _frame = Frame('contrat_social.html') + _frame.setStyleName('infoFrame') + _dialog = dialog.GenericDialog("Contrat Social", _frame) + _dialog.setSize('80%', '80%') + _dialog.show() + + def onAbout(self): + _about = HTML("""<b>Libervia</b>, a Salut à Toi project<br /> +<br /> +You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br /> +Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br /> +Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br /> +<br /> +Any help welcome :) +<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p> +""") + _dialog = dialog.GenericDialog("About", _about) + _dialog.show() + + #Contact menu + def onManageContactGroups(self): + """Open the contact groups manager.""" + + def onCloseCallback(): + pass + + ContactGroupEditor(self.host, None, onCloseCallback) + + #Group menu + def onJoinRoom(self): + + def invite(room_jid, contacts): + for contact in contacts: + self.host.bridge.call('inviteMUC', None, contact, room_jid) + + def join(room_jid, contacts): + if self.host.whoami: + nick = self.host.whoami.node + if room_jid not in [room.bare for room in self.host.room_list]: + self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick) + else: + self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare) + invite(room_jid, contacts) + + dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False)) + + def onCollectiveRadio(self): + def callback(room_jid, contacts): + self.host.bridge.call('launchRadioCollective', None, contacts, room_jid) + dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True)) + + #Game menu + def onTarotGame(self): + def onPlayersSelected(room_jid, other_players): + self.host.bridge.call('launchTarotGame', None, other_players, room_jid) + dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True)) + + def onXiangqiGame(self): + Window.alert("A Xiangqi game is planed, but not available yet") + + #Settings menu + + def onAccount(self): + def gotUI(xmlui): + if not xmlui: + return + body = XMLUI(self.host, xmlui) + _dialog = dialog.GenericDialog("Manage your XMPP account", body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() + self.host.bridge.call('getAccountDialogUI', gotUI) + + def onParameters(self): + def gotParams(xmlui): + if not xmlui: + return + body = XMLUI(self.host, xmlui) + _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.setSize('80%', '80%') + _dialog.show() + self.host.bridge.call('getParamsUI', gotParams) + + def removeItemParams(self): + """Remove the Parameters item from the Settings menu bar.""" + self.menu_settings.removeItem(self.item_params) + + def onAvatarUpload(self): + body = AvatarUpload() + _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.setWidth('40%') + _dialog.show()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/nativedom.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +""" +This class provide basic DOM parsing based on native javascript parser +__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989 +""" + +from __pyjamas__ import JS + + +class Node(): + + def __init__(self, js_node): + self._node = js_node + + def _jsNodesList2List(self, js_nodes_list): + ret=[] + for i in range(len(js_nodes_list)): + #ret.append(Element(js_nodes_list.item(i))) + ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug + return ret + + @property + def nodeName(self): + return self._node.nodeName + + @property + def wholeText(self): + return self._node.wholeText + + @property + def childNodes(self): + return self._jsNodesList2List(self._node.childNodes) + + def getAttribute(self, attr): + return self._node.getAttribute(attr) + + def setAttribute(self, attr, value): + return self._node.setAttribute(attr, value) + + def hasAttribute(self, attr): + return self._node.hasAttribute(attr) + + def toxml(self): + return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""") + + +class Element(Node): + + def __init__(self, js_node): + Node.__init__(self, js_node) + + def getElementsByTagName(self, tagName): + return self._jsNodesList2List(self._node.getElementsByTagName(tagName)) + + +class Document(Node): + + def __init__(self, js_document): + Node.__init__(self, js_document) + + @property + def documentElement(self): + return Element(self._node.documentElement) + + +class NativeDOM: + + def __init__(self): + JS(""" + + if (typeof window.DOMParser != "undefined") { + this.parseXml = function(xmlStr) { + return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); + }; + } else if (typeof window.ActiveXObject != "undefined" && + new window.ActiveXObject("Microsoft.XMLDOM")) { + this.parseXml = function(xmlStr) { + var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = "false"; + xmlDoc.loadXML(xmlStr); + return xmlDoc; + }; + } else { + throw new Error("No XML parser found"); + } + """) + + def parseString(self, xml): + return Document(self.parseXml(xml)) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/notification.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,122 @@ +from __pyjamas__ import JS, wnd +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ + +from pyjamas import Window +from pyjamas.Timer import Timer + +import dialog + +TIMER_DELAY = 5000 + + +class Notification(object): + """ + If the browser supports it, the user allowed it to and the tab is in the + background, send desktop notifications on messages. + + Requires both Web Notifications and Page Visibility API. + """ + + def __init__(self): + self.enabled = False + user_agent = None + notif_permission = None + JS(""" + if (!('hidden' in document)) + document.hidden = false; + + user_agent = navigator.userAgent + + if (!('Notification' in window)) + return; + + notif_permission = Notification.permission + + if (Notification.permission === 'granted') + this.enabled = true; + + else if (Notification.permission === 'default') { + Notification.requestPermission(function(permission){ + if (permission !== 'granted') + return; + + self.enabled = true; //need to use self instead of this + }); + } + """) + + if "Chrome" in user_agent and notif_permission not in ['granted', 'denied']: + self.user_agent = user_agent + self._installChromiumWorkaround() + + wnd().onfocus = self.onFocus + # wnd().onblur = self.onBlur + self._notif_count = 0 + self._orig_title = Window.getTitle() + + def _installChromiumWorkaround(self): + # XXX: Workaround for Chromium behaviour, it's doens't manage requestPermission on onLoad event + # see https://code.google.com/p/chromium/issues/detail?id=274284 + # FIXME: need to be removed if Chromium behaviour changes + try: + version_full = [s for s in self.user_agent.split() if "Chrome" in s][0].split('/')[1] + version = int(version_full.split('.')[0]) + except (IndexError, ValueError): + log.warning("Can't find Chromium version") + version = 0 + log.info("Chromium version: %d" % (version,)) + if version < 22: + log.info("Notification use the old prefixed version or are unmanaged") + return + if version < 32: + dialog.InfoDialog(_("Notifications activation for Chromium"), _('You need to activate notifications manually for your Chromium version.<br/>To activate notifications, click on the favicon on the left of the address bar')).show() + return + + log.info("==> Installing Chromium notifications request workaround <==") + self._old_click = wnd().onclick + wnd().onclick = self._chromiumWorkaround + + def _chromiumWorkaround(self): + log.info("Activating workaround") + JS(""" + Notification.requestPermission(function(permission){ + if (permission !== 'granted') + return; + self.enabled = true; //need to use self instead of this + }); + """) + wnd().onclick = self._old_click + + def onFocus(self): + Window.setTitle(self._orig_title) + self._notif_count = 0 + + # def onBlur(self): + # pass + + def isHidden(self): + JS("""return document.hidden;""") + + def _notify(self, title, body, icon): + if not self.enabled: + return + notification = None + JS(""" + notification = new Notification(title, {body: body, icon: icon}); + // Probably won’t work, but it doesn’t hurt to try. + notification.addEventListener('click', function() { + window.focus(); + }); + """) + notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close()) + + def highlightTab(self): + self._notif_count += 1 + Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count)) + + def notify(self, title, body, icon='/media/icons/apps/48/sat.png'): + if self.isHidden(): + self._notify(title, body, icon) + self.highlightTab()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/panels.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,1409 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) + +from sat_frontends.tools.strings import addURLToText +from sat_frontends.tools.games import SYMBOLS +from sat.core.i18n import _ + +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.FlowPanel import FlowPanel +from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.Timer import Timer +from pyjamas import DOM +from pyjamas import Window +from __pyjamas__ import doc + +from datetime import datetime +from time import time + +from jid import JID +from html_tools import html_sanitize +from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor +from card_game import CardPanel +from radiocol import RadioColPanel +from menu import Menu +import dialog +import base_widget +import richtext +import contact +from constants import Const as C +from plugin_xep_0085 import ChatStateMachine + + +# TODO: at some point we should decide which behaviors to keep and remove these two constants +TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button +NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry + + +class UniBoxPanel(HorizontalPanel): + """Panel containing the UniBox""" + + def __init__(self, host): + HorizontalPanel.__init__(self) + self.host = host + self.setStyleName('uniBoxPanel') + self.unibox = None + + def refresh(self): + """Enable or disable this panel. Contained widgets are created when necessary.""" + enable = self.host.params_ui['unibox']['value'] + self.setVisible(enable) + if enable and not self.unibox: + 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.openRichMessageEditor) + self.unibox.addKey("@@: ") + self.unibox.onSelectedChange(self.host.getSelected()) + + def openRichMessageEditor(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 afterEditCb(): + 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.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) + 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 timer: 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') + 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: + log.warning("trying to remove an unknown key") + + 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: + log.error("Unknown target") + target_hook, _type, msg = getSelectedOrStatus() + + return (target_hook, _type, msg) + + def onKeyPress(self, sender, keycode, modifiers): + _txt = self.getText() + target_hook, type_, msg = self._getTarget(_txt) + + if keycode == KEY_ENTER: + if _txt: + if target_hook: + parsed_txt, data = target_hook + self.host.send([(type_, data)], parsed_txt) + self.host._updateInputHistory(_txt) + self.setText('') + self.host.showWarning(None, None) + else: + self.host.showWarning(type_, msg) + 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 WarningPopup(): + + def __init__(self): + self._popup = None + self._timer = Timer(notify=self._timeCb) + + def showWarning(self, type_=None, msg=None, duration=2000): + """Display a popup information message, e.g. to notify the recipient of a message being composed. + If type_ is None, a popup being currently displayed will be hidden. + @type_: a type determining the CSS style to be applied (see __showWarning) + @msg: message to be displayed + """ + if type_ is None: + self.__removeWarning() + return + if not self._popup: + self.__showWarning(type_, msg) + elif (type_, msg) != self._popup.target_data: + self._timeCb(None) # we remove the popup + self.__showWarning(type_, msg) + + self._timer.schedule(duration) + + def __showWarning(self, type_, msg): + """Display a popup information message, e.g. to notify the recipient of a message being composed. + @type_: a type determining the CSS style to be applied. For now the defined styles are + "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". + @msg: message to be displayed + """ + if type_ == "NONE": + return + if not msg: + log.warning("no msg set uniBox warning") + return + if type_ == "PUBLIC": + style = "targetPublic" + elif type_ == "GROUP": + style = "targetGroup" + elif type_ == "STATUS": + style = "targetStatus" + elif type_ == "ONE2ONE": + style = "targetOne2One" + else: + log.error("unknown message type") + return + contents = HTML(msg) + + self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) + self._popup.target_data = (type_, msg) + 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 __removeWarning(self): + """Remove the popup""" + self._timeCb(None) + + +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.title = data.get('title', '') + self.title_xhtml = data.get('title_xhtml', '') + self.content = data.get('content', '') + self.content_xhtml = data.get('content_xhtml', '') + self.author = data['author'] + self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here + self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here + self.service = data.get('service', '') + self.node = data.get('node', '') + self.comments = data.get('comments', False) + self.comments_service = data.get('comments_service', '') + self.comments_node = data.get('comments_node', '') + + +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. + """ + self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) + for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', + 'author', 'updated', 'published', 'comments', 'service', 'node', + 'comments_service', 'comments_node']: + getter = lambda attr: lambda inst: getattr(inst._base_item, attr) + setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) + setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) + + 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) + + if TOGGLE_EDITION_USE_ICON: + self.entry_dialog = HorizontalPanel() + else: + self.entry_dialog = VerticalPanel() + 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.service, self.node, self.id) + self.__setContent() + + def __setContent(self): + """Actually set the entry content (header, icons, bubble...)""" + self.delete_label = self.update_label = self.comment_label = None + self.bubble = self._current_comment = None + self.__setHeader() + self.__setBubble() + 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.comments: + self.comment_label = addIcon(u"↶", "Comment this message") + self.comment_label.setStyleName('mb_entry_action_larger') + is_publisher = self.author == self._blog_panel.host.whoami.bare + if is_publisher: + self.update_label = addIcon(u"✍", "Edit this message") + if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): + self.delete_label = addIcon(u"✗", "Delete 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: + try: # prevent re-selection of the main entry after a comment has been focused + if self.__ignoreNextEvent: + self.__ignoreNextEvent = False + return + except AttributeError: + pass + self._blog_panel.setSelectedEntry(self) + elif sender == self.delete_label: + self._delete() + elif sender == self.update_label: + self.edit(True) + elif sender == self.comment_label: + self.__ignoreNextEvent = True + self._comment() + + def __modifiedCb(self, content): + """Send the new content to the backend + @return: False to restore the original content if a deletion has been cancelled + """ + if not content['text']: # previous content has been emptied + self._delete(True) + return False + extra = {'published': str(self.published)} + if isinstance(self.bubble, richtext.RichTextEditor): + # TODO: if the user change his parameters after the message edition started, + # the message syntax could be different then the current syntax: pass the + # message syntax in extra for the frontend to use it instead of current syntax. + extra.update({'content_rich': content['text'], 'title': content['title']}) + if self.empty: + if self.type == 'main_item': + self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) + else: + self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) + else: + self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) + return True + + def __afterEditCb(self, content): + """Remove the entry if it was an empty one (used for creating a new blog post). + Data for the actual new blog post will be received from the bridge""" + if self.empty: + self._blog_panel.removeEntry(self.type, self.id) + if self.type == 'main_item': # restore the "New message" button + self._blog_panel.refresh() + else: # allow to create a new comment + self._parent_entry._current_comment = None + self.entry_dialog.setWidth('auto') + try: + self.toggle_syntax_button.removeFromParent() + except TypeError: + pass + + def __setBubble(self, edit=False): + """Set the bubble displaying the initial content.""" + content = {'text': self.content_xhtml if self.content_xhtml else self.content, + 'title': self.title_xhtml if self.title_xhtml else self.title} + if self.content_xhtml: + content.update({'syntax': C.SYNTAX_XHTML}) + if self.author != self._blog_panel.host.whoami.bare: + options = ['read_only'] + else: + options = [] if self.empty else ['update_msg'] + self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) + else: # assume raw text message have no title + self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) + self.bubble.addStyleName("bubble") + try: + self.toggle_syntax_button.removeFromParent() + except TypeError: + pass + self.entry_dialog.add(self.bubble) + self.edit(edit) + self.bubble.addEditListener(self.__showWarning) + + def __showWarning(self, sender, keycode): + if keycode == KEY_ENTER: + self._blog_panel.host.showWarning(None, None) + else: + self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) + + def _delete(self, empty=False): + """Ask confirmation for deletion. + @return: False if the deletion has been cancelled.""" + def confirm_cb(answer): + if answer: + self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) + else: # restore the text if it has been emptied during the edition + self.bubble.setContent(self.bubble._original_content) + + if self.empty: + text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") + dialog.InfoDialog(_("Information"), text).show() + return + text = "" + if empty: + text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>") + target = _('message and all its comments') if self.comments else _('comment') + text += _("Do you really want to delete this %s?") % target + dialog.ConfirmDialog(confirm_cb, text=text).show() + + def _comment(self): + """Add an empty entry for a new comment""" + if self._current_comment: + self._current_comment.bubble.setFocus(True) + self._blog_panel.setSelectedEntry(self._current_comment) + return + data = {'id': str(time()), + 'new': True, + 'type': 'comment', + 'author': self._blog_panel.host.whoami.bare, + 'service': self.comments_service, + 'node': self.comments_node + } + entry = self._blog_panel.addEntry(data) + if entry is None: + log.info("The entry of id %s can not be commented" % self.id) + return + entry._parent_entry = self + self._current_comment = entry + self.edit(True, entry) + self._blog_panel.setSelectedEntry(entry) + + def edit(self, edit, entry=None): + """Toggle the bubble between display and edit mode + @edit: boolean value + @entry: MicroblogEntry instance, or None to use self + """ + if entry is None: + entry = self + try: + entry.toggle_syntax_button.removeFromParent() + except TypeError: + pass + entry.bubble.edit(edit) + if edit: + if isinstance(entry.bubble, richtext.RichTextEditor): + image = '<a class="richTextIcon">A</a>' + html = '<a style="color: blue;">raw text</a>' + title = _('Switch to raw text edition') + else: + image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>' + html = '<a style="color: blue;">rich text</a>' + title = _('Switch to rich text edition') + if TOGGLE_EDITION_USE_ICON: + entry.entry_dialog.setWidth('80%') + entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) + entry.toggle_syntax_button.setTitle(title) + entry.entry_dialog.add(entry.toggle_syntax_button) + else: + entry.toggle_syntax_button = HTML(html) + entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) + entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') + entry.entry_dialog.add(entry.toggle_syntax_button) + entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS + entry.toggle_syntax_button.setStyleAttribute('left', '-20px') + + def toggleContentSyntax(self): + """Toggle the editor between raw and rich text""" + original_content = self.bubble.getOriginalContent() + rich = not isinstance(self.bubble, richtext.RichTextEditor) + if rich: + original_content['syntax'] = C.SYNTAX_XHTML + + def setBubble(text): + self.content = text + self.content_xhtml = text if rich else '' + self.content_title = self.content_title_xhtml = '' + self.bubble.removeFromParent() + self.__setBubble(True) + self.bubble.setOriginalContent(original_content) + if rich: + self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble + + text = self.bubble.getContent()['text'] + if not text: + setBubble(' ') # something different than empty string is needed to initialize the rich text editor + return + if not rich: + def confirm_cb(answer): + if answer: + self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) + dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() + else: + self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) + + +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) + + def refresh(self): + """Refresh the display of this widget. If the unibox is disabled, + display the 'New message' button or an empty bubble on top of the panel""" + if hasattr(self, 'new_button'): + self.new_button.setVisible(self.host.uni_box is None) + return + if self.host.uni_box is None: + def addBox(): + if hasattr(self, 'new_button'): + self.new_button.setVisible(False) + data = {'id': str(time()), + 'new': True, + 'author': self.host.whoami.bare, + } + entry = self.addEntry(data) + entry.edit(True) + if NEW_MESSAGE_USE_BUTTON: + self.new_button = Button("New message", listener=addBox) + self.new_button.setStyleName("microblogNewButton") + self.vpanel.insert(self.new_button, 0) + else: + addBox() + + @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) + _new_panel.refresh() + 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, comment=None): + """ + @param comment: True if the composed message is a comment. If None, consider we are + composing from the unibox and guess the message type from self.selected_entry + @return: a couple (type, msg) for calling self.host.showWarning""" + if comment is None: # composing from the unibox + if self.selected_entry and not self.selected_entry.comments: + log.error("an item without comment is selected") + return ("NONE", None) + comment = self.selected_entry is not None + if comment: + 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_url = self.selected_entry.comments + if not comments_url: + raise Exception("ERROR: the comments URL is empty") + target = ("COMMENT", comments_url) + 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 + """ + log.debug("Massive insertion of %d microblogs" % len(mblogs)) + for publisher in mblogs: + log.debug("adding blogs for [%s]" % publisher) + for mblog in mblogs[publisher]: + if not "content" in mblog: + log.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: + log.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": + comments_hash = (_entry.service, _entry.node) + if not comments_hash in self.comments: + # The comments node is not known in this panel + return None + parent = self.comments[comments_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.id == _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 comments service/node as a reference + comments_hash = (_entry.comments_service, _entry.comments_node) + self.comments[comments_hash] = _entry + self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_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.id == 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.id == id_: + comment.removeFromParent() + self.selected_entry = None + break + + def setSelectedEntry(self, entry): + try: + self.vpanel.getParent().ensureVisible(entry) # scroll to the clicked entry + except AttributeError: + log.warning("FIXME: MicroblogPanel.vpanel should be wrapped in a ScrollPanel!") + removeStyle = lambda entry: entry.removeStyleName('selected_entry') + if not self.host.uni_box or not entry.comments: + entry.addStyleName('selected_entry') # blink the clicked entry + clicked_entry = entry # entry may be None when the timer is done + Timer(500, lambda timer: removeStyle(clicked_entry)) + if not self.host.uni_box: + return # unibox is disabled + # from here the previous behavior (toggle main item selection) is conserved + entry = entry if entry.comments else None + if self.selected_entry == entry: + entry = None + if self.selected_entry: + removeStyle(self.selected_entry) + if entry: + log.debug("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(HTMLTextEditor): + + EMPTY_STATUS = '<click to set a status>' + + def __init__(self, host, status=''): + self.host = host + modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True + HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True}) + self.edit(False) + self.setStyleName('statusPanel') + + @property + def status(self): + return self._original_content['text'] + + def __cleanContent(self, content): + status = content['text'] + if status == self.EMPTY_STATUS or status in C.PRESENCE.values(): + content['text'] = '' + return content + + def getContent(self): + return self.__cleanContent(HTMLTextEditor.getContent(self)) + + def setContent(self, content): + content = self.__cleanContent(content) + BaseTextEditor.setContent(self, content) + + def setDisplayContent(self): + status = self._original_content['text'] + try: + presence = self.host.status_panel.presence + except AttributeError: # during initialization + presence = None + if not status: + if presence and presence in C.PRESENCE: + status = C.PRESENCE[presence] + else: + status = self.EMPTY_STATUS + self.display.setHTML(addURLToText(status)) + + +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 C.PRESENCE.keys(): + entries.update({C.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) + + self.status_panel.edit(False) + + ClickHandler.__init__(self) + self.addClickListener(self) + + @property + def presence(self): + return self._presence + + @property + def status(self): + return self.status_panel._original_content['text'] + + def setPresence(self, presence): + self._presence = presence + contact.setPresenceStyle(self.presence_button, self._presence) + + def setStatus(self, status): + self.status_panel.setContent({'text': status}) + self.status_panel.setDisplayContent() + + 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 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, title=target.bare, selectable=True) + self.vpanel = VerticalPanel() + self.vpanel.setSize('100%', '100%') + self.type = type_ + self.nick = None + if not target: + log.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._state = None + + @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) + _new_panel.refresh() + return _new_panel + + def refresh(self): + """Refresh the display of this widget. If the unibox is disabled, + add a message box at the bottom of the panel""" + self.host.contact_panel.setContactMessageWaiting(self.target.bare, False) + self.content_scroll.scrollToBottom() + + enable_box = self.host.uni_box is None + if hasattr(self, 'message_box'): + self.message_box.setVisible(enable_box) + return + if enable_box: + self.message_box = MessageBox(self.host) + self.message_box.onSelectedChange(self) + self.vpanel.add(self.message_box) + + 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 changeUserNick(self, old_nick, new_nick): + assert(self.type == "group") + self.occupants_list.removeOccupant(old_nick) + self.occupants_list.addOccupant(new_nick) + self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_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', link_cb=None): + """Print general info + @param msg: message to print + @param type_: one of: + "normal": general info like "toto has joined the room" + "link": general info that is clickable like "click here to join the main room" + "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" + @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link' + """ + _wid = HTML(msg) if type_ == 'link' else Label(msg) + if type_ == 'normal': + _wid.setStyleName('chatTextInfo') + elif type_ == 'link': + _wid.setStyleName('chatTextInfo-link') + if link_cb: + _wid.addClickListener(link_cb) + 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.updateSpecials(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 + log.info("%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 + + def setState(self, state, nick=None): + """Set the chat state (XEP-0085) of the contact. Leave nick to None + to set the state for a one2one conversation, or give a nickname or + C.ALL_OCCUPANTS to set the state of a participant within a MUC. + @param state: the new chat state + @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS + """ + if nick: + assert(self.type == 'group') + occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick] + for occupant in occupants: + self.occupants_list.occupants_list[occupant].setState(state) + else: + assert(self.type == 'one2one') + self._state = state + self.refreshTitle() + self.state_machine.started = not not state # start to send "composing" state from now + + def refreshTitle(self): + """Refresh the title of this ChatPanel dialog""" + if self._state: + self.setTitle(self.target.bare + " (" + self._state + ")") + else: + self.setTitle(self.target.bare) + + +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) + + # 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) + + self.header = AbsolutePanel() + self.header.add(self.menu) + self.header.add(self.unibox_panel) + self.header.add(self.host.status_panel) + self.header.setStyleName('header') + self.add(self.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 refresh(self): + """Refresh the main panel""" + self.unibox_panel.refresh()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/plugin_xep_0085.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT plugin for Chat State Notifications Protocol (xep-0085) +# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.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/>. + +from pyjamas.Timer import Timer + + +# Copy of the map from sat/src/plugins/plugin_xep_0085 +TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120}, + "inactive": {"next_state": "gone", "delay": 480}, + "gone": {"next_state": "", "delay": 0}, + "composing": {"next_state": "paused", "delay": 30}, + "paused": {"next_state": "inactive", "delay": 450} + } + + +class ChatStateMachine: + """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085 + which manage a timer on the web browser and keep it synchronized with the timer that runs + on the backend. This is only needed to avoid calling the bridge method chatStateComposing + too often ; accuracy is not needed so we can ignore the delay of the communication between + the web browser and the backend (the timer on the web browser always starts a bit before). + /!\ Keep this file up to date if you modify the one in the sat plugins directory. + """ + def __init__(self, host, target_s): + + self.host = host + self.target_s = target_s + self.started = False + self.state = None + self.timer = None + + def _onEvent(self, state): + """Pyjamas callback takes no extra argument so we need this trick""" + # Here we should check the value of the parameter "Send chat state notifications" + # but this costs two messages. It's even better to call chatStateComposing + # with a doubt, it will be checked by the back-end anyway before sending + # the actual notifications to the other client. + if state == "composing" and not self.started: + return + self.started = True + self.next_state = state + self.__onEvent(None) + + def __onEvent(self, timer): + # print "on event %s" % self.next_state + state = self.next_state + self.next_state = "" + if state != self.state and state == "composing": + self.host.bridge.call('chatStateComposing', None, self.target_s) + self.state = state + if not self.timer is None: + self.timer.cancel() + + if not state in TRANSITIONS: + return + if not "next_state" in TRANSITIONS[state]: + return + if not "delay" in TRANSITIONS[state]: + return + next_state = TRANSITIONS[state]["next_state"] + delay = TRANSITIONS[state]["delay"] + if next_state == "" or delay < 0: + return + self.next_state = next_state + # pyjamas timer in milliseconds + self.timer = Timer(delay * 1000, self.__onEvent)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/public/contrat_social.html Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,110 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html><head> + + <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type"> + <title>Salut Toi: Contrat Social</title> + + +</head><body> +Le projet Salut Toi est n d'un besoin de protection de nos +liberts, de notre vie prive et de notre indpendance. Il se veut +garant des droits et liberts qu'un utilisateur a vis vis de ses +propres informations, des informations numriques sur sa vie ou celles +de ses connaissances, des donnes qu'il manipule; et se veut galement +un point de contact humain, ne se substituant pas aux rapports rels, +mais au contraire les facilitant.<br> + +Salut Toi lutte et luttera toujours contre toute forme de main mise +sur les technologies par des intrts privs. Le rseau global doit +appartenir tous, et tre un point d'expression et de libert pour +l'Humanit.<br> + +<br> + + ce titre, Salut Toi et ceux qui y participent se basent sur un +contrat social, un engagement vis vis de ceux qui l'utilisent. Ce +contrat consiste en les points suivants:<br> + +<ul> + + <li>nous plaons la <span style="font-style: italic;">Libert</span> en tte de nos priorits: libert de +l'utilisateur, libert vis vis de ses donnes. Pour cela, Salut +Toi est un logiciel Libre - condition essentielle -, et son +infrastructure se base galement sur des logiciels Libres, c'est dire +des logiciels qui respectent ces 4 liberts fondamentales + <ul> + + <li>la libert d'excuter le programme, pour tous les usages,</li> + + </ul> + <ul> + + <li>la libert d'tudier le fonctionnement du programme et de +l'adapter ses besoins,</li> + + </ul> + <ul> + + <li>la libert de redistribuer des copies du programme,</li> + + </ul> + <ul> + + <li>la libert d'amliorer le programme et de distribuer ces +amliorations au public.<br> +</li> + + </ul> +</li> + + + + + +Vous avez ainsi la possibilit d'installer votre propre version de +Salut Toi sur votre propre machine, d'en vrifier - et de +comprendre - ainsi son fonctionnement, de l'adapter vos besoins, d'en +faire profiter vos amis. + + <li>Les informations vous concernant vous appartiennent, et nous +n'aurons pas la prtention - et l'indcence ! - de considrer le +contenu que vous produisez ou faites circuler via Salut Toi comme +nous appartenant. De mme, nous nous engageons ne jamais faire de +profit en revendant vos informations personnelles.</li> + <li>Nous incitons fortement la <span style="text-decoration: underline;">dcentralisation gnralise</span>. +Salut Toi tant bas sur un protocole dcentralis (XMPP), il l'est +lui-mme par nature. La dcentralisation est essentielle pour une +meilleure protection de vos informations, une meilleure rsistance la +censure ou aux pannes, et pour viter les drives autoritaires.</li> + <li>Luttant contre les tentatives de contrle priv et les abus +commerciaux du rseau global, et afin de garder notre indpendance, +nous nous refusons toute forme de publicit: vous ne verrez <span style="font-weight: bold;">jamais</span> +de forme de rclame commerciale de notre fait.</li> + <li>L'<span style="font-style: italic;">galit</span> des utilisateurs est essentielle pour nous, nous +refusons toute forme de discrimination, que ce soit pour une zone +gographique, une catgorie de la population, ou tout autre raison.</li> + <li>Nous ferons tout notre possible pour lutter contre toute +tentative de censure. Le rseau global doit tre un moyen d'expression +pour tous.</li> + <li>Nous refusons toute ide d'autorit absolue en ce qui concerne +les dcisions prises pour Salut Toi et son fonctionnement, et le +choix de la dcentralisation et l'utilisation de logiciel Libre permet +de lutter contre toute forme de hirarchie.</li> + + <li>L'ide de <span style="font-style: italic;">Fraternit</span> est essentielle, aussi: + <ul> + <li>nous ferons notre +possible pour aider les utilisateurs, quel que soit leur niveau</li> + <li>de mme, des efforts seront fait quant +l'accessibilit aux personnes victimes d'un handicap</li> + <li> Salut Toi , +XMPP, et les technologies utilises facilitent les changes +lectroniques, mais nous dsirons mettre l'accent sur les rencontres +relles et humaines: nous favoriserons toujours le rel sur le virtuel.</li> + </ul> +</li> + + +</ul> + +</body></html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/public/libervia.css Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,1580 @@ +/* +Libervia: a Salut à Toi frontend +Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> +Copyright (C) 2011 Adrien Vigneron <adrienvigneron@mailoo.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/>. +*/ + + +/* + * CSS Reset: see http://pyjs.org/wiki/csshellandhowtodealwithit/ + */ + +/* reset/default styles */ + +html, body, div, span, applet, object, iframe, +p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, dl, dt, dd, li, +fieldset, form, label, legend, table, caption, +tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; + color: #444; +} + +/* styles for displaying rich text - START */ +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + border: 0; + outline: 0; + vertical-align: baseline; + background: transparent; + color: #444; + border-bottom: 1px solid rgb(170, 170, 170); + margin-bottom: 0.6em; +} +ol, ul { + margin: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; + color: #444; +} +a:link { + color: blue; +} +.bubble p { + margin: 0.4em 0em; +} +.bubble img { + /* /!\ setting a max-width percentage value affects the toolbar icons */ + max-width: 600px; +} + +/* styles for displaying rich text - END */ + +blockquote, q { quotes: none; } + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +:focus { outline: 0; } +ins { text-decoration: none; } +del { text-decoration: line-through; } + +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* pyjamas iframe hide */ +iframe { position: absolute; } + + +html, body { + width: 100%; + height: 100%; + min-height: 100%; + +} + +body { + line-height: 1em; + font-size: 1em; + overflow: auto; + +} + +.scrollpanel { + margin-bottom: -10000px; + +} + +.iescrollpanelfix { + position: relative; + top: 100%; + margin-bottom: -10000px; + +} + +/* undo part of the above (non-IE) */ +html>body .iescrollpanelfix { position: static; } + +/* CSS Reset END */ + +body { + background-color: #fff; + font: normal 0.8em/1.5em Arial, Helvetica, sans-serif; +} + +.header { + background-color: #eee; + border-bottom: 1px solid #ddd; +} + +/* Misc Pyjamas stuff */ + +.menuContainer { + margin: 0 32px 0 20px; +} + +.gwt-MenuBar,.gwt-MenuBar-horizontal { + /*background-color: #01FF78; + border: 1px solid #87B3FF; + cursor: default;*/ + width: 100%; + height: 28px; + margin: 0; + padding: 5px 5px 0 5px; + line-height: 100%; + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); + border: 1px solid #ddd; + border-radius: 0 0 1em 1em; + -webkit-border-radius: 0 0 1em 1em; + -moz-border-radius: 0 0 1em 1em; + background-color: #222; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’); + background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); + background: -moz-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(#444444,#222222); + display: inline-block; +} + +.gwt-MenuBar-horizontal .gwt-MenuItem { + text-decoration: none; + font-weight: bold; + height: 100%; + color: #e7e5e5; + padding: 3px 15px; + /*display: block;*/ + border-radius: 1em 1em 1em 1em; + -webkit-border-radius: 1em 1em 1em 1em; + -moz-border-radius: 1em 1em 1em 1em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -o-transition: color 0.2s linear; +} + +.gwt-MenuItem img { + padding-right: 2px; +} + +.gwt-MenuBar-horizontal .gwt-MenuItem-selected { + background-color: #eee; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′); + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -moz-linear-gradient(top, #eee, #aaa); + background-image: -o-linear-gradient(#eee,#aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + cursor: pointer; +} + +.menuSeparator { + width: 100%; +} + +.menuSeparator.gwt-MenuItem-selected { + border: 0; + background: inherit; + cursor: default; +} + +.gwt-MenuBar { + background-color: #fff; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’); + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); + background: -moz-linear-gradient(top, #fff, #ccc); + background-image: -o-linear-gradient(#fff,#ccc); + /*display: none;*/ + height: 100%; + min-width: 148px; + margin: 0; + padding: 0; + /*min-width: 148px; + top: 28px;*/ + border: solid 1px #aaa; + -webkit-border-radius: 0 0 10px 10px; + -moz-border-radius: 0 0 10px 10px; + border-radius: 0 0 10px 10px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, .3); + box-shadow: 0 1px 3px rgba(0, 0, 0, .3); +} + +.gwt-MenuBar table { + width: 100%; + display: inline-table; +} + +.gwt-MenuBar .gwt-MenuItem { + padding: 8px 15px; +} + + +.gwt-MenuBar .gwt-MenuItem-selected { + background: #cf2828 !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’) !important; + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; + background: -moz-linear-gradient(top, #cf2828, #981a1a) !important; + background-image: -o-linear-gradient(#cf2828,#981a1a) !important; + color: #fff !important; + -webkit-border-radius: 0 0 0 0; + -moz-border-radius: 0 0 0 0; + border-radius: 0 0 0 0; + text-shadow: 0 1px 1px rgba(0, 0, 0, .1); + transition: color 0.2s linear; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -o-transition: color 0.2s linear; + cursor: pointer; +} + +/*.menuLastPopup div tr:first-child td{ + border-radius: 0 0 9px 9px !important; + -webkit-border-radius: 0 0 9px 9px !important; + -moz-border-radius: 0 0 9px 9px !important; +}*/ + +.gwt-MenuBar tr:last-child td { + border-radius: 0 0 9px 9px !important; + -webkit-border-radius: 0 0 9px 9px !important; + -moz-border-radius: 0 0 9px 9px !important; +} + + +.menuLastPopup .gwt-MenuBar { + border-top-right-radius: 9px 9px 9px 9px; + -webkit-border-top-right-radius: 9px 9px 9px 9px; + -moz-border-top-right-radius: 9px 9px 9px 9px; +} + +.gwt-AutoCompleteTextBox { + width: 80%; + border: 1px solid #87B3FF; + margin-top: 20px; +} +.gwt-DialogBox { + padding: 10px; + border: 1px solid #aaa; + background-color: #fff; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#fff’, endColorstr=’#ccc’); + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); + background: -moz-linear-gradient(top, #fff, #ccc); + background-image: -o-linear-gradient(#fff,#ccc); + border-radius: 9px 9px 9px 9px; + -webkit-border-radius: 9px 9px 9px 9px; + -moz-border-radius: 9px 9px 9px 9px; + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); +} + +.gwt-DialogBox .Caption { + height: 20px; + font-size: 1.3em !important; + background-color: #cf2828; + background: #cf2828 !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828’, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)) !important; + background: -moz-linear-gradient(top, #cf2828, #981a1a) !important; + background-image: -o-linear-gradient(#cf2828,#981a1a); + color: #fff; + padding: 3px 3px 4px 3px; + margin: -10px; + margin-bottom: 5px; + font-weight: bold; + cursor: default; + text-align: center; + border-radius: 7px 7px 0 0; + -webkit-border-radius: 7px 7px 0 0; + -moz-border-radius: 7px 7px 0 0; +} + +/*DIALOG: button, listbox, textbox, label */ + +.gwt-DialogBox .gwt-button { + background-color: #ccc; + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444͸, endColorstr=’#222’); + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); + background: -moz-linear-gradient(top, #444, #222); + background-image: -o-linear-gradient(#444,#222); + text-shadow: 1px 1px 1px rgba(0,0,0,0.2); + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + color: #fff; + font-weight: bold; + font-size: 1em; + border: none; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -o-transition: color 0.2s linear; +} + +.gwt-DialogBox .gwt-button:hover { + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); + color: #fff; + text-shadow: 1px 1px 1px rgba(0,0,0,0.25); +} + +.gwt-DialogBox .gwt-TextBox { + background-color: #fff; + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + box-shadow:inset 0px 1px 4px #000; + -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); + -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + color: #444; + font-size: 1em; + border: none; +} + +.gwt-DialogBox .gwt-ListBox { + overflow: auto; + width: 100%; + background-color: #fff; + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + box-shadow:inset 0px 1px 4px #000; + -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); + -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.6); + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + color: #444; + font-size: 1em; + border: none; +} + +.gwt-DialogBox .gwt-Label { + margin-top: 13px; +} + +/* Custom Dialogs */ + +.formWarning { /* used when a form is not valid and must be corrected before submission */ + font-weight: bold; + color: red !important; +} + +.contactsChooser { + text-align: center; + margin:auto; + cursor: pointer; +} + +.infoDialogBody { + width: 100%; + height: 100% +} +/* Contact List */ + +div.contactBox { + width: 100%; + /* We want the contact panel to not use all the available height when displayed + in the unibox panel (grey part), because the dialogs panels (white part) should + still be visible. The setting max-height: fit-content would be appropriate here + but it doesn't work with firefox 24.0. TODO: check if the current setting works + with other browsers... the panel should of course not be displayed on 100px + but exactly fit the contacts box. + */ + max-height: 100px; +} + +.contactTitle { + color: #cf2828; + font-size: 1.7em; + text-indent: 5px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + width: 200px; + height: 30px; +} + +.contactsSwitch { + /* Button used to switch contacts panel */ + background: none; + border: 0; + padding: 0; + font-size: large; +} + +.groupList { + width: 100%; +} + +.groupList tr:first-child td { + padding-top: 10px; +} + +.group { + padding: 2px 15px; + margin: 5px; + display: inline-block; + text-decoration: none; + font-weight: bold; + color: #e7e5e5; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + border-radius: 1em 1em 1em 1em; + -webkit-border-radius: 1em 1em 1em 1em; + -moz-border-radius: 1em 1em 1em 1em; + background-color: #eee; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa͸); + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -moz-linear-gradient(top, #eee, #aaa); + background-image: -o-linear-gradient(#eee,#aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + box-shadow: 0px 1px 1px #000; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6); +} + +div.group:hover { + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); + -webkit-transition: color 0.1s linear; + -moz-transition: color 0.1s linear; + -o-transition: color 0.1s linear; +} +.contact { + font-size: 1em; + margin-top: 3px; + padding: 3px 10px 3px 10px; +} + +.contact-menu-selected { + font-size: 1em; + margin-top: 3px; + padding: 3px 10px 3px 10px; + border-radius: 5px; + background-color: rgb(175, 175, 175); +} + +/* START - contact presence status */ +.contact-connected { + color: #3c7e0c; + font-weight: bold; +} +.contact-unavailable { +} +.contact-chat { + color: #3c7e0c; + font-weight: bold; +} +.contact-away { + color: brown; + font-weight: bold; +} +.contact-dnd { + color: red; + font-weight: bold; +} +.contact-xa { + color: red; + font-weight: bold; +} +/* END - contact presence status */ + +.selected { + color: #fff; + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); + border-radius: 1em 1em 1em 1em; + -webkit-border-radius: 1em 1em 1em 1em; + -moz-border-radius: 1em 1em 1em 1em; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -o-transition: color 0.2s linear; +} + +.messageBox { + width: 100%; + padding: 5px; + border: 1px solid #bbb; + color: #444; + background: #fff url('media/libervia/unibox_2.png') top bottom no-repeat; + box-shadow:inset 0 0 10px #ddd; + -webkit-box-shadow:inset 0 0 10px #ddd; + -moz-box-shadow:inset 0 0 10px #ddd; + border-radius: 0px 0px 10px 10px; + height: 25px; + margin: 0px; +} + +/* UniBox & Status */ + +.uniBoxPanel { + margin: 15px 22px 0 22px; +} + +.uniBox { + width: 100%; + height: 45px; + padding: 5px; + border: 1px solid #bbb; + color: #444; + background: #fff url('media/libervia/unibox_2.png') top right no-repeat; + box-shadow:inset 0 0 10px #ddd; + -webkit-box-shadow:inset 0 0 10px #ddd; + -moz-box-shadow:inset 0 0 10px #ddd; +} + +.uniBoxButton { + width:30px; + height:45px; +} + +.statusPanel { + margin: auto; + text-align: center; + width: 100%; + padding: 5px 0px; + text-shadow: 0 -1px 1px rgba(255,255,255,0.25); + font-size: 1.2em; + background-color: #eee; + font-style: italic; + font-weight: bold; + color: #666; + cursor: pointer; +} + +.presence-button { + font-size: x-large; + padding-right: 5px; + cursor: pointer; +} + +/* RegisterBox */ + +.registerPanel_main button { + margin: 0; + padding: 0; + border: 0; +} + +.registerPanel_main div, .registerPanel_main button { + color: #fff; + text-decoration: none; +} + +.registerPanel_main{ + height: 100%; + border: 5px solid #222; + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +} + +.registerPanel_tabs .gwt-Label { + margin: 15px 7.5px 0px 7.5px; + cursor: pointer; + font-size: larger; +} + +.registerPanel_tabs .gwt-TabBarItem div { + color: #444; + padding: 5px 7.5px; + border-radius: 5px 5px 0px 5px; + box-shadow: inset 0px 0px 2px 1px #9F2828; +} + +.registerPanel_tabs .gwt-TabBarItem div:hover { + color: #fff; + box-shadow: inset 0px 0px 2px 2px #9F2828; +} + +.registerPanel_tabs .gwt-TabBarItem-selected div { + color: #fff; + box-shadow: inset 0px 0px 2px 2px #9F2828; +} + +.registerPanel_tabs .gwt-TabBarRest { + border-bottom: 1px #3F1818 dashed; +} + +.registerPanel_right_side { + background: #111 url('media/libervia/register_right.png'); + height: 100%; + width: 100%; +} +.registerPanel_content { + margin-left: 50px; + margin-top: 30px; +} + +.registerPanel_content div { + font-size: 1em; + margin-left: 10px; + margin-top: 15px; + font-style: bold; + color: #888; +} + +.registerPanel_content input { + height: 25px; + line-height: 25px; + width: 200px; + text-indent: 11px; + + background: #000; + color: #aaa; + border: 1px solid #222; + border-radius: 15px 15px 15px 15px; + -webkit-border-radius: 15px 15px 15px 15px; + -moz-border-radius: 15px 15px 15px 15px; +} + +.registerPanel_content input:focus { + border: 1px solid #444; +} + + +.registerPanel_content .button, .registerPanel_content .button:visited { + background: #222 url('media/libervia/gradient.png') repeat-x; + display: inline-block; + text-decoration: none; + border-radius: 6px 6px 6px 6px; + -moz-border-radius: 6px 6px 6px 6px; + -webkit-border-radius: 6px 6px 6px 6px; + -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.6); + -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.6); + border-bottom: 1px solid rgba(0,0,0,0.25); + cursor: pointer; + margin-top: 30px; +} + +/* Fix for Opera */ +.button, .button:visited { + border-radius: 6px 6px 6px 6px !important; +} + +.registerPanel_content .button:hover { background-color: #111; color: #fff; } +.registerPanel_content .button:active { top: 1px; } +.registerPanel_content .button, .registerPanel_content .button:visited { font-size: 1em; font-weight: bold; line-height: 1; text-shadow: 0 -1px 1px rgba(0,0,0,0.25); padding: 7px 10px 8px; } +.registerPanel_content .red.button, .registerPanel_content .red.button:visited { background-color: #000; } +.registerPanel_content .red.button:hover { background-color: #bc0000; } + +/* Widgets */ + +.widgetsPanel td { + vertical-align: top; +} + +.widgetsPanel > div > table { + border-collapse: separate !important; + border-spacing: 7px; +} + +.widgetHeader { + margin: auto; + height: 25px; + /*border: 1px solid #ddd;*/ + border-radius: 10px 10px 0 0; + -webkit-border-radius: 10px 10px 0 0; + -moz-border-radius: 10px 10px 0 0; + background-color: #222; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444͸, endColorstr=’#222’); + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); + background: -moz-linear-gradient(top, #444, #222); + background-image: -o-linear-gradient(#444,#222); +} + +.widgetHeader_title { + color: #fff; + font-weight: bold; + text-align: left; + text-indent: 15px; + margin-top: 4px; +} + +.widgetHeader_buttonsWrapper { + position: absolute; + top: 0; + height: 100%; + width: 100%; +} + +.widgetHeader_buttonGroup { + float: right; +} + +.widgetHeader_buttonGroup img { + background-color: transparent; + width: 25px; + height: 20px; + padding-top: 2px; + padding-bottom: 3px; + border-left: 1px solid #666; + border-top: 0; + border-radius: 0 10px 0 0; + -webkit-border-radius: 0 10px 0 0; + -moz-border-radius: 0 10px 0 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#555͸, endColorstr=’#333’); + background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333)); + background: -moz-linear-gradient(top, #555, #333); + background-image: -o-linear-gradient(#555,#333); +} + +.widgetHeader_closeButton { + border-radius: 0 10px 0 0 !important; + -webkit-border-radius: 0 10px 0 0 !important; + -moz-border-radius: 0 10px 0 0 !important; +} + +.widgetHeader_settingButton { + border-radius: 0 0 0 0 !important; + -webkit-border-radius: 0 0 0 0 !important; + -moz-border-radius: 0 0 0 0 !important; +} + +.widgetHeader_buttonGroup img:hover { + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); +} + +.widgetBody { + border-radius: 0 0 10px 10px; + -webkit-border-radius: 0 0 10px 10px; + -moz-border-radius: 0 0 10px 10px; + background-color: #fff; + min-width: 200px; + min-height: 150px; + box-shadow:inset 0px 0 1px #444; + -webkit-box-shadow:inset 0 0 1px #444; + -moz-box-shadow:inset 0 0 1px #444; +} + +/* BorderWidgets */ + +.bottomBorderWidget { + height: 10px !important; +} + +.leftBorderWidget, .rightBorderWidget { + width: 10px !important; +} + +/* Microblog */ + +.microblogPanel { +/* margin: auto; + width: 95% !important;*/ + width: 100%; +} + +.microblogNewButton { + width: 100%; + height: 35px; +} + +.subPanel { +} + +.subpanel .mb_entry { + padding-left: 65px; +} + +.mb_entry { + min-height: 64px; +} + +.mb_entry_header +{ + cursor: pointer; +} + +.selected_widget .selected_entry .mb_entry_header +{ + background: #cf2828; +} + +.mb_entry_author { + font-weight: bold; + padding-left: 5px; +} + +.mb_entry_avatar { + float: left; +} + +.mb_entry_avatar img { + width: 48px; + height: 48px; + padding: 8px; +} + +.mb_entry_dialog { + float: left; + min-height: 54px; + padding: 5px 20px 5px 20px; + border-collapse: separate; # for the bubble queue since the entry dialog is now a HorizontalPanel +} + +.bubble { + position: relative; + padding: 15px; + margin: 2px; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; + background: #EDEDED; + border-color: #C1C1C1; + border-width: 1px; + border-style: solid; + display: block; + border-collapse: separate; + min-height: 15px; # for the bubble queue to be aligned when the bubble is empty +} + +.bubble:after { + background: transparent url('media/libervia/bubble_after.png') top right no-repeat; + border: none; + content: ""; + position: absolute; + bottom: auto; + left: -20px; + top: 16px; + display: block; + height: 20; + width: 20; +} + +.bubble textarea{ + width: 100%; +} + +.mb_entry_timestamp { + font-style: italic; +} + +.mb_entry_actions { + float: right; + margin: 5px; + cursor: pointer; + font-size: large; +} + +.mb_entry_action_larger { + font-size: x-large; +} + +.mb_entry_toggle_syntax { + cursor: pointer; + text-align: right; + display: block; + position: relative; + top: -20px: + left: -20px; +} + +/* Chat & MUC Room */ + +.chatPanel { + height: 100%; + width: 100%; +} + +.chatPanel_body { + height: 100%; + width: 100%; +} + +.chatContent { + overflow: auto; + padding: 5px 15px 5px 15px; +} + +.chatText { + margin-top: 7px; +} + +.chatTextInfo { + font-weight: bold; + font-style: italic; +} + +.chatTextInfo-link { + font-weight: bold; + font-style: italic; + cursor: pointer; + display: inline; +} + +.chatArea { + height:100%; + width:100%; +} + +.chat_text_timestamp { + font-style: italic; + margin-right: -4px; + padding: 1px 3px 1px 3px; + -moz-border-radius: 15px 0 0 15px; + -webkit-border-radius: 15px 0 0 15px; + border-radius: 15px 0 0 15px; + background-color: #eee; + color: #888; + border: 1px solid #ddd; + border-right: none; +} + +.chat_text_nick { + font-weight: bold; + padding: 1px 3px 1px 3px; + -moz-border-radius: 0 15px 15px 0; + -webkit-border-radius: 10 15px 15px 0; + border-radius: 0 15px 15px 0; + background-color: #eee; + color: #b01e1e; + border: 1px solid #ddd; + border-left: none; +} + +.chat_text_msg { + white-space: pre-wrap; +} + +.chat_text_mymess { + color: #006600; +} + +.occupant { + margin-top: 10px; + margin-right: 4px; + min-width: 120px; + padding: 5px 15px 5px 15px; + font-weight: bold; + background-color: #eee; + border: 1px solid #ddd; + white-space: nowrap; +} + +.occupantsList { + border-right: 2px dotted #ddd; + margin-left: 5px; + margin-right: 10px; + height: 100%; +} + +/* Games */ + +.cardPanel { + background: #02FE03; + margin: 0 auto; +} + +.cardGamePlayerNick { + font-weight: bold; +} + +/* Radiocol */ + +.radiocolPanel { + +} + +.radiocol_metadata_lbl { + font-weight: bold; + padding-right: 5px; +} + +.radiocol_next_song { + margin-right: 5px; + font-style:italic; +} + +.radiocol_status { + margin-left: 10px; + margin-right: 10px; + font-weight: bold; + color: black; +} + +.radiocol_upload_status_ok { + margin-left: 10px; + margin-right: 10px; + font-weight: bold; + color: #28F215; +} + +.radiocol_upload_status_ko { + margin-left: 10px; + margin-right: 10px; + font-weight: bold; + color: #B80000; +} + +/* Drag and drop */ + +.dragover { + background: #cf2828 !important; + border-radius: 1em 1em 1em 1em !important; + -webkit-border-radius: 1em 1em 1em 1em !important; + -moz-border-radius: 1em 1em 1em 1em !important; +} + +.dragover .widgetHeader, .dragover .widgetBody, .dragover .widgetBody span, .dragover .widgetHeader img { + background: #cf2828 !important; +} + +.dragover.widgetHeader { + border-radius: 1em 1em 0 0 !important; + -webkit-border-radius: 1em 1em 0 0 !important; + -moz-border-radius: 1em 1em 0 0 !important; +} + +.dragover.widgetBody { + border-radius: 0 0 1em 1em !important; + -webkit-border-radius: 0 0 1em 1em !important; + -moz-border-radius: 0 0 1em 1em !important; +} + +/* Warning message */ + +.warningPopup { + font-size: 1em; + width: 100%; + height: 26px; + text-align: center; + padding: 5px 0; + border-bottom: 1px solid #444; + /*background-color: #fff; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’); + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); + background: -moz-linear-gradient(top, #fff, #ccc); + background-image: -o-linear-gradient(#fff,#ccc); */ + +} + +.warningTarget { + font-weight: bold; + +} + +.targetPublic { + background-color: red; /*#cf2828;*/ + /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); */ +} + +.targetGroup { + background-color: #00FFFB; + /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’68ba0f′, endColorstr=’#40700d’); + background: -webkit-gradient(linear, left top, left bottom, from(#68ba0f), to(#40700d)); + background: -moz-linear-gradient(top, #68ba0f, #40700d); + background-image: -o-linear-gradient(#68ba0f,#40700d); */ +} + +.targetOne2One { + background-color: #66FF00; + /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’); + background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); + background: -moz-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(#444444,#222222);*/ +} + +.targetStatus { + background-color: #fff; + /*filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’fff′, endColorstr=’#ccc’); + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ccc)); + background: -moz-linear-gradient(top, #fff, #ccc); + background-image: -o-linear-gradient(#fff,#ccc); */ +} + +/* Tab panel */ + +.liberviaTabPanel { +} + +.gwt-TabPanel { +} + +.gwt-TabPanelBottom { + height: 100%; +} + +.gwt-TabBar { + font-weight: bold; + text-decoration: none; + border-bottom: 3px solid #a01c1c; +} + +.mainTabPanel .gwt-TabBar { + z-index: 10; + position: fixed; + bottom: 0; + left: 0; +} + +.gwt-TabBar .gwt-TabBarFirst { + height: 100%; +} + +.gwt-TabBar .gwt-TabBarRest { +} + +.liberviaTabPanel .gwt-TabBar {; +} + +.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem { + cursor: pointer; + margin-right: 5px; +} + +.liberviaTabPanel .gwt-TabBarItem div { + color: #fff; +} + +.liberviaTabPanel .gwt-TabBarItem { + color: #444 !important; + background-color: #222; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#444′, endColorstr=’#222’); + background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222)); + background: -moz-linear-gradient(top, #444, #222); + background-image: -o-linear-gradient(#444,#222); + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + padding: 4px 15px 4px 15px; + border-radius: 1em 1em 0 0; + -webkit-border-radius: 1em 1em 0 0; + -moz-border-radius: 1em 1em 0 0; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.liberviaTabPanel .gwt-TabBarItem-selected { + color: #fff; + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + padding: 4px 15px 4px 15px; + border-radius: 1em 1em 0 0; + -webkit-border-radius: 1em 1em 0 0; + -moz-border-radius: 1em 1em 0 0; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); +} + +.liberviaTabPanel div.gwt-TabBarItem:hover { + color: #fff; + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828′, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + padding: 4px 15px 4px 15px; + border-radius: 1em 1em 0 0; + -webkit-border-radius: 1em 1em 0 0; + -moz-border-radius: 1em 1em 0 0; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); +} + +.liberviaTabPanel .gwt-TabBar .gwt-TabBarItem-selected { + cursor: default; +} + +.globalLeftArea { + margin-top: 9px; +} + + +/* Misc */ + +.selected_widget .widgetHeader { + /* this property is set when a widget is the current target of the uniBox + * (messages entered in unibox will be sent to this widget) + */ + background-color: #cf2828; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#cf2828͸, endColorstr=’#981a1a’); + background: -webkit-gradient(linear, left top, left bottom, from(#cf2828), to(#981a1a)); + background: -moz-linear-gradient(top, #cf2828, #981a1a); + background-image: -o-linear-gradient(#cf2828,#981a1a); +} + +.infoFrame { + position: relative; + width: 100%; + height: 100%; +} + +.marginAuto { + margin: auto; +} + +.transparent { + opacity: 0; +} + +/* URLs */ + +a.url { + color: blue; + text-decoration: none +} + +a:hover.url { + text-decoration: underline +} + +/* Rich Text/Message Editor */ + +.richTextEditor { +} + +.richTextEditor tbody { + width: 100%; + display: table; +} + +.richMessageEditor { + width: 100%; + margin: 9px 18px; +} + +.richTextTitle { + margin-bottom: 5px; +} + +.richTextTitle textarea { + height: 23px; + width: 99%; + margin: auto; + display: block; +} + +.richTextToolbar { + white-space: nowrap; + width: 100%; +} + +.richTextArea { + width: 100%; +} + +.richMessageArea { + width: 100%; + height: 250px; +} + +.richTextWysiwyg { + min-height: 50px; + background-color: white; + border: 1px solid #a0a0a0; + border-radius: 5px; + display: block; + font-size: larger; + white-space: pre; +} + +.richTextSyntaxLabel { + text-align: right; + margin: 14px 0px 0px 14px; + font-size: 12px; +} + +.richTextToolButton { + cursor: pointer; + width:26px; + height:26px; + vertical-align: middle; + margin: 2px 1px; + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + box-shadow: 0px 1px 4px #000; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.6); + border: none; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -o-transition: color 0.2s linear; +} + +.richTextIcon { + width:16px; + height:16px; + vertical-align: middle; +} + +/* Recipients panel */ + +.recipientButtonCell { + width:55px; +} + +.recipientTypeMenu { +} + +.recipientTypeItem { + cursor: pointer; + border-radius: 5px; + width: 50px; +} + +.recipientPanel { +} + +.recipientTextBox { + cursor: pointer; + width: auto; + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + box-shadow: inset 0px 1px 4px rgba(135, 179, 255, 0.6); + -webkit-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6); + -moz-box-shadow:inset 0 1px 4px rgba(135, 179, 255, 0.6); + padding: 2px 1px; + margin: 0px; + color: #444; + font-size: 1em; +} + +.recipientTextBox-invalid { + box-shadow: inset 0px 1px 4px rgba(255, 0, 0, 0.6); + -webkit-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6); + -moz-box-shadow:inset 0 1px 4px rgba(255, 0, 0, 0.6); + border: 1px solid rgb(255, 0, 0); +} + +.recipientRemoveButton { + margin: 0px 10px 0px 0px; + padding: 0px; + border: 1px dashed red; + border-radius: 5px 5px 5px 5px; +} + +.recipientRemoveIcon { + color: red; + width:15px; + height:15px; + vertical-align: baseline; +} + +.dragover-recipientPanel { + border-radius: 5px; + background: none repeat scroll 0% 0% rgb(135, 179, 255); + border: 1px dashed rgb(35,79,255); +} + +.recipientSpacer { + height: 15px; +} + +/* Popup (context) menu */ + +.popupMenuItem { + cursor: pointer; + border-radius: 5px; + width: 100%; +} + +/* Contact group manager */ + +.contactGroupEditor { + width: 800px; + max-width:800px; + min-width: 800px; + margin-top: 9px; + margin-left:18px; +} + +.contactGroupRemoveButton { + margin: 0px 10px 0px 0px; + padding: 0px; + border: 1px dashed red; + border-radius: 5px 5px 5px 5px; +} + +.addContactGroupPanel { + +} + +.contactGroupPanel { + vertical-align:middle; +} + +.toggleAssignedContacts { + white-space: nowrap; +} + +.contactGroupButtonCell { + vertical-align: baseline; + width: 55px; + white-space: nowrap; +} + +/* Room and contacts chooser */ + +.room-contact-chooser { + width:380px; +} + +/* StackPanel */ + +.gwt-StackPanel { +} + +.gwt-StackPanel .gwt-StackPanelItem { + background-color: #222; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’444444′, endColorstr=’#222222’); + background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222)); + background: -moz-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(#444444,#222222); + text-decoration: none; + font-weight: bold; + height: 100%; + color: #e7e5e5; + padding: 3px 15px; + /*display: block;*/ + border-radius: 1em 1em 1em 1em; + -webkit-border-radius: 1em 1em 1em 1em; + -moz-border-radius: 1em 1em 1em 1em; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -o-transition: color 0.2s linear; +} + +.gwt-StackPanel .gwt-StackPanelItem:hover { + background-color: #eee; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′); + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -moz-linear-gradient(top, #eee, #aaa); + background-image: -o-linear-gradient(#eee,#aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + cursor: pointer; +} + +.gwt-StackPanel .gwt-StackPanelItem-selected { + background-color: #eee; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=’#eee’, endColorstr=’#aaa′); + background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa)); + background: -moz-linear-gradient(top, #eee, #aaa); + background-image: -o-linear-gradient(#eee,#aaa); + color: #444; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); + cursor: pointer; +} + +/* Caption Panel */ + +.gwt-CaptionPanel { + overflow: auto; + background-color: #fff; + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + padding: 3px 5px 3px 5px; + margin: 10px 5px 10px 5px; + color: #444; + font-size: 1em; + border: solid 1px gray; +} + +/* Radio buttons */ + +.gwt-RadioButton { + white-space: nowrap; +} + +[contenteditable="true"] { +} + +/* XMLUI styles */ + +.AdvancedListSelectable tr{ + cursor: pointer; +} + +.AdvancedListSelectable tr:hover{ + background: none repeat scroll 0 0 #EE0000; +} + +.line hr { + +} + +.dot hr { + height: 0px; + border-top: 1px dotted; + border-bottom: 0px; +} + +.dash hr { + height: 0px; + border-top: 1px dashed; + border-bottom: 0px; +} + +.plain hr { + height: 10px; + color: black; + background-color: black; +} + +.blank hr { + border: 0px; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/public/libervia.html Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,32 @@ +<!-- +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/>. +--> + +<html> +<head profile="http://www.w3.org/2005/10/profile"> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +<meta name="pygwt:module" content="libervia_main"> +<link rel='stylesheet' href='libervia.css'> +<link rel="icon" type="image/png" href="sat_logo_16.png"> + +<title>Libervia</title> +</head> +<body bgcolor="white"> +<script language="javascript" src="bootstrap.js"></script> +<iframe id='__pygwt_historyFrame' style='display:none;width:0;height:0;border:0'></iframe> +</body> +</html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/radiocol.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,322 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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 sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools.misc import DEFAULT_MUC +from sat.core.i18n import _ + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.FormPanel import FormPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.Button import Button +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Hidden import Hidden +from pyjamas.ui.CaptionPanel import CaptionPanel +from pyjamas.media.Audio import Audio +from pyjamas import Window +from pyjamas.Timer import Timer + +from html_tools import html_sanitize +from file_tools import FilterFileUpload + + +class MetadataPanel(FlexTable): + + def __init__(self): + FlexTable.__init__(self) + title_lbl = Label("title:") + title_lbl.setStyleName('radiocol_metadata_lbl') + artist_lbl = Label("artist:") + artist_lbl.setStyleName('radiocol_metadata_lbl') + album_lbl = Label("album:") + album_lbl.setStyleName('radiocol_metadata_lbl') + self.title = Label("") + self.title.setStyleName('radiocol_metadata') + self.artist = Label("") + self.artist.setStyleName('radiocol_metadata') + self.album = Label("") + self.album.setStyleName('radiocol_metadata') + self.setWidget(0, 0, title_lbl) + self.setWidget(1, 0, artist_lbl) + self.setWidget(2, 0, album_lbl) + self.setWidget(0, 1, self.title) + self.setWidget(1, 1, self.artist) + self.setWidget(2, 1, self.album) + self.setStyleName("radiocol_metadata_pnl") + + def setTitle(self, title): + self.title.setText(title) + + def setArtist(self, artist): + self.artist.setText(artist) + + def setAlbum(self, album): + self.album.setText(album) + + +class ControlPanel(FormPanel): + """Panel used to show controls to add a song, or vote for the current one""" + + def __init__(self, parent): + FormPanel.__init__(self) + self.setEncoding(FormPanel.ENCODING_MULTIPART) + self.setMethod(FormPanel.METHOD_POST) + self.setAction("upload_radiocol") + self.timer_on = False + self._parent = parent + vPanel = VerticalPanel() + + types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'), + ('video/ogg', '*.ogv', 'Ogg Vorbis Video'), + ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'), + ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'), + ('audio/mp3', '*.mp3', 'MPEG-Layer 3'), + ] + self.file_upload = FilterFileUpload("song", 10, types) + vPanel.add(self.file_upload) + + hPanel = HorizontalPanel() + self.upload_btn = Button("Upload song", getattr(self, "onBtnClick")) + hPanel.add(self.upload_btn) + self.status = Label() + self.updateStatus() + hPanel.add(self.status) + #We need to know the filename and the referee + self.filename_field = Hidden('filename', '') + hPanel.add(self.filename_field) + referee_field = Hidden('referee', self._parent.referee) + hPanel.add(self.filename_field) + hPanel.add(referee_field) + vPanel.add(hPanel) + + self.add(vPanel) + self.addFormHandler(self) + + def updateStatus(self): + if self.timer_on: + return + # TODO: the status should be different if a song is being played or not + queue = self._parent.getQueueSize() + queue_data = self._parent.queue_data + if queue < queue_data[0]: + left = queue_data[0] - queue + self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else "")) + elif queue < queue_data[1]: + left = queue_data[1] - queue + self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else "")) + elif queue >= queue_data[1]: + self.status.setText("[The queue is currently full]") + self.status.setStyleName('radiocol_status') + + def onBtnClick(self): + if self.file_upload.check(): + self.status.setText('[Submitting, please wait...]') + self.filename_field.setValue(self.file_upload.getFilename()) + if self.file_upload.getFilename().lower().endswith('.mp3'): + self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000) + self.submit() + self.file_upload.setFilename("") + + def onSubmit(self, event): + pass + + def blockUpload(self): + self.file_upload.setVisible(False) + self.upload_btn.setEnabled(False) + + def unblockUpload(self): + self.file_upload.setVisible(True) + self.upload_btn.setEnabled(True) + + def setTemporaryStatus(self, text, style): + self.status.setText(text) + self.status.setStyleName('radiocol_upload_status_%s' % style) + self.timer_on = True + + def cb(timer): + self.timer_on = False + self.updateStatus() + + Timer(5000, cb) + + def onSubmitComplete(self, event): + result = event.getResults() + if result == "OK": + # the song can still be rejected (not readable, full queue...) + self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok") + elif result == "KO": + self.setTemporaryStatus('[Something went wrong during your song upload]', "ko") + self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")) + # TODO: would be great to re-use the original Exception class and message + # but it is lost in the middle of the traceback and encapsulated within + # a DBusException instance --> extract the data from the traceback? + else: + Window.alert('Submit error: %s' % result) + self.status.setText('') + + +class Player(Audio): + + def __init__(self, player_id, metadata_panel): + Audio.__init__(self) + self._id = player_id + self.metadata = metadata_panel + self.timestamp = "" + self.title = "" + self.artist = "" + self.album = "" + self.filename = None + self.played = False # True when the song is playing/has played, becomes False on preload + self.setAutobuffer(True) + self.setAutoplay(False) + self.setVisible(False) + + + def preload(self, timestamp, filename, title, artist, album): + """preload the song but doesn't play it""" + self.timestamp = timestamp + self.filename = filename + self.title = title + self.artist = artist + self.album = album + self.played = False + self.setSrc("radiocol/%s" % html_sanitize(filename)) + log.debug("preloading %s in %s" % (title, self._id)) + + def play(self, play=True): + """Play or pause the song + @param play: set to True to play or to False to pause + """ + if play: + self.played = True + self.metadata.setTitle(self.title) + self.metadata.setArtist(self.artist) + self.metadata.setAlbum(self.album) + Audio.play(self) + else: + self.pause() + + +class RadioColPanel(HorizontalPanel, ClickHandler): + + def __init__(self, parent, referee, player_nick, players, queue_data): + """ + @param parent + @param referee + @param player_nick + @param players + @param queue_data: list of integers (queue to start, queue limit) + """ + # We need to set it here and not in the CSS :( + HorizontalPanel.__init__(self, Height="90px") + ClickHandler.__init__(self) + self._parent = parent + self.referee = referee + self.queue_data = queue_data + self.setStyleName("radiocolPanel") + + # Now we set up the layout + self.metadata_panel = MetadataPanel() + self.add(CaptionPanel("Now playing", self.metadata_panel)) + self.playlist_panel = VerticalPanel() + self.add(CaptionPanel("Songs queue", self.playlist_panel)) + self.control_panel = ControlPanel(self) + self.add(CaptionPanel("Controls", self.control_panel)) + + self.next_songs = [] + self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)] + self.current_player = None + for player in self.players: + self.add(player) + self.addClickListener(self) + + help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br /> + Please do not submit files that are protected by copyright.<br /> + Click <a style="color: red;">here</a> if you need some support :)""" + link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick) + self._parent.printInfo(help_msg, type_='link', link_cb=link_cb) + + def pushNextSong(self, title): + """Add a song to the left panel's next songs queue""" + next_song = Label(title) + next_song.setStyleName("radiocol_next_song") + self.next_songs.append(next_song) + self.playlist_panel.append(next_song) + self.control_panel.updateStatus() + + def popNextSong(self): + """Remove the first song of next songs list + should be called when the song is played""" + #FIXME: should check that the song we remove is the one we play + next_song = self.next_songs.pop(0) + self.playlist_panel.remove(next_song) + self.control_panel.updateStatus() + + def getQueueSize(self): + return len(self.playlist_panel.getChildren()) + + def radiocolCheckPreload(self, timestamp): + for player in self.players: + if player.timestamp == timestamp: + return False + return True + + def radiocolPreload(self, timestamp, filename, title, artist, album, sender): + if not self.radiocolCheckPreload(timestamp): + return # song already preloaded + preloaded = False + for player in self.players: + if not player.filename or \ + (player.played and player != self.current_player): + #if player has no file loaded, or it has already played its song + #we use it to preload the next one + player.preload(timestamp, filename, title, artist, album) + preloaded = True + break + if not preloaded: + log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1]) + else: + self.pushNextSong(title) + self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title}) + + def radiocolPlay(self, filename): + found = False + for player in self.players: + if not found and player.filename == filename: + player.play() + self.popNextSong() + self.current_player = player + found = True + else: + player.play(False) # in case the previous player was not sync + if not found: + log.error("Song not found in queue, can't play it. This should not happen") + + def radiocolNoUpload(self): + self.control_panel.blockUpload() + + def radiocolUploadOk(self): + self.control_panel.unblockUpload() + + def radiocolSongRejected(self, reason): + Window.alert("Song rejected: %s" % reason)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/register.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,252 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> +# Copyright (C) 2011, 2012 Adrien Vigneron <adrienvigneron@mailoo.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/>. + +#This page manage subscription and new account creation + +import pyjd # this is dummy in pyjs +from sat.core.i18n import _ + +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.TabPanel import TabPanel +from pyjamas.ui.TabBar import TabBar +from pyjamas.ui.PasswordTextBox import PasswordTextBox +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.FormPanel import FormPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.Image import Image +from pyjamas.ui.Hidden import Hidden +from pyjamas import Window +from pyjamas.ui.KeyboardListener import KEY_ENTER +from pyjamas.Timer import Timer + +import re + +from constants import Const as C + + +class RegisterPanel(FormPanel): + + def __init__(self, callback): + """ + @param callback: method to call if login successful + """ + FormPanel.__init__(self) + self.setSize('600px', '350px') + self.callback = callback + self.setMethod(FormPanel.METHOD_POST) + main_panel = HorizontalPanel() + main_panel.setStyleName('registerPanel_main') + left_side = Image("media/libervia/register_left.png") + main_panel.add(left_side) + + ##TabPanel## + tab_bar = TabBar() + tab_bar.setStyleName('registerPanel_tabs') + self.right_side = TabPanel(tab_bar) + self.right_side.setStyleName('registerPanel_right_side') + main_panel.add(self.right_side) + main_panel.setCellWidth(self.right_side, '100%') + + ##Login tab## + login_tab = SimplePanel() + login_tab.setStyleName('registerPanel_content') + login_vpanel = VerticalPanel() + login_tab.setWidget(login_vpanel) + + self.login_warning_msg = Label('') + self.login_warning_msg.setVisible(False) + self.login_warning_msg.setStyleName('formWarning') + login_vpanel.add(self.login_warning_msg) + + login_label = Label('Login:') + self.login_box = TextBox() + self.login_box.setName("login") + self.login_box.addKeyboardListener(self) + login_pass_label = Label('Password:') + self.login_pass_box = PasswordTextBox() + self.login_pass_box.setName("login_password") + self.login_pass_box.addKeyboardListener(self) + + login_vpanel.add(login_label) + login_vpanel.add(self.login_box) + login_vpanel.add(login_pass_label) + login_vpanel.add(self.login_pass_box) + login_but = Button("Log in", getattr(self, "onLogin")) + login_but.setStyleName('button') + login_but.addStyleName('red') + login_vpanel.add(login_but) + + #The hidden submit_type field + self.submit_type = Hidden('submit_type') + login_vpanel.add(self.submit_type) + + ##Register tab## + register_tab = SimplePanel() + register_tab.setStyleName('registerPanel_content') + register_vpanel = VerticalPanel() + register_tab.setWidget(register_vpanel) + + self.register_warning_msg = HTML('') + self.register_warning_msg.setVisible(False) + self.register_warning_msg.setStyleName('formWarning') + register_vpanel.add(self.register_warning_msg) + + register_login_label = Label('Login:') + self.register_login_box = TextBox() + self.register_login_box.setName("register_login") + self.register_login_box.addKeyboardListener(self) + email_label = Label('E-mail:') + self.email_box = TextBox() + self.email_box.setName("email") + self.email_box.addKeyboardListener(self) + register_pass_label = Label('Password:') + self.register_pass_box = PasswordTextBox() + self.register_pass_box.setName("register_password") + self.register_pass_box.addKeyboardListener(self) + register_vpanel.add(register_login_label) + register_vpanel.add(self.register_login_box) + register_vpanel.add(email_label) + register_vpanel.add(self.email_box) + register_vpanel.add(register_pass_label) + register_vpanel.add(self.register_pass_box) + + register_but = Button("Register", getattr(self, "onRegister")) + register_but.setStyleName('button') + register_but.addStyleName('red') + register_vpanel.add(register_but) + + self.right_side.add(login_tab, 'Login') + self.right_side.add(register_tab, 'Register') + self.right_side.addTabListener(self) + self.right_side.selectTab(1) + login_tab.setWidth(None) + register_tab.setWidth(None) + + self.add(main_panel) + self.addFormHandler(self) + self.setAction('register_api/login') + + def onBeforeTabSelected(self, sender, tabIndex): + return True + + def onTabSelected(self, sender, tabIndex): + if tabIndex == 0: + self.login_box.setFocus(True) + elif tabIndex == 1: + self.register_login_box.setFocus(True) + + def onKeyPress(self, sender, keycode, modifiers): + if keycode == KEY_ENTER: + # Browsers offer an auto-completion feature to any + # text box, but the selected value is not set when + # the widget looses the focus. Using a timer with + # any delay value > 0 would do the trick. + if sender == self.login_box: + Timer(5, lambda timer: self.login_pass_box.setFocus(True)) + elif sender == self.login_pass_box: + self.onLogin(None) + elif sender == self.register_login_box: + Timer(5, lambda timer: self.email_box.setFocus(True)) + elif sender == self.email_box: + Timer(5, lambda timer: self.register_pass_box.setFocus(True)) + elif sender == self.register_pass_box: + self.onRegister(None) + + def onKeyUp(self, sender, keycode, modifiers): + pass + + def onKeyDown(self, sender, keycode, modifiers): + pass + + def onLogin(self, button): + if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE): + self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -') + self.login_warning_msg.setVisible(True) + else: + self.submit_type.setValue('login') + self.submit() + + def onRegister(self, button): + if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE): + self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -')) + self.register_warning_msg.setVisible(True) + elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE): + self.register_warning_msg.setHTML(_('Invalid email address')) + self.register_warning_msg.setVisible(True) + elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: + self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH) + self.register_warning_msg.setVisible(True) + else: + self.register_warning_msg.setVisible(False) + self.submit_type.setValue('register') + self.submit() + + def onSubmit(self, event): + pass + + def onSubmitComplete(self, event): + result = event.getResults() + if result == "AUTH ERROR": + Window.alert('Your login and/or password is incorrect. Please try again') + elif result == "LOGGED": + self.callback() + elif result == "SESSION_ACTIVE": + Window.alert('Session already active, this should not happen, please contact the author to fix it') + elif result == "ALREADY EXISTS": + self.register_warning_msg.setHTML('This login already exists,<br>please choose another one') + self.register_warning_msg.setVisible(True) + elif result == "INTERNAL": + self.register_warning_msg.setHTML('SERVER ERROR: something went wrong during registration process, please contact the server administrator') + self.register_warning_msg.setVisible(True) + elif result == "REGISTRATION": + self.login_warning_msg.setVisible(False) + self.register_warning_msg.setVisible(False) + self.login_box.setText(self.register_login_box.getText()) + self.login_pass_box.setText('') + self.register_login_box.setText('') + self.register_pass_box.setText('') + self.email_box.setText('') + self.right_side.selectTab(0) + self.login_pass_box.setFocus(True) + Window.alert('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO') + else: + Window.alert('Submit error: %s' % result) + + +class RegisterBox(PopupPanel): + + def __init__(self, callback, *args, **kwargs): + PopupPanel.__init__(self, *args, **kwargs) + self._form = RegisterPanel(callback) + self.setWidget(self._form) + + def onWindowResized(self, width, height): + super(RegisterBox, self).onWindowResized(width, height) + self.centerBox() + + def show(self): + super(RegisterBox, self).show() + self.centerBox() + self._form.login_box.setFocus(True)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/richtext.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 Adrien Cossa <souliane@mailoo.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/>. + +from sat_frontends.tools import composition +from sat.core.i18n import _ + +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.Button import Button +from pyjamas.ui.CheckBox import CheckBox +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas import Window +from pyjamas.ui.KeyboardListener import KeyboardHandler +from __pyjamas__ import doc + +from constants import Const as C +from dialog import ConfirmDialog, InfoDialog +from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor +from list_manager import ListManager +from html_tools import html_sanitize +import panels + + +class RichTextEditor(BaseTextEditor, FlexTable): + """Panel for the rich text editor.""" + + def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None): + """ + @param host: the SatWebFrontend instance + @param content: dict with at least a 'text' key + @param modifiedCb: method to be called when the text has been modified + @param afterEditCb: method to be called when the edition is done + @param options: list of UI options (see self.readOptions) + """ + self.host = host + self._debug = False # TODO: don't forget to set it False before commit + self.wysiwyg = False + self.__readOptions(options) + self.style = {'main': 'richTextEditor', + 'title': 'richTextTitle', + 'toolbar': 'richTextToolbar', + 'textarea': 'richTextArea'} + if isinstance(style, dict): + self.style.update(style) + self._prepareUI() + BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) + + def __readOptions(self, options): + """Set the internal flags according to the given options.""" + if options is None: + options = [] + self.read_only = 'read_only' in options + self.update_msg = 'update_msg' in options + self.no_title = 'no_title' in options or self.read_only + self.no_command = 'no_command' in options or self.read_only + + def _prepareUI(self, y_offset=0): + """Prepare the UI to host title panel, toolbar, text area... + @param y_offset: Y offset to start from (extra rows on top)""" + if not self.read_only: + self.title_offset = y_offset + self.toolbar_offset = self.title_offset + (0 if self.no_title else 1) + self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1) + self.command_offset = self.content_offset + 1 + else: + self.title_offset = self.toolbar_offset = self.content_offset = y_offset + self.command_offset = self.content_offset + 1 + FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2) + self.addStyleName(self.style['main']) + + def addEditListener(self, listener): + """Add a method to be called whenever the text is edited. + @param listener: method taking two arguments: sender, keycode""" + BaseTextEditor.addEditListener(self, listener) + if hasattr(self, 'display'): + self.display.addEditListener(listener) + + def refresh(self, edit=None): + """Refresh the UI for edition/display mode + @param edit: set to True to display the edition mode""" + if edit is None: + edit = hasattr(self, 'textarea') and self.textarea.getVisible() + + for widget in ['title_panel', 'command']: + if hasattr(self, widget): + getattr(self, widget).setVisible(edit) + + if hasattr(self, 'toolbar'): + self.toolbar.setVisible(False) + if not hasattr(self, 'display'): + self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode + for listener in self.edit_listeners: + self.display.addEditListener(listener) + if not self.read_only and not hasattr(self, 'textarea'): + self.textarea = EditTextArea(self) # for edition mode + self.textarea.addStyleName(self.style['textarea']) + + self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) + if edit and not self.wysiwyg: + self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why + self.setWidget(self.content_offset, 0, self.textarea) + else: + self.setWidget(self.content_offset, 0, self.display) + if not edit: + return + + if not self.no_title and not hasattr(self, 'title_panel'): + self.title_panel = TitlePanel() + self.title_panel.addStyleName(self.style['title']) + self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) + self.setWidget(self.title_offset, 0, self.title_panel) + + if not self.no_command and not hasattr(self, 'command'): + self.command = HorizontalPanel() + self.command.addStyleName("marginAuto") + self.command.add(Button("Cancel", lambda: self.edit(True, True))) + self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) + self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) + self.setWidget(self.command_offset, 0, self.command) + + def setToolBar(self, syntax): + """This method is called asynchronously after the parameter + holding the rich text syntax is retrieved. It is called at + each call of self.edit(True) because the user may + have change his setting since the last time.""" + if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): + syntax = composition.RICH_SYNTAXES.keys()[0] + if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: + self.toolbar.setVisible(True) + return + count = 0 + for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]: + self.toolbar = HorizontalPanel() + self.toolbar.syntax = syntax + self.toolbar.addStyleName(self.style['toolbar']) + for key in composition.RICH_SYNTAXES[syntax].keys(): + self.addToolbarButton(syntax, key) + self.wysiwyg_button = CheckBox(_('WYSIWYG edition')) + wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) + self.wysiwyg_button.addClickListener(wysiywgCb) + self.toolbar.add(self.wysiwyg_button) + self.syntax_label = Label(_("Syntax: %s") % syntax) + self.syntax_label.addStyleName("richTextSyntaxLabel") + self.toolbar.add(self.syntax_label) + self.toolbar.setCellWidth(self.syntax_label, "100%") + self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2) + self.setWidget(self.toolbar_offset + count, 0, self.toolbar) + count += 1 + + def setWysiwyg(self, wysiwyg, init=False): + """Toggle the edition mode between rich content syntax and wysiwyg. + @param wysiwyg: boolean value + @param init: set to True to re-init without switching the widgets.""" + def setWysiwyg(): + self.wysiwyg = wysiwyg + try: + self.wysiwyg_button.setChecked(wysiwyg) + except TypeError: + pass + try: + if wysiwyg: + self.syntax_label.addStyleName('transparent') + else: + self.syntax_label.removeStyleName('transparent') + except TypeError: + pass + if not wysiwyg: + self.display.removeStyleName('richTextWysiwyg') + + if init: + setWysiwyg() + return + + self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) + if wysiwyg: + def syntaxConvertCb(text): + self.display.setContent({'text': text}) + self.textarea.removeFromParent() # XXX: force as it is not always done... + self.setWidget(self.content_offset, 0, self.display) + self.display.addStyleName('richTextWysiwyg') + self.display.edit(True) + content = self.getContent() + if content['text'] and content['syntax'] != C.SYNTAX_XHTML: + self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) + else: + syntaxConvertCb(content['text']) + else: + syntaxConvertCb = lambda text: self.textarea.setText(text) + text = self.display.getContent()['text'] + if text and self.toolbar.syntax != C.SYNTAX_XHTML: + self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) + else: + syntaxConvertCb(text) + self.setWidget(self.content_offset, 0, self.textarea) + self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why + + setWysiwyg() # do it in the end because it affects self.getContent + + def addToolbarButton(self, syntax, key): + """Add a button with the defined parameters.""" + button = Button('<img src="%s" class="richTextIcon" />' % + composition.RICH_BUTTONS[key]["icon"]) + button.setTitle(composition.RICH_BUTTONS[key]["tip"]) + button.addStyleName('richTextToolButton') + self.toolbar.add(button) + + def buttonCb(): + """Generic callback for a toolbar button.""" + text = self.textarea.getText() + cursor_pos = self.textarea.getCursorPos() + selection_length = self.textarea.getSelectionLength() + data = composition.RICH_SYNTAXES[syntax][key] + if selection_length == 0: + middle_text = data[1] + else: + middle_text = text[cursor_pos:cursor_pos + selection_length] + self.textarea.setText(text[:cursor_pos] + + data[0] + + middle_text + + data[2] + + text[cursor_pos + selection_length:]) + self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) + self.textarea.setFocus(True) + self.textarea.onKeyDown() + + def wysiwygCb(): + """Callback for a toolbar button while wysiwyg mode is enabled.""" + data = composition.COMMANDS[key] + + def execCommand(command, arg): + self.display.setFocus(True) + doc().execCommand(command, False, arg.strip() if arg else '') + # use Window.prompt instead of dialog.PromptDialog to not loose the focus + prompt = lambda command, text: execCommand(command, Window.prompt(text)) + if isinstance(data, tuple) or isinstance(data, list): + if data[1]: + prompt(data[0], data[1]) + else: + execCommand(data[0], data[2]) + else: + execCommand(data, False, '') + self.textarea.onKeyDown() + + button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) + + def getContent(self): + assert(hasattr(self, 'textarea')) + assert(hasattr(self, 'toolbar')) + if self.wysiwyg: + content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} + else: + content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} + if hasattr(self, 'title_panel'): + content.update({'title': self.strproc(self.title_panel.getText())}) + return content + + def edit(self, edit=False, abort=False, sync=False): + """ + Remark: the editor must be visible before you call this method. + @param edit: set to True to edit the content or False to only display it + @param abort: set to True to cancel the edition and loose the changes. + If edit and abort are both True, self.abortEdition can be used to ask for a + confirmation. When edit is False and abort is True, abortion is actually done. + @param sync: set to True to cancel the edition after the content has been saved somewhere else + """ + if not (edit and abort): + self.refresh(edit) # not when we are asking for a confirmation + BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed + if (edit and abort): + return # self.abortEdition is called by BaseTextEditor.edit + self.setWysiwyg(False, init=True) # after BaseTextEditor (it affects self.getContent) + if sync: + return + # the following must NOT be done at each UI refresh! + content = self._original_content + if edit: + def getParamCb(syntax): + # set the editable text in the current user-selected syntax + def syntaxConvertCb(text=None): + if text is not None: + # Important: this also update self._original_content + content.update({'text': text}) + content.update({'syntax': syntax}) + self.textarea.setText(content['text']) + if hasattr(self, 'title_panel') and 'title' in content: + self.title_panel.setText(content['title']) + self.title_panel.setStackVisible(0, content['title'] != '') + self.setToolBar(syntax) + if content['text'] and content['syntax'] != syntax: + self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) + else: + syntaxConvertCb() + self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) + else: + if not self.initialized: + # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification + self.setDisplayContent() + self.display.edit(False) + + def setDisplayContent(self): + """Set the content of the HTMLTextEditor which is used for display/wysiwyg""" + content = self._original_content + text = content['text'] + if 'title' in content and content['title']: + text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text']) + self.display.setContent({'text': text}) + + def setFocus(self, focus): + self.textarea.setFocus(focus) + + def abortEdition(self, content): + """Ask for confirmation before closing the dialog.""" + def confirm_cb(answer): + if answer: + self.edit(False, True) + _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) + _dialog.cancel_button.setText(_("No")) + _dialog.show() + + +class RichMessageEditor(RichTextEditor): + """Use the rich text editor for sending messages with extended addressing. + Recipient panels are on top and data may be synchronized from/to the unibox.""" + + @classmethod + def getOrCreate(cls, host, parent=None, callback=None): + """Get or create the message editor associated to that host. + Add it to parent if parent is not None, otherwise display it + in a popup dialog. + @param host: the host + @param parent: parent panel (or None to display in a popup). + @return: the RichTextEditor instance if parent is not None, + otherwise a popup DialogBox containing the RichTextEditor. + """ + if not hasattr(host, 'richtext'): + modifiedCb = lambda content: True + + def afterEditCb(content): + if hasattr(host.richtext, 'popup'): + host.richtext.popup.hide() + else: + host.richtext.setVisible(False) + callback() + options = ['no_title'] + style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'} + host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style) + + def add(widget, parent): + if widget.getParent() is not None: + if widget.getParent() != parent: + widget.removeFromParent() + parent.add(widget) + else: + parent.add(widget) + widget.setVisible(True) + widget.initialized = False # fake a new creation + widget.edit(True) + + if parent is None: + if not hasattr(host.richtext, 'popup'): + host.richtext.popup = DialogBox(autoHide=False, centered=True) + host.richtext.popup.setHTML("Compose your message") + host.richtext.popup.add(host.richtext) + add(host.richtext, host.richtext.popup) + host.richtext.popup.center() + else: + add(host.richtext, parent) + return host.richtext.popup if parent is None else host.richtext + + def _prepareUI(self, y_offset=0): + """Prepare the UI to host recipients panel, toolbar, text area... + @param y_offset: Y offset to start from (extra rows on top)""" + self.recipient_offset = y_offset + self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES) + RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1) + + def refresh(self, edit=None): + """Refresh the UI between edition/display mode + @param edit: set to True to display the edition mode""" + if edit is None: + edit = hasattr(self, 'textarea') and self.textarea.getVisible() + RichTextEditor.refresh(self, edit) + + for widget in ['recipient', 'recipient_spacer']: + if hasattr(self, widget): + getattr(self, widget).setVisible(edit) + + if not edit: + return + + if not hasattr(self, 'recipient'): + # recipient types sub-panels are automatically added by the manager + self.recipient = RecipientManager(self, self.recipient_offset) + self.recipient.createWidgets(title_format="%s: ") + self.recipient_spacer = HTML('') + self.recipient_spacer.setStyleName('recipientSpacer') + self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2) + self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer) + + if not hasattr(self, 'sync_button'): + self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True)) + self.command.insert(self.sync_button, 1) + + def syncToEditor(self): + """Synchronize from unibox.""" + def setContent(target, data): + if hasattr(self, 'recipient'): + self.recipient.setContacts({"To": [target]} if target else {}) + self.setContent({'text': data if data else '', 'syntax': ''}) + self.textarea.setText(data if data else '') + data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None) + setContent(target, data) + + def __syncToUniBox(self, recipients=None, emptyText=False): + """Synchronize to unibox if a maximum of one recipient is set. + @return True if the sync could be done, False otherwise""" + if not self.host.uni_box: + return + setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text']) + if not hasattr(self, 'recipient'): + setText() + return True + if recipients is None: + recipients = self.recipient.getContacts() + target = "" + # we could eventually allow more in the future + allowed = 1 + for key in recipients: + count = len(recipients[key]) + if count == 0: + continue + allowed -= count + if allowed < 0: + return False + # TODO: change this if later more then one recipients are allowed + target = recipients[key][0] + setText() + if target == "": + return True + if target.startswith("@"): + _class = panels.MicroblogPanel + target = None if target == "@@" else target[1:] + else: + _class = panels.ChatPanel + self.host.getOrCreateLiberviaWidget(_class, target) + return True + + def syncFromEditor(self, content): + """Synchronize to unibox and close the dialog afterward. Display + a message and leave the dialog open if the sync was not possible.""" + if self.__syncToUniBox(): + self._afterEditCb(content) + return + InfoDialog("Too many recipients", + "A message with more than one direct recipient (To)," + + " or with any special recipient (Cc or Bcc), could not be" + + " stored in the quick box.\n\nPlease finish your composing" + + " in the rich text editor, and send your message directly" + + " from here.", Width="400px").center() + + def edit(self, edit=True, abort=False, sync=False): + if not edit and not abort and not sync: # force sending message even when the text has not been modified + if not self.__sendMessage(): # message has not been sent (missing information), do nothing + return + RichTextEditor.edit(self, edit, abort, sync) + + def __sendMessage(self): + """Send the message.""" + recipients = self.recipient.getContacts() + targets = [] + for addr in recipients: + for recipient in recipients[addr]: + if recipient.startswith("@"): + targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr)) + else: + targets.append(("chat", recipient, addr)) + # check that we actually have a message target and data + content = self.getContent() + if content['text'] == "" or len(targets) == 0: + InfoDialog("Missing information", + "Some information are missing and the message hasn't been sent.", Width="400px").center() + return None + self.__syncToUniBox(recipients, emptyText=True) + extra = {'content_rich': content['text']} + if hasattr(self, 'title_panel'): + extra.update({'title': content['title']}) + self.host.send(targets, content['text'], extra=extra) + return True + + +class RecipientManager(ListManager): + """A manager for sub-panels to set the recipients for each recipient type.""" + + def __init__(self, parent, y_offset=0): + # TODO: be sure we also display empty groups and disconnected contacts + their groups + # store the full list of potential recipients (groups and contacts) + list_ = [] + list_.append("@@") + list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) + list_.extend(contact for contact in parent.host.contact_panel.getContacts()) + ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset}) + + self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, + hide=lambda sender, key: self.__children[key]["panel"].isVisible(), + callback=self.setContactPanelVisible) + + +class EditTextArea(TextArea, KeyboardHandler): + def __init__(self, _parent): + TextArea.__init__(self) + self._parent = _parent + KeyboardHandler.__init__(self) + self.addKeyboardListener(self) + + def onKeyDown(self, sender=None, keycode=None, modifiers=None): + for listener in self._parent.edit_listeners: + listener(self, keycode)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/xmlui.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,420 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools import xmlui + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.TabPanel import TabPanel +from pyjamas.ui.Grid import Grid +from pyjamas.ui.Label import Label +from pyjamas.ui.TextBox import TextBox +from pyjamas.ui.PasswordTextBox import PasswordTextBox +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.CheckBox import CheckBox +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML + +from nativedom import NativeDOM + + +class EmptyWidget(xmlui.EmptyWidget, Label): + + def __init__(self, parent): + Label.__init__(self, '') + + +class TextWidget(xmlui.TextWidget, Label): + + def __init__(self, parent, value): + Label.__init__(self, value) + + +class LabelWidget(xmlui.LabelWidget, TextWidget): + + def __init__(self, parent, value): + TextWidget.__init__(self, parent, value+": ") + + +class JidWidget(xmlui.JidWidget, TextWidget): + + def __init__(self, parent, value): + TextWidget.__init__(self, parent, value) + + +class DividerWidget(xmlui.DividerWidget, HTML): + + def __init__(self, parent, style='line'): + """Add a divider + + @param parent + @param style (string): one of: + - line: a simple line + - dot: a line of dots + - dash: a line of dashes + - plain: a full thick line + - blank: a blank line/space + """ + HTML.__init__(self, "<hr/>") + self.addStyleName(style) + + +class StringWidget(xmlui.StringWidget, TextBox): + + def __init__(self, parent, value): + TextBox.__init__(self) + self.setText(value) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class PasswordWidget(xmlui.PasswordWidget, PasswordTextBox): + + def __init__(self, parent, value): + PasswordTextBox.__init__(self) + self.setText(value) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class TextBoxWidget(xmlui.TextBoxWidget, TextArea): + + def __init__(self, parent, value): + TextArea.__init__(self) + self.setText(value) + + def _xmluiSetValue(self, value): + self.setText(value) + + def _xmluiGetValue(self): + return self.getText() + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + +class BoolWidget(xmlui.BoolWidget, CheckBox): + + def __init__(self, parent, state): + CheckBox.__init__(self) + self.setChecked(state) + + def _xmluiSetValue(self, value): + self.setChecked(value == "true") + + def _xmluiGetValue(self): + return "true" if self.isChecked() else "false" + + def _xmluiOnChange(self, callback): + self.addClickListener(callback) + + +class ButtonWidget(xmlui.ButtonWidget, Button): + + def __init__(self, parent, value, click_callback): + Button.__init__(self, value, click_callback) + + def _xmluiOnClick(self, callback): + self.addClickListener(callback) + + +class ListWidget(xmlui.ListWidget, ListBox): + + def __init__(self, parent, options, selected, flags): + ListBox.__init__(self) + multi_selection = 'single' not in flags + self.setMultipleSelect(multi_selection) + if multi_selection: + self.setVisibleItemCount(5) + for option in options: + self.addItem(option[1]) + self._xmlui_attr_map = {label: value for value, label in options} + self._xmluiSelectValues(selected) + + def _xmluiSelectValue(self, value): + """Select a value checking its item""" + try: + label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0] + except IndexError: + log.warning("Can't find value [%s] to select" % value) + return + self.selectItem(label) + + def _xmluiSelectValues(self, values): + """Select multiple values, ignore the items""" + self.setValueSelection(values) + + def _xmluiGetSelectedValues(self): + ret = [] + for label in self.getSelectedItemText(): + ret.append(self._xmlui_attr_map[label]) + return ret + + def _xmluiOnChange(self, callback): + self.addChangeListener(callback) + + def _xmluiAddValues(self, values, select=True): + selected = self._xmluiGetSelectedValues() + for value in values: + if value not in self._xmlui_attr_map.values(): + self.addItem(value) + self._xmlui_attr_map[value] = value + if value not in selected: + selected.append(value) + self._xmluiSelectValues(selected) + + +class LiberviaContainer(object): + + def _xmluiAppend(self, widget): + self.append(widget) + + +class AdvancedListContainer(xmlui.AdvancedListContainer, Grid): + + def __init__(self, parent, columns, selectable='no'): + Grid.__init__(self, 0, columns) + self.columns = columns + self.row = -1 + self.col = 0 + self._xmlui_rows_idx = [] + self._xmlui_selectable = selectable != 'no' + self._xmlui_selected_row = None + self.addTableListener(self) + if self._xmlui_selectable: + self.addStyleName('AdvancedListSelectable') + + def onCellClicked(self, grid, row, col): + if not self._xmlui_selectable: + return + self._xmlui_selected_row = row + try: + self._xmlui_select_cb(self) + except AttributeError: + log.warning("no select callback set") + + + def _xmluiAppend(self, widget): + self.setWidget(self.row, self.col, widget) + self.col += 1 + + def _xmluiAddRow(self, idx): + self.row += 1 + self.col = 0 + self._xmlui_rows_idx.insert(self.row, idx) + self.resizeRows(self.row+1) + + def _xmluiGetSelectedWidgets(self): + return [self.getWidget(self._xmlui_selected_row, col) for col in range(self.columns)] + + def _xmluiGetSelectedIndex(self): + try: + return self._xmlui_rows_idx[self._xmlui_selected_row] + except TypeError: + return None + + def _xmluiOnSelect(self, callback): + self._xmlui_select_cb = callback + + +class PairsContainer(xmlui.PairsContainer, Grid): + + def __init__(self, parent): + Grid.__init__(self, 0, 0) + self.row = 0 + self.col = 0 + + def _xmluiAppend(self, widget): + if self.col == 0: + self.resize(self.row+1, 2) + self.setWidget(self.row, self.col, widget) + self.col += 1 + if self.col == 2: + self.row +=1 + self.col = 0 + + + +class TabsContainer(LiberviaContainer, xmlui.TabsContainer, TabPanel): + + def __init__(self, parent): + TabPanel.__init__(self) + self.setStyleName('liberviaTabPanel') + + def _xmluiAddTab(self, label): + tab_panel = VerticalContainer(self) + self.add(tab_panel, label) + if len(self.getChildren()) == 1: + self.selectTab(0) + return tab_panel + + +class VerticalContainer(LiberviaContainer, xmlui.VerticalContainer, VerticalPanel): + __bases__ = (LiberviaContainer, xmlui.VerticalContainer, VerticalPanel) + + def __init__(self, parent): + VerticalPanel.__init__(self) + + +class WidgetFactory(object): + # XXX: __getattr__ doesn't work here for an unknown reason + + def createVerticalContainer(self, *args, **kwargs): + instance = VerticalContainer(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createPairsContainer(self, *args, **kwargs): + instance = PairsContainer(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createTabsContainer(self, *args, **kwargs): + instance = TabsContainer(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createAdvancedListContainer(self, *args, **kwargs): + instance = AdvancedListContainer(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createEmptyWidget(self, *args, **kwargs): + instance = EmptyWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createTextWidget(self, *args, **kwargs): + instance = TextWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createLabelWidget(self, *args, **kwargs): + instance = LabelWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createJidWidget(self, *args, **kwargs): + instance = JidWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createDividerWidget(self, *args, **kwargs): + instance = DividerWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createStringWidget(self, *args, **kwargs): + instance = StringWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createPasswordWidget(self, *args, **kwargs): + instance = PasswordWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createTextBoxWidget(self, *args, **kwargs): + instance = TextBoxWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createBoolWidget(self, *args, **kwargs): + instance = BoolWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createButtonWidget(self, *args, **kwargs): + instance = ButtonWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + def createListWidget(self, *args, **kwargs): + instance = ListWidget(*args, **kwargs) + instance._xmlui_main = self._xmlui_main + return instance + + + # def __getattr__(self, attr): + # if attr.startswith("create"): + # cls = globals()[attr[6:]] + # cls._xmlui_main = self._xmlui_main + # return cls + + +class XMLUI(xmlui.XMLUI, VerticalPanel): + widget_factory = WidgetFactory() + + def __init__(self, host, xml_data, title = None, flags = None): + self.widget_factory._xmlui_main = self + self.dom = NativeDOM() + dom_parse = lambda xml_data: self.dom.parseString(xml_data) + VerticalPanel.__init__(self) + self.setSize('100%', '100%') + xmlui.XMLUI.__init__(self, host, xml_data, title, flags, dom_parse) + + def setCloseCb(self, close_cb): + self.close_cb = close_cb + + def _xmluiClose(self): + if self.close_cb: + self.close_cb() + else: + log.warning("no close method defined") + + def _xmluiLaunchAction(self, action_id, data): + self.host.launchAction(action_id, data) + + def _xmluiSetParam(self, name, value, category): + self.host.bridge.call('setParam', None, name, value, category) + + def constructUI(self, xml_data): + super(XMLUI, self).constructUI(xml_data) + self.add(self.main_cont) + self.setCellHeight(self.main_cont, '100%') + if self.type == 'form': + hpanel = HorizontalPanel() + hpanel.setStyleName('marginAuto') + hpanel.add(Button('Submit',self.onFormSubmitted)) + if not 'NO_CANCEL' in self.flags: + hpanel.add(Button('Cancel',self.onFormCancelled)) + self.add(hpanel) + elif self.type == 'param': + assert(isinstance(self.children[0][0],TabPanel)) + hpanel = HorizontalPanel() + hpanel.add(Button('Save', self.onSaveParams)) + hpanel.add(Button('Cancel', lambda ignore: self._xmluiClose())) + self.add(hpanel)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/common/constants.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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/>. + +from sat.core.i18n import D_ +from sat_frontends import constants + + +class Const(constants.Const): + + # Frontend parameters + ENABLE_UNIBOX_KEY = D_("Composition") + ENABLE_UNIBOX_PARAM = D_("Enable unibox") + + # MISC + PASSWORD_MIN_LENGTH = 6 # for new account creation
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libervia.sh Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,78 @@ +#!/bin/sh + +DEBUG="" +PYTHON="python2" + +kill_process() { + # $1 is the file containing the PID to kill, $2 is the process name + if [ -f $1 ]; then + PID=`cat $1` + if ps -p $PID > /dev/null; then + echo "Terminating $2... " + kill -INT $PID + else + echo "No running process of ID $PID... removing PID file" + rm -f $1 + fi + else + echo "$2 is probably not running (PID file doesn't exist)" + fi +} + +#We use python to parse config files +eval `"$PYTHON" << PYTHONEND + +from sat.core.constants import Const as C +from sat.memory.memory import fixLocalDir +from ConfigParser import SafeConfigParser +from os.path import expanduser, join +import sys + +fixLocalDir() # XXX: tmp update code, will be removed in the future + +config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) +try: + config.read(C.CONFIG_FILES) +except: + print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") + print ("exit 1") + sys.exit() + +env=[] +env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) +env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) +print ";".join(env) +PYTHONEND +` +APP_NAME="Libervia" # FIXME: the import from Python needs libervia module to be in PYTHONPATH +APP_NAME_FILE="libervia" +PID_FILE="$PID_DIR$APP_NAME_FILE.pid" +LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" + +# if there is one argument which is "stop", then we kill Libervia +if [ $# -eq 1 ];then + if [ $1 = "stop" ];then + kill_process $PID_FILE "$APP_NAME" + exit 0 + fi + if [ $1 = "debug" ];then + echo "Launching $APP_NAME in debug mode" + DEBUG="--debug" + fi +fi + +DAEMON="n" +MAIN_OPTIONS="-${DAEMON}o" +DATA_DIR=".." + +#Don't change the next line +AUTO_OPTIONS="" +ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" + +log_dir=`dirname "$LOG_FILE"` +if [ ! -d $log_dir ] ; then + mkdir $log_dir +fi + +echo "Starting $APP_NAME..." +twistd $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE -d $DATA_DIR
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/server/blog.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,226 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +from sat.core.i18n import _ +from sat_frontends.tools.strings import addURLToText + +from twisted.internet import defer +from twisted.web import server +from twisted.web.resource import Resource +from twisted.words.protocols.jabber.jid import JID +from datetime import datetime +import uuid +import re + +from libervia.server.html_tools import sanitizeHtml +from libervia.server.constants import Const as C + + +class MicroBlog(Resource): + isLeaf = True + + ERROR_TEMPLATE = """ + <html> + <head profile="http://www.w3.org/2005/10/profile"> + <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png"> + <title>MICROBLOG ERROR</title> + </head> + <body> + <h1 style='text-align: center; color: red;'>%(message)s</h1> + </body> + </html> + """ + + def __init__(self, host): + self.host = host + Resource.__init__(self) + + def render_GET(self, request): + if not request.postpath: + return MicroBlog.ERROR_TEMPLATE % {'root': '', + 'message': "You must indicate a nickname"} + else: + prof_requested = request.postpath[0] + #TODO: char check: only use alphanumerical chars + some extra(_,-,...) here + prof_found = self.host.bridge.getProfileName(prof_requested) + if not prof_found or prof_found == 'libervia': + return MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath), + 'message': "Invalid nickname"} + else: + def got_jid(pub_jid_s): + pub_jid = JID(pub_jid_s) + d2 = defer.Deferred() + item_id = None + if len(request.postpath) > 1: + if request.postpath[1] == 'atom.xml': # return the atom feed + d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None) + self.host.bridge.getLastGroupBlogsAtom(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback) + return + try: # check if the given path is a valid UUID + uuid.UUID(request.postpath[1]) + item_id = request.postpath[1] + except ValueError: + pass + d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None) + if item_id: # display one message and its comments + self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], 'libervia', d2.callback, d2.errback) + else: # display the last messages without comment + self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), 10, 'libervia', d2.callback, d2.errback) + + d1 = defer.Deferred() + JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback)) + d1.addCallbacks(got_jid) + + return server.NOT_DONE_YET + + def render_html_blog(self, mblog_data, request, profile): + """Retrieve the user parameters before actually rendering the static blog + @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data) + @param request: HTTP request + @param profile + """ + d_list = [] + style = {} + + def getCallback(param_name): + d = defer.Deferred() + d.addCallback(lambda value: style.update({param_name: value})) + d_list.append(d) + return d.callback + + eb = lambda failure: self.render_error_blog(failure, request, profile) + + for param_name in (C.STATIC_BLOG_PARAM_TITLE, C.STATIC_BLOG_PARAM_BANNER, C.STATIC_BLOG_PARAM_KEYWORDS, C.STATIC_BLOG_PARAM_DESCRIPTION): + self.host.bridge.asyncGetParamA(param_name, C.STATIC_BLOG_KEY, 'value', C.SERVER_SECURITY_LIMIT, profile, callback=getCallback(param_name), errback=eb) + + cb = lambda dummy: self.__render_html_blog(mblog_data, style, request, profile) + defer.DeferredList(d_list).addCallback(cb) + + def __render_html_blog(self, mblog_data, style, request, profile): + """Actually render the static blog. If mblog_data is a list of dict, we are missing + the comments items so we just display the main items. If mblog_data is a list of couple, + each couple is associating a main item data with the list of its comments, so we render all. + @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data) + @param style: dict defining the blog's rendering parameters + @param request: the HTTP request + @profile + """ + if not isinstance(style, dict): + style = {} + user = sanitizeHtml(profile).encode('utf-8') + root_url = '../' * len(request.postpath) + base_url = root_url + 'blog/' + user + + def getFromData(key): + return sanitizeHtml(style[key]).encode('utf-8') if key in style else '' + + def getImageFromData(key, alt): + """regexp from http://answers.oreilly.com/topic/280-how-to-validate-urls-with-regular-expressions/""" + url = style[key].encode('utf-8') if key in style else '' + regexp = r"^(https?|ftp)://[a-z0-9-]+(\.[a-z0-9-]+)+(/[\w-]+)*/[\w-]+\.(gif|png|jpg)$" + return "<img src='%(url)s' alt='%(alt)s'/>" % {'alt': alt, 'url': url} if re.match(regexp, url) else alt + + request.write(""" + <html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <meta name="keywords" content="%(keywords)s"> + <meta name="description" content="%(description)s"> + <link rel="alternate" type="application/atom+xml" href="%(base)s/atom.xml"/> + <link rel="stylesheet" type="text/css" href="%(root)scss/blog.css" /> + <link rel="icon" type="image/png" href="%(root)ssat_logo_16.png"> + <title>%(title)s</title> + </head> + <body> + <div class="mblog_title"><a href="%(base)s">%(banner_elt)s</a></div> + """ % {'base': base_url, + 'root': root_url, + 'user': user, + 'keywords': getFromData(C.STATIC_BLOG_PARAM_KEYWORDS), + 'description': getFromData(C.STATIC_BLOG_PARAM_DESCRIPTION), + 'title': getFromData(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user, + 'banner_elt': getImageFromData(C.STATIC_BLOG_PARAM_BANNER, user)}) + mblog_data = [(entry if isinstance(entry, tuple) else (entry, [])) for entry in mblog_data] + mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('published', 0)))) + for entry in mblog_data: + self.__render_html_entry(entry[0], base_url, request) + comments = sorted(entry[1], key=lambda entry: (float(entry.get('published', 0)))) + for comment in comments: + self.__render_html_entry(comment, base_url, request) + request.write('</body></html>') + request.finish() + + def __render_html_entry(self, entry, base_url, request): + """Render one microblog entry. + @param entry: the microblog entry + @param base_url: the base url of the blog + @param request: the HTTP request + """ + timestamp = float(entry.get('published', 0)) + datetime_ = datetime.fromtimestamp(timestamp) + is_comment = entry['type'] == 'comment' + if is_comment: + author = (_("comment from %s") % entry['author']).encode('utf-8') + item_link = '' + else: + author = ' ' + item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8') + + def getText(key): + if ('%s_xhtml' % key) in entry: + return entry['%s_xhtml' % key].encode('utf-8') + elif key in entry: + processor = addURLToText if key.startswith('content') else sanitizeHtml + return processor(entry[key]).encode('utf-8') + return '' + + def addMainItemLink(elem): + if not item_link or not elem: + return elem + return """<a href="%(link)s" class="item_link">%(elem)s</a>""" % {'link': item_link, 'elem': elem} + + header = addMainItemLink("""<div class="mblog_header"> + <div class="mblog_metadata"> + <div class="mblog_author">%(author)s</div> + <div class="mblog_timestamp">%(date)s</div> + </div> + </div>""" % {'author': author, 'date': datetime_}) + + title = addMainItemLink(getText('title')) + body = getText('content') + if title: # insert the title within the body + body = """<h1>%(title)s</h1>\n%(body)s""" % {'title': title, 'body': body} + + request.write("""<div class="mblog_entry %(extra_style)s"> + %(header)s + <span class="mblog_content">%(content)s</span> + </div>""" % + {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '', + 'item_link': item_link, + 'header': header, + 'content': body}) + + def render_atom_feed(self, feed, request): + request.write(feed.encode('utf-8')) + request.finish() + + def render_error_blog(self, error, request, profile): + request.write(MicroBlog.ERROR_TEMPLATE % {'root': '../' * len(request.postpath), + 'message': "Can't access requested data"}) + request.finish()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/server/constants.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,42 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a SAT frontend +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 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/>. + +from libervia.common import constants + + +class Const(constants.Const): + + APP_NAME = 'Libervia' + SERVICE_PROFILE = 'libervia' # the SàT profile that is used for exporting the service + + TIMEOUT = 300 # Session's time out, after that the user will be disconnected + HTML_DIR = "html/" + SERVER_CSS_DIR = "server_css/" + MEDIA_DIR = "media/" + AVATARS_DIR = "avatars/" + CARDS_DIR = "games/cards/tarot" + + ERRNUM_BRIDGE_ERRBACK = 0 # FIXME + ERRNUM_LIBERVIA = 0 # FIXME + + # Security limit for Libervia (get/set params) + SECURITY_LIMIT = 5 + + # Security limit for Libervia server_side + SERVER_SECURITY_LIMIT = constants.Const.NO_SECURITY_LIMIT
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/server/html_tools.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +def sanitizeHtml(text): + """Sanitize HTML by escaping everything""" + #this code comes from official python wiki: http://wiki.python.org/moin/EscapingHtml + html_escape_table = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", + } + + return "".join(html_escape_table.get(c,c) for c in text) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/server/server.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,1168 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 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/>. + +from twisted.application import service +from twisted.internet import glib2reactor +glib2reactor.install() +from twisted.internet import reactor, defer +from twisted.web import server +from twisted.web.static import File +from twisted.web.resource import Resource, NoResource +from twisted.web.util import Redirect, redirectTo +from twisted.python.components import registerAdapter +from twisted.python.failure import Failure +from twisted.words.protocols.jabber.jid import JID + +from txjsonrpc.web import jsonrpc +from txjsonrpc import jsonrpclib + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService +from sat.core.i18n import _, D_ +from sat.tools.xml_tools import paramsXML2XMLUI + +import re +import glob +import os.path +import sys +import tempfile +import shutil +import uuid +from zope.interface import Interface, Attribute, implements +from xml.dom import minidom +from httplib import HTTPS_PORT + +try: + import OpenSSL + from twisted.internet import ssl + ssl_available = True +except: + ssl_available = False + +from libervia.server.constants import Const as C +from libervia.server.blog import MicroBlog + + +class ISATSession(Interface): + profile = Attribute("Sat profile") + jid = Attribute("JID associated with the profile") + + +class SATSession(object): + implements(ISATSession) + def __init__(self, session): + self.profile = None + self.jid = None + + +class LiberviaSession(server.Session): + sessionTimeout = C.TIMEOUT + + def __init__(self, *args, **kwargs): + self.__lock = False + server.Session.__init__(self, *args, **kwargs) + + def lock(self): + """Prevent session from expiring""" + self.__lock = True + self._expireCall.reset(sys.maxint) + + def unlock(self): + """Allow session to expire again, and touch it""" + self.__lock = False + self.touch() + + def touch(self): + if not self.__lock: + server.Session.touch(self) + +class ProtectedFile(File): + """A File class which doens't show directory listing""" + + def directoryListing(self): + return NoResource() + +class SATActionIDHandler(object): + """Manage SàT action action_id lifecycle""" + ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored + + def __init__(self): + self.waiting_ids = {} + + def waitForId(self, callback, action_id, profile, *args, **kwargs): + """Wait for an action result + @param callback: method to call when action gave a result back + @param action_id: action_id to wait for + @param profile: %(doc_profile)s + @param *args: additional argument to pass to callback + @param **kwargs: idem""" + action_tuple = (action_id, profile) + self.waiting_ids[action_tuple] = (callback, args, kwargs) + reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple) + + def purgeID(self, action_tuple): + """Called when an action_id has not be handled in time""" + if action_tuple in self.waiting_ids: + log.warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple) + del self.waiting_ids[action_tuple] + + def actionResultCb(self, answer_type, action_id, data, profile): + """Manage the actionResult signal""" + action_tuple = (action_id, profile) + if action_tuple in self.waiting_ids: + callback, args, kwargs = self.waiting_ids[action_tuple] + del self.waiting_ids[action_tuple] + callback(answer_type, action_id, data, *args, **kwargs) + +class JSONRPCMethodManager(jsonrpc.JSONRPC): + + def __init__(self, sat_host): + jsonrpc.JSONRPC.__init__(self) + self.sat_host=sat_host + + def asyncBridgeCall(self, method_name, *args, **kwargs): + """Call an asynchrone bridge method and return a deferred + @param method_name: name of the method as a unicode + @return: a deferred which trigger the result + + """ + d = defer.Deferred() + + def _callback(*args): + if not args: + d.callback(None) + else: + if len(args) != 1: + Exception("Multiple return arguments not supported") + d.callback(args[0]) + + def _errback(result): + d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(result)))) + + kwargs["callback"] = _callback + kwargs["errback"] = _errback + getattr(self.sat_host.bridge, method_name)(*args, **kwargs) + return d + + +class MethodHandler(JSONRPCMethodManager): + + def __init__(self, sat_host): + JSONRPCMethodManager.__init__(self, sat_host) + self.authorized_params = None + + def render(self, request): + self.session = request.getSession() + profile = ISATSession(self.session).profile + if not profile: + #user is not identified, we return a jsonrpc fault + parsed = jsonrpclib.loads(request.content.read()) + fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia + return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) + return jsonrpc.JSONRPC.render(self, request) + + def jsonrpc_getProfileJid(self): + """Return the jid of the profile""" + sat_session = ISATSession(self.session) + profile = sat_session.profile + sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile)) + return sat_session.jid.full() + + def jsonrpc_disconnect(self): + """Disconnect the profile""" + sat_session = ISATSession(self.session) + profile = sat_session.profile + self.sat_host.bridge.disconnect(profile) + + def jsonrpc_getContacts(self): + """Return all passed args.""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getContacts(profile) + + def jsonrpc_addContact(self, entity, name, groups): + """Subscribe to contact presence, and add it to the given groups""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.addContact(entity, profile) + self.sat_host.bridge.updateContact(entity, name, groups, profile) + + def jsonrpc_delContact(self, entity): + """Remove contact from contacts list""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.delContact(entity, profile) + + def jsonrpc_updateContact(self, entity, name, groups): + """Update contact's roster item""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.updateContact(entity, name, groups, profile) + + def jsonrpc_subscription(self, sub_type, entity, name, groups): + """Confirm (or infirm) subscription, + and setup user roster in case of subscription""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.subscription(sub_type, entity, profile) + if sub_type == 'subscribed': + self.sat_host.bridge.updateContact(entity, name, groups, profile) + + def jsonrpc_getWaitingSub(self): + """Return list of room already joined by user""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getWaitingSub(profile) + + def jsonrpc_setStatus(self, presence, status): + """Change the presence and/or status + @param presence: value from ("", "chat", "away", "dnd", "xa") + @param status: any string to describe your status + """ + profile = ISATSession(self.session).profile + self.sat_host.bridge.setPresence('', presence, {'': status}, profile) + + + def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}): + """send message""" + profile = ISATSession(self.session).profile + return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile) + + def jsonrpc_sendMblog(self, type_, dest, text, extra={}): + """ Send microblog message + @param type_: one of "PUBLIC", "GROUP" + @param dest: destinees (list of groups, ignored for "PUBLIC") + @param text: microblog's text + """ + profile = ISATSession(self.session).profile + extra['allow_comments'] = 'True' + + if not type_: # auto-detect + type_ = "PUBLIC" if dest == [] else "GROUP" + + if type_ in ("PUBLIC", "GROUP") and text: + if type_ == "PUBLIC": + #This text if for the public microblog + print "sending public blog" + return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile) + else: + print "sending group blog" + dest = dest if isinstance(dest, list) else [dest] + return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile) + else: + raise Exception("Invalid data") + + def jsonrpc_deleteMblog(self, pub_data, comments): + """Delete a microblog node + @param pub_data: a tuple (service, comment node identifier, item identifier) + @param comments: comments node identifier (for main item) or False + """ + profile = ISATSession(self.session).profile + return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile) + + def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}): + """Modify a microblog node + @param pub_data: a tuple (service, comment node identifier, item identifier) + @param comments: comments node identifier (for main item) or False + @param message: new message + @param extra: dict which option name as key, which can be: + - allow_comments: True to accept an other level of comments, False else (default: False) + - rich: if present, contain rich text in currently selected syntax + """ + profile = ISATSession(self.session).profile + if comments: + extra['allow_comments'] = 'True' + return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile) + + def jsonrpc_sendMblogComment(self, node, text, extra={}): + """ Send microblog message + @param node: url of the comments node + @param text: comment + """ + profile = ISATSession(self.session).profile + if node and text: + return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile) + else: + raise Exception("Invalid data") + + def jsonrpc_getMblogs(self, publisher_jid, item_ids): + """Get specified microblogs posted by a contact + @param publisher_jid: jid of the publisher + @param item_ids: list of microblogs items IDs + @return list of microblog data (dict)""" + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile) + return d + + def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids): + """Get specified microblogs posted by a contact and their comments + @param publisher_jid: jid of the publisher + @param item_ids: list of microblogs items IDs + @return list of couple (microblog data, list of microblog data)""" + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile) + return d + + def jsonrpc_getLastMblogs(self, publisher_jid, max_item): + """Get last microblogs posted by a contact + @param publisher_jid: jid of the publisher + @param max_item: number of items to ask + @return list of microblog data (dict)""" + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile) + return d + + def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item): + """Get lasts microblogs posted by several contacts at once + @param publishers_type: one of "ALL", "GROUP", "JID" + @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids) + @param max_item: number of items to ask + @return: dictionary key=publisher's jid, value=list of microblog data (dict)""" + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile) + self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile) + return d + + def jsonrpc_getMblogComments(self, service, node): + """Get all comments of given node + @param service: jid of the service hosting the node + @param node: comments node + """ + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile) + return d + + + def jsonrpc_getPresenceStatuses(self): + """Get Presence information for connected contacts""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getPresenceStatuses(profile) + + def jsonrpc_getHistory(self, from_jid, to_jid, size, between): + """Return history for the from_jid/to_jid couple""" + sat_session = ISATSession(self.session) + profile = sat_session.profile + sat_jid = sat_session.jid + if not sat_jid: + log.error("No jid saved for this profile") + return {} + if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost(): + log.error("Trying to get history from a different jid, maybe a hack attempt ?") + return {} + d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile) + def show(result_dbus): + result = [] + for line in result_dbus: + #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types + # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query + timestamp, from_jid, to_jid, message, mess_type, extra = line + result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra))) + return result + d.addCallback(show) + return d + + def jsonrpc_joinMUC(self, room_jid, nick): + """Join a Multi-User Chat room + @room_jid: leave empty string to generate a unique name + """ + profile = ISATSession(self.session).profile + try: + if room_jid != "": + room_jid = JID(room_jid).userhost() + except: + log.warning('Invalid room jid') + return + d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile) + return d + + def jsonrpc_inviteMUC(self, contact_jid, room_jid): + """Invite a user to a Multi-User Chat room""" + profile = ISATSession(self.session).profile + try: + room_jid = JID(room_jid).userhost() + except: + log.warning('Invalid room jid') + return + room_id = room_jid.split("@")[0] + service = room_jid.split("@")[1] + self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile) + + def jsonrpc_mucLeave(self, room_jid): + """Quit a Multi-User Chat room""" + profile = ISATSession(self.session).profile + try: + room_jid = JID(room_jid) + except: + log.warning('Invalid room jid') + return + self.sat_host.bridge.mucLeave(room_jid.userhost(), profile) + + def jsonrpc_getRoomsJoined(self): + """Return list of room already joined by user""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getRoomsJoined(profile) + + def jsonrpc_launchTarotGame(self, other_players, room_jid=""): + """Create a room, invite the other players and start a Tarot game + @param room_jid: leave empty string to generate a unique room name + """ + profile = ISATSession(self.session).profile + try: + if room_jid != "": + room_jid = JID(room_jid).userhost() + except: + log.warning('Invalid room jid') + return + self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile) + + def jsonrpc_getTarotCardsPaths(self): + """Give the path of all the tarot cards""" + _join = os.path.join + _media_dir = _join(self.sat_host.media_dir,'') + return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png'))); + + def jsonrpc_tarotGameReady(self, player, referee): + """Tell to the server that we are ready to start the game""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.tarotGameReady(player, referee, profile) + + def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards): + """Tell to the server the cards we want to put on the table""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile) + + def jsonrpc_launchRadioCollective(self, invited, room_jid=""): + """Create a room, invite people, and start a radio collective + @param room_jid: leave empty string to generate a unique room name + """ + profile = ISATSession(self.session).profile + try: + if room_jid != "": + room_jid = JID(room_jid).userhost() + except: + log.warning('Invalid room jid') + return + self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile) + + def jsonrpc_getEntityData(self, jid, keys): + """Get cached data for an entit + @param jid: jid of contact from who we want data + @param keys: name of data we want (list) + @return: requested data""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getEntityData(jid, keys, profile) + + def jsonrpc_getCard(self, jid): + """Get VCard for entiry + @param jid: jid of contact from who we want data + @return: id to retrieve the profile""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getCard(jid, profile) + + def jsonrpc_getAccountDialogUI(self): + """Get the dialog for managing user account + @return: XML string of the XMLUI""" + profile = ISATSession(self.session).profile + return self.sat_host.bridge.getAccountDialogUI(profile) + + def jsonrpc_getParamsUI(self): + """Return the parameters XML for profile""" + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("getParams", C.SECURITY_LIMIT, C.APP_NAME, profile) + + def setAuthorizedParams(params_xml): + if self.authorized_params is None: + self.authorized_params = {} + for cat in minidom.parseString(params_xml.encode('utf-8')).getElementsByTagName("category"): + params = cat.getElementsByTagName("param") + params_list = [param.getAttribute("name") for param in params] + self.authorized_params[cat.getAttribute("name")] = params_list + if self.authorized_params: + return params_xml + else: + return None + + d.addCallback(setAuthorizedParams) + + d.addCallback(lambda params_xml: paramsXML2XMLUI(params_xml) if params_xml else "") + + return d + + def jsonrpc_asyncGetParamA(self, param, category, attribute="value"): + """Return the parameter value for profile""" + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile) + return d + + def jsonrpc_setParam(self, name, value, category): + profile = ISATSession(self.session).profile + if category in self.authorized_params and name in self.authorized_params[category]: + return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile) + else: + log.warning("Trying to set parameter '%s' in category '%s' without authorization!!!" + % (name, category)) + + def jsonrpc_launchAction(self, callback_id, data): + #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed + # a security system with authorised callback_id must be implemented, similar to the one for authorised params + profile = ISATSession(self.session).profile + d = self.asyncBridgeCall("launchAction", callback_id, data, profile) + return d + + def jsonrpc_chatStateComposing(self, to_jid_s): + """Call the method to process a "composing" state. + @param to_jid_s: contact the user is composing to + """ + profile = ISATSession(self.session).profile + self.sat_host.bridge.chatStateComposing(to_jid_s, profile) + + def jsonrpc_getNewAccountDomain(self): + """@return: the domain for new account creation""" + d = self.asyncBridgeCall("getNewAccountDomain") + return d + + def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data): + """Send the user's answer to any previous 'askConfirmation' signal""" + profile = ISATSession(self.session).profile + self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile) + + def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT): + """ Convert a text between two syntaxes + @param text: text to convert + @param syntax_from: source syntax (e.g. "markdown") + @param syntax_to: dest syntax (e.g.: "XHTML") + @param safe: clean resulting XHTML to avoid malicious code if True (forced here) + @return: converted text """ + profile = ISATSession(self.session).profile + return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile) + + +class Register(JSONRPCMethodManager): + """This class manage the registration procedure with SàT + It provide an api for the browser, check password and setup the web server""" + + def __init__(self, sat_host): + JSONRPCMethodManager.__init__(self, sat_host) + self.profiles_waiting={} + self.request=None + + def getWaitingRequest(self, profile): + """Tell if a profile is trying to log in""" + if self.profiles_waiting.has_key(profile): + return self.profiles_waiting[profile] + else: + return None + + def render(self, request): + """ + Render method with some hacks: + - if login is requested, try to login with form data + - except login, every method is jsonrpc + - user doesn't need to be authentified for explicitely listed methods, but must be for all others + """ + if request.postpath == ['login']: + return self.loginOrRegister(request) + _session = request.getSession() + parsed = jsonrpclib.loads(request.content.read()) + method = parsed.get("method") + if method not in ['isRegistered', 'registerParams', 'getMenus']: + #if we don't call these methods, we need to be identified + profile = ISATSession(_session).profile + if not profile: + #user is not identified, we return a jsonrpc fault + fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia + return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) + self.request = request + return jsonrpc.JSONRPC.render(self, request) + + def loginOrRegister(self, request): + """This method is called with the POST information from the registering form. + + @param request: request of the register form + @return: a constant indicating the state: + - BAD REQUEST: something is wrong in the request (bad arguments) + - a return value from self._loginAccount or self._registerNewAccount + """ + try: + submit_type = request.args['submit_type'][0] + except KeyError: + return "BAD REQUEST" + + if submit_type == 'register': + return self._registerNewAccount(request) + elif submit_type == 'login': + return self._loginAccount(request) + return Exception('Unknown submit type') + + def _loginAccount(self, request): + """Try to authenticate the user with the request information. + @param request: request of the register form + @return: a constant indicating the state: + - BAD REQUEST: something is wrong in the request (bad arguments) + - AUTH ERROR: either the profile (login) or the password is wrong + - ALREADY WAITING: a request has already been submitted for this profile + - server.NOT_DONE_YET: the profile is being processed, the return + value will be given by self._logged or self._logginError + """ + try: + login_ = request.args['login'][0] + password_ = request.args['login_password'][0] + except KeyError: + return "BAD REQUEST" + + if login_.startswith('@'): + raise Exception('No profile_key allowed') + + profile_check = self.sat_host.bridge.getProfileName(login_) + if not profile_check or profile_check != login_ or not password_: + # profiles with empty passwords are restricted to local frontends + return "AUTH ERROR" + + if login_ in self.profiles_waiting: + return "ALREADY WAITING" + + def auth_eb(ignore=None): + self.__cleanWaiting(login_) + log.info("Profile %s doesn't exist or the submitted password is wrong" % login_) + request.write("AUTH ERROR") + request.finish() + + self.profiles_waiting[login_] = request + d = self.asyncBridgeCall("asyncConnect", login_, password_) + d.addCallbacks(lambda connected: self._logged(login_, request) if connected else None, auth_eb) + + return server.NOT_DONE_YET + + def _registerNewAccount(self, request): + """Create a new account, or return error + @param request: request of the register form + @return: a constant indicating the state: + - BAD REQUEST: something is wrong in the request (bad arguments) + - REGISTRATION: new account has been successfully registered + - ALREADY EXISTS: the given profile already exists + - INTERNAL or 'Unknown error (...)' + - server.NOT_DONE_YET: the profile is being processed, the return + value will be given later (one of those previously described) + """ + try: + profile = login = request.args['register_login'][0] + password = request.args['register_password'][0] + email = request.args['email'][0] + except KeyError: + return "BAD REQUEST" + if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \ + not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \ + len(password) < C.PASSWORD_MIN_LENGTH: + return "BAD REQUEST" + + def registered(result): + request.write('REGISTRATION') + request.finish() + + def registeringError(failure): + reason = failure.value.faultString + if reason == "ConflictError": + request.write('ALREADY EXISTS') + elif reason == "InternalError": + request.write('INTERNAL') + else: + log.error('Unknown registering error: %s' % (reason,)) + request.write('Unknown error (%s)' % reason) + request.finish() + + d = self.asyncBridgeCall("registerSatAccount", email, password, profile) + d.addCallback(registered) + d.addErrback(registeringError) + return server.NOT_DONE_YET + + def __cleanWaiting(self, login): + """Remove login from waiting queue""" + try: + del self.profiles_waiting[login] + except KeyError: + pass + + def _logged(self, profile, request): + """Set everything when a user just logged in + + @param profile + @param request + @return: a constant indicating the state: + - LOGGED + - SESSION_ACTIVE + """ + self.__cleanWaiting(profile) + _session = request.getSession() + sat_session = ISATSession(_session) + if sat_session.profile: + log.error(('/!\\ Session has already a profile, this should NEVER happen!')) + request.write('SESSION_ACTIVE') + request.finish() + return + sat_session.profile = profile + self.sat_host.prof_connected.add(profile) + + def onExpire(): + log.info("Session expired (profile=%s)" % (profile,)) + try: + #We purge the queue + del self.sat_host.signal_handler.queue[profile] + except KeyError: + pass + #and now we disconnect the profile + self.sat_host.bridge.disconnect(profile) + + _session.notifyOnExpire(onExpire) + + request.write('LOGGED') + request.finish() + + def _logginError(self, login, request, error_type): + """Something went wrong during logging in + @return: error + """ + self.__cleanWaiting(login) + return error_type + + def jsonrpc_isConnected(self): + _session = self.request.getSession() + profile = ISATSession(_session).profile + return self.sat_host.bridge.isConnected(profile) + + def jsonrpc_connect(self): + _session = self.request.getSession() + profile = ISATSession(_session).profile + if self.profiles_waiting.has_key(profile): + raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia + self.profiles_waiting[profile] = self.request + self.sat_host.bridge.connect(profile) + return server.NOT_DONE_YET + + def jsonrpc_isRegistered(self): + """ + @return: a couple (registered, message) with: + - registered: True if the user is already registered, False otherwise + - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise + """ + _session = self.request.getSession() + profile = ISATSession(_session).profile + if bool(profile): + return (True, None) + return (False, self.__getSecurityWarning()) + + def jsonrpc_registerParams(self): + """Register the frontend specific parameters""" + params = """ + <params> + <individual> + <category name="%(category_name)s" label="%(category_label)s"> + <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/> + </category> + </individual> + </params> + """ % { + 'category_name': C.ENABLE_UNIBOX_KEY, + 'category_label': _(C.ENABLE_UNIBOX_KEY), + 'param_name': C.ENABLE_UNIBOX_PARAM, + 'param_label': _(C.ENABLE_UNIBOX_PARAM) + } + + self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME) + + def jsonrpc_getMenus(self): + """Return the parameters XML for profile""" + # XXX: we put this method in Register because we get menus before being logged + return self.sat_host.bridge.getMenus('', C.SECURITY_LIMIT) + + def __getSecurityWarning(self): + """@return: a security warning message, or None if the connection is secure""" + if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning: + return None + text = D_("You are about to connect to an unsecured service.") + if self.sat_host.connection_type == 'both': + new_port = (':%s' % self.sat_host.port_https_ext) if self.sat_host.port_https_ext != HTTPS_PORT else '' + url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port) + text += D_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'url': url} + return text + + +class SignalHandler(jsonrpc.JSONRPC): + + def __init__(self, sat_host): + Resource.__init__(self) + self.register=None + self.sat_host=sat_host + self.signalDeferred = {} + self.queue = {} + + def plugRegister(self, register): + self.register = register + + def jsonrpc_getSignals(self): + """Keep the connection alive until a signal is received, then send it + @return: (signal, *signal_args)""" + _session = self.request.getSession() + profile = ISATSession(_session).profile + if profile in self.queue: #if we have signals to send in queue + if self.queue[profile]: + return self.queue[profile].pop(0) + else: + #the queue is empty, we delete the profile from queue + del self.queue[profile] + _session.lock() #we don't want the session to expire as long as this connection is active + def unlock(signal, profile): + _session.unlock() + try: + source_defer = self.signalDeferred[profile] + if source_defer.called and source_defer.result[0] == "disconnected": + log.info(u"[%s] disconnected" % (profile,)) + _session.expire() + except IndexError: + log.error("Deferred result should be a tuple with fonction name first") + + self.signalDeferred[profile] = defer.Deferred() + self.request.notifyFinish().addBoth(unlock, profile) + return self.signalDeferred[profile] + + def getGenericCb(self, function_name): + """Return a generic function which send all params to signalDeferred.callback + function must have profile as last argument""" + def genericCb(*args): + profile = args[-1] + if not profile in self.sat_host.prof_connected: + return + if profile in self.signalDeferred: + self.signalDeferred[profile].callback((function_name,args[:-1])) + del self.signalDeferred[profile] + else: + if not self.queue.has_key(profile): + self.queue[profile] = [] + self.queue[profile].append((function_name, args[:-1])) + return genericCb + + def connected(self, profile): + assert(self.register) # register must be plugged + request = self.register.getWaitingRequest(profile) + if request: + self.register._logged(profile, request) + + def disconnected(self, profile): + if not profile in self.sat_host.prof_connected: + log.error("'disconnected' signal received for a not connected profile") + return + self.sat_host.prof_connected.remove(profile) + if profile in self.signalDeferred: + self.signalDeferred[profile].callback(("disconnected",)) + del self.signalDeferred[profile] + else: + if not self.queue.has_key(profile): + self.queue[profile] = [] + self.queue[profile].append(("disconnected",)) + + + def connectionError(self, error_type, profile): + assert(self.register) #register must be plugged + request = self.register.getWaitingRequest(profile) + if request: #The user is trying to log in + if error_type == "AUTH_ERROR": + _error_t = "AUTH ERROR" + else: + _error_t = "UNKNOWN" + self.register._logginError(profile, request, _error_t) + + def render(self, request): + """ + Render method wich reject access if user is not identified + """ + _session = request.getSession() + parsed = jsonrpclib.loads(request.content.read()) + profile = ISATSession(_session).profile + if not profile: + #user is not identified, we return a jsonrpc fault + fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia + return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) + self.request = request + return jsonrpc.JSONRPC.render(self, request) + +class UploadManager(Resource): + """This class manage the upload of a file + It redirect the stream to SàT core backend""" + isLeaf = True + NAME = 'path' #name use by the FileUpload + + def __init__(self, sat_host): + self.sat_host=sat_host + self.upload_dir = tempfile.mkdtemp() + self.sat_host.addCleanup(shutil.rmtree, self.upload_dir) + + def getTmpDir(self): + return self.upload_dir + + def _getFileName(self, request): + """Generate unique filename for a file""" + raise NotImplementedError + + def _fileWritten(self, request, filepath): + """Called once the file is actually written on disk + @param request: HTTP request object + @param filepath: full filepath on the server + @return: a tuple with the name of the async bridge method + to be called followed by its arguments. + """ + raise NotImplementedError + + def render(self, request): + """ + Render method with some hacks: + - if login is requested, try to login with form data + - except login, every method is jsonrpc + - user doesn't need to be authentified for isRegistered, but must be for all other methods + """ + filename = self._getFileName(request) + filepath = os.path.join(self.upload_dir, filename) + #FIXME: the uploaded file is fully loaded in memory at form parsing time so far + # (see twisted.web.http.Request.requestReceived). A custom requestReceived should + # be written in the futur. In addition, it is not yet possible to get progression informations + # (see http://twistedmatrix.com/trac/ticket/288) + + with open(filepath,'w') as f: + f.write(request.args[self.NAME][0]) + + def finish(d): + error = isinstance(d, Exception) or isinstance (d, Failure) + request.write('KO' if error else 'OK') + # TODO: would be great to re-use the original Exception class and message + # but it is lost in the middle of the backtrace and encapsulated within + # a DBusException instance --> extract the data from the backtrace? + request.finish() + + d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath)) + d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure)) + return server.NOT_DONE_YET + + +class UploadManagerRadioCol(UploadManager): + NAME = 'song' + + def _getFileName(self, request): + extension = os.path.splitext(request.args['filename'][0])[1] + return "%s%s" % (str(uuid.uuid4()), extension) # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type + + def _fileWritten(self, request, filepath): + """Called once the file is actually written on disk + @param request: HTTP request object + @param filepath: full filepath on the server + @return: a tuple with the name of the async bridge method + to be called followed by its arguments. + """ + profile = ISATSession(request.getSession()).profile + return ("radiocolSongAdded", request.args['referee'][0], filepath, profile) + + +class UploadManagerAvatar(UploadManager): + NAME = 'avatar_path' + + def _getFileName(self, request): + return str(uuid.uuid4()) + + def _fileWritten(self, request, filepath): + """Called once the file is actually written on disk + @param request: HTTP request object + @param filepath: full filepath on the server + @return: a tuple with the name of the async bridge method + to be called followed by its arguments. + """ + profile = ISATSession(request.getSession()).profile + return ("setAvatar", filepath, profile) + + +def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS + allowed_values = ('http', 'https', 'both') + if value not in allowed_values: + raise ValueError("%(given)s not in %(expected)s" % {'given': value, 'expected': str(allowed_values)}) + return value + + +def coerceDataDir(value): # called from Libervia.OPT_PARAMETERS + html = os.path.join(value, C.HTML_DIR) + if not os.path.isfile(os.path.join(html, 'libervia.html')): + raise ValueError("%s is not a Libervia's browser HTML directory" % os.path.realpath(html)) + server_css = os.path.join(value, C.SERVER_CSS_DIR) + if not os.path.isfile(os.path.join(server_css, 'blog.css')): + raise ValueError("%s is not a Libervia's server data directory" % os.path.realpath(server_css)) + return value + + +class Libervia(service.Service): + + DATA_DIR_DEFAULT = '' + OPT_PARAMETERS = [['connection_type', 't', 'https', "'http', 'https' or 'both' (to launch both servers).", coerceConnectionType], + ['port', 'p', 8080, 'The port number to listen HTTP on.', int], + ['port_https', 's', 8443, 'The port number to listen HTTPS on.', int], + ['port_https_ext', 'e', 0, 'The external port number used for HTTPS (0 means port_https value).', int], + ['ssl_certificate', 'c', 'libervia.pem', 'PEM certificate with both private and public parts.', str], + ['redirect_to_https', 'r', 1, 'Automatically redirect from HTTP to HTTPS.', int], + ['security_warning', 'w', 1, 'Warn user that he is about to connect on HTTP.', int], + # FIXME: twistd bugs when printing 'à' on "Unknown command" error (works on normal command listing) + ['passphrase', 'k', '', u"Passphrase for the SaT profile named '%s'" % C.SERVICE_PROFILE, str], + ['data_dir', 'd', DATA_DIR_DEFAULT, u'Data directory for Libervia', coerceDataDir], + ] + + def __init__(self, *args, **kwargs): + if not kwargs: + # During the loading of the twisted plugins, we just need the default values. + # This part is not executed when the plugin is actually started. + for name, value in [(option[0], option[2]) for option in self.OPT_PARAMETERS]: + kwargs[name] = value + self.initialised = defer.Deferred() + self.connection_type = kwargs['connection_type'] + self.port = kwargs['port'] + self.port_https = kwargs['port_https'] + self.port_https_ext = kwargs['port_https_ext'] + if not self.port_https_ext: + self.port_https_ext = self.port_https + self.ssl_certificate = kwargs['ssl_certificate'] + self.redirect_to_https = kwargs['redirect_to_https'] + self.security_warning = kwargs['security_warning'] + self.passphrase = kwargs['passphrase'] + self.data_dir = kwargs['data_dir'] + if self.data_dir == Libervia.DATA_DIR_DEFAULT: + coerceDataDir(self.data_dir) # this is not done when using the default value + self.html_dir = os.path.join(self.data_dir, C.HTML_DIR) + self.server_css_dir = os.path.join(self.data_dir, C.SERVER_CSS_DIR) + self._cleanup = [] + root = ProtectedFile(self.html_dir) + self.signal_handler = SignalHandler(self) + _register = Register(self) + _upload_radiocol = UploadManagerRadioCol(self) + _upload_avatar = UploadManagerAvatar(self) + self.signal_handler.plugRegister(_register) + self.sessions = {} #key = session value = user + self.prof_connected = set() #Profiles connected + self.action_handler = SATActionIDHandler() + ## bridge ## + try: + self.bridge=DBusBridgeFrontend() + except BridgeExceptionNoService: + print(u"Can't connect to SàT backend, are you sure it's launched ?") + sys.exit(1) + def backendReady(dummy): + self.bridge.register("connected", self.signal_handler.connected) + self.bridge.register("disconnected", self.signal_handler.disconnected) + self.bridge.register("connectionError", self.signal_handler.connectionError) + self.bridge.register("actionResult", self.action_handler.actionResultCb) + #core + for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']: + self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name)) + #plugins + for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat', + 'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers', + 'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers', + 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']: + self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin") + self.media_dir = self.bridge.getConfig('', 'media_dir') + self.local_dir = self.bridge.getConfig('', 'local_dir') + root.putChild('', Redirect('libervia.html')) + root.putChild('json_signal_api', self.signal_handler) + root.putChild('json_api', MethodHandler(self)) + root.putChild('register_api', _register) + root.putChild('upload_radiocol', _upload_radiocol) + root.putChild('upload_avatar', _upload_avatar) + root.putChild('blog', MicroBlog(self)) + root.putChild('css', ProtectedFile(self.server_css_dir)) + root.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) + root.putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR))) + root.putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg")) # We cheat for PoC because we know we are on the same host, so we use directly upload dir + self.site = server.Site(root) + self.site.sessionFactory = LiberviaSession + + self.bridge.getReady(lambda: self.initialised.callback(None), + lambda failure: self.initialised.errback(Exception(failure))) + self.initialised.addCallback(backendReady) + self.initialised.addErrback(lambda failure: log.error("Init error: %s" % failure)) + + def addCleanup(self, callback, *args, **kwargs): + """Add cleaning method to call when service is stopped + cleaning method will be called in reverse order of they insertion + @param callback: callable to call on service stop + @param *args: list of arguments of the callback + @param **kwargs: list of keyword arguments of the callback""" + self._cleanup.insert(0, (callback, args, kwargs)) + + def startService(self): + """Connect the profile for Libervia and start the HTTP(S) server(s)""" + def eb(e): + log.error(_("Connection failed: %s") % e) + self.stop() + + def initOk(dummy): + if not self.bridge.isConnected(C.SERVICE_PROFILE): + self.bridge.asyncConnect(C.SERVICE_PROFILE, self.passphrase, + callback=self._startService, errback=eb) + + self.initialised.addCallback(initOk) + + def _startService(self, dummy): + """Actually start the HTTP(S) server(s) after the profile for Libervia is connected. + @raise IOError: the certificate file doesn't exist + @raise OpenSSL.crypto.Error: the certificate file is invalid + """ + if self.connection_type in ('https', 'both'): + if not ssl_available: + raise(ImportError(_("Python module pyOpenSSL is not installed!"))) + try: + with open(os.path.expanduser(self.ssl_certificate)) as keyAndCert: + try: + cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read()) + except OpenSSL.crypto.Error as e: + log.error(_("The file '%s' must contain both private and public parts of the certificate") % self.ssl_certificate) + raise e + except IOError as e: + log.error(_("The file '%s' doesn't exist") % self.ssl_certificate) + raise e + reactor.listenSSL(self.port_https, self.site, cert.options()) + if self.connection_type in ('http', 'both'): + if self.connection_type == 'both' and self.redirect_to_https: + reactor.listenTCP(self.port, server.Site(RedirectToHTTPS(self.port, self.port_https_ext))) + else: + reactor.listenTCP(self.port, self.site) + + def stopService(self): + print "launching cleaning methods" + for callback, args, kwargs in self._cleanup: + callback(*args, **kwargs) + self.bridge.disconnect(C.SERVICE_PROFILE) + + def run(self): + reactor.run() + + def stop(self): + reactor.stop() + + +class RedirectToHTTPS(Resource): + + def __init__(self, old_port, new_port): + Resource.__init__(self) + self.isLeaf = True + self.old_port = old_port + self.new_port = new_port + + def render(self, request): + netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port) + url = "https://" + netloc + request.uri + return redirectTo(url, request) + + +registerAdapter(SATSession, server.Session, ISATSession)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/twisted/plugins/libervia_server.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> + +# 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/>. + +from twisted.internet import defer +if defer.Deferred.debug: + # if we are in debug mode, we want to use ipdb instead of pdb + try: + import ipdb + import pdb + pdb.set_trace = ipdb.set_trace + pdb.post_mortem = ipdb.post_mortem + except ImportError: + pass + +# XXX: We need to configure logs before any log method is used, so here is the best place. +from libervia.server.constants import Const as C +from sat.core import log_config +log_config.satConfigure(C.LOG_BACKEND_TWISTED, C) + +from zope.interface import implements + +from twisted.python import usage +from twisted.plugin import IPlugin +from twisted.application.service import IServiceMaker + +from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError +try: + from libervia.server.server import Libervia + opt_params = Libervia.OPT_PARAMETERS +except (ImportError, SystemExit): + # avoid raising an error when you call twisted and sat is not launched + opt_params = [] + + +class Options(usage.Options): + + # optArgs is not really useful in our case, we need more than a flag + optParameters = opt_params + + def __init__(self): + """You want to read SàT configuration file now in order to overwrite the hard-coded default values. + This is because the priority for the usage of the values is (from lowest to highest): + - hard-coded default values + - values from SàT configuration files + - values passed on the command line + If you do it later: after the command line options have been parsed, there's no good way to know + if the options values are the hard-coded ones or if they have been passed on the command line. + """ + config = SafeConfigParser() + config.read(C.CONFIG_FILES) + for index, param in list(enumerate(self.optParameters)): + # index is only used to not modify the loop variable "param" + name = param[0] + try: + value = config.get('libervia', name) + self.optParameters[index][2] = param[4](value) + except (NoSectionError, NoOptionError): + pass + usage.Options.__init__(self) + + +class LiberviaMaker(object): + implements(IServiceMaker, IPlugin) + + tapname = 'libervia' + # FIXME: twistd bugs when printing 'à' on "Unknown command" error (works on normal command listing) + description = u'The web frontend of Salut a Toi' + options = Options + + def makeService(self, options): + return Libervia(**dict(options)) # get rid of the usage.Option overload + + +# affectation to some variable is necessary for twisted introspection to work +serviceMaker = LiberviaMaker() +
--- a/twisted/plugins/libervia.py Fri May 16 11:51:10 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> - -# 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/>. - -from twisted.internet import defer -if defer.Deferred.debug: - # if we are in debug mode, we want to use ipdb instead of pdb - try: - import ipdb - import pdb - pdb.set_trace = ipdb.set_trace - pdb.post_mortem = ipdb.post_mortem - except ImportError: - pass - -# XXX: We need to configure logs before any log method is used, so here is the best place. -from constants import Const as C -from sat.core import log_config -log_config.satConfigure(C.LOG_BACKEND_TWISTED, C) - -from zope.interface import implements - -from twisted.python import usage -from twisted.plugin import IPlugin -from twisted.application.service import IServiceMaker - -from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError -from sat.core.constants import Const as C -try: - from libervia_server import Libervia - opt_params = Libervia.OPT_PARAMETERS -except (ImportError, SystemExit): - # avoid raising an error when you call twisted and sat is not launched - opt_params = [] - - -class Options(usage.Options): - - # optArgs is not really useful in our case, we need more than a flag - optParameters = opt_params - - def __init__(self): - """You want to read SàT configuration file now in order to overwrite the hard-coded default values. - This is because the priority for the usage of the values is (from lowest to highest): - - hard-coded default values - - values from SàT configuration files - - values passed on the command line - If you do it later: after the command line options have been parsed, there's no good way to know - if the options values are the hard-coded ones or if they have been passed on the command line. - """ - config = SafeConfigParser() - config.read(C.CONFIG_FILES) - for index, param in list(enumerate(self.optParameters)): - # index is only used to not modify the loop variable "param" - name = param[0] - try: - value = config.get('libervia', name) - self.optParameters[index][2] = param[4](value) - except (NoSectionError, NoOptionError): - pass - usage.Options.__init__(self) - - -class LiberviaMaker(object): - implements(IServiceMaker, IPlugin) - tapname = 'libervia' - description = u'The web frontend of Salut à Toi' - options = Options - - def makeService(self, options): - return Libervia(**dict(options)) # get rid of the usage.Option overload - - -# affectation to some variable is necessary for twisted introspection to work -serviceMaker = LiberviaMaker()