# HG changeset patch # User Goffi # Date 1402344926 -7200 # Node ID 97c72fe4a5f2dcc7224f39258e9b2b37fa4bfb01 # Parent 01880aa8ea2d6b7d19c00aa142939004fa405922 browser_side: import fixes: - moved browser modules in a sat_browser packages, to avoid import conflicts with std lib (e.g. logging), and let pyjsbuild work normaly - refactored bad import practices: classes are most of time not imported directly, module is imported instead. diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/__init__.py diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/base_panels.py --- a/src/browser/base_panels.py Mon Jun 09 20:37:28 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 - -# 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 . - -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, "%(timestamp)s %(nick)s %(msg)s" % - {"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("
%s%s%s
" % (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 or . - - 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 + - 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/base_widget.py --- a/src/browser/base_widget.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,732 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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, expect=True): - return self.getParent(WidgetsPanel, expect) - - def getParent(self, class_=None, expect=True): - """Return the closest ancestor of the specified class. - - 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 expect: set to True if the parent is expected (print a message if not found) - @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 expect: - 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/card_game.py --- a/src/browser/card_game.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,387 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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 win !" - else: - title = "You loose :(" - 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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/constants.py --- a/src/browser/constants.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -#!/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 . - -from libervia.common.constants import Const as C - - -# Auxiliary functions -param_to_bool = lambda value: value == 'true' - - -class Const(C): - """Add here the constants that are only used by the browser side.""" - - # Parameters that have an incidence on UI display/refresh: - # - they can be any parameter (not necessarily specific to Libervia) - # - 'cast_from_str' is a method used to eventually convert to a non string type - # - 'initial_value' is used to initialize the display before any profile connection - UI_PARAMS = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM, - "category": C.COMPOSITION_KEY, - "cast_from_str": param_to_bool, - "initial_value": False - }, - } diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/contact.py --- a/src/browser/contact.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,419 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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 = "(*) " - 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 [child.group for child 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_s, name=None): - """Add a contact - - @param jid_s (str): JID as unicode - @param name (str): nickname - """ - def item_cb(item): - self.context_menu.registerRightClickSender(item) - GenericContactList.add(self, jid_s, 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_s, attributes, groups): - """Add a contact to the panel if it doesn't exist, update it else - @param jid_s: jid userhost as unicode - @param attributes: cf SàT Bridge API's newContact - @param groups: list of groups""" - _current_groups = self.getContactGroups(jid_s) - _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_s) - 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_s) - - # We add the contact to contact list, it will check if contact already exists - self._contact_list.add(jid_s) - - 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_s): - """Get groups where contact is - @param group: string of single group, or list of string - @param contact_jid_s: jid to test, as unicode - """ - result = set() - for group in self.groups: - if self.isContactInGroup(group, contact_jid_s): - 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") diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/contact_group.py --- a/src/browser/contact_group.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,237 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2013, 2014 Adrien Cossa - -# 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 . - -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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/dialog.py --- a/src/browser/dialog.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,547 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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 = "" - - 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/file_tools.py --- a/src/browser/file_tools.py Mon Jun 09 20:37:28 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 - -# 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 . - -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': 'Submitting, please wait...', - '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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/html_tools.py --- a/src/browser/html_tools.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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"^(
| |\s)+", "", html) - cleaned = re.sub(r"(
| |\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', '
') diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/jid.py --- a/src/browser/jid.py Mon Jun 09 20:37:28 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 - -# 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 . - - -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 diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/libervia_main.py --- a/src/browser/libervia_main.py Mon Jun 09 20:37:28 2014 +0200 +++ b/src/browser/libervia_main.py Mon Jun 09 22:15:26 2014 +0200 @@ -20,7 +20,7 @@ import pyjd # this is dummy in pyjs ### logging configuration ### -import logging +from sat_browser import logging logging.configure() from sat.core.log import getLogger log = getLogger(__name__) @@ -37,18 +37,17 @@ 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 -import dialog -from jid import JID -from xmlui import XMLUI -from html_tools import html_sanitize -from notification import Notification +from sat_browser import register +from sat_browser import contact +from sat_browser import base_widget +from sat_browser import panels +from sat_browser import dialog +from sat_browser import jid +from sat_browser import xmlui +from sat_browser import html_tools +from sat_browser import notification -from constants import Const as C +from sat_browser.constants import Const as C MAX_MBLOG_CACHE = 500 # Max microblog entries kept in memories @@ -56,7 +55,7 @@ # 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 +# and added to new base_widget.WidgetsPanel, or replaced to the expected # position if the previous and the new parent are the same. REUSE_EXISTING_LIBERVIA_WIDGETS = True @@ -178,7 +177,7 @@ self.bridge_signals = BridgeSignals(self) self.uni_box = None self.status_panel = HTML('
') - self.contact_panel = ContactPanel(self) + self.contact_panel = contact.ContactPanel(self) self.panel = panels.MainPanel(self) self.discuss_panel = self.panel.discuss_panel self.tab_panel = self.panel.tab_panel @@ -189,7 +188,7 @@ 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() + self.notification = notification.Notification() DOM.addEventPreview(self) self._register = RegisterCall() self._register.call('getMenus', self.panel.menu.createMenus) @@ -204,15 +203,15 @@ 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") + if not isinstance(wid, base_widget.WidgetsPanel): + log.error("Tab widget is not a base_widget.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): + if not isinstance(widgets_panel, base_widget.WidgetsPanel): return selected = widgets_panel.selected @@ -293,7 +292,7 @@ @param wid: LiberviaWidget to add @param select: True to select the added tab """ - widgets_panel = WidgetsPanel(self) + widgets_panel = base_widget.WidgetsPanel(self) self.tab_panel.add(widgets_panel, label) widgets_panel.addWidget(wid) if select: @@ -316,7 +315,7 @@ def _isRegisteredCB(self, result): registered, warning = result if not registered: - self._register_box = RegisterBox(self.logged) + self._register_box = register.RegisterBox(self.logged) self._register_box.centerBox() self._register_box.show() if warning: @@ -395,7 +394,7 @@ # action was a one shot, nothing to do pass elif "xmlui" in data: - ui = XMLUI(self, xml_data=data['xmlui']) + ui = xmlui.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) @@ -420,8 +419,8 @@ 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 + for contact_ in contacts_data: + jid, attributes, groups = contact_ self._newContactCb(jid, attributes, groups) def _getSignalsCB(self, signal_data): @@ -487,9 +486,9 @@ elif name == 'chatStateReceived': self._chatStateReceivedCb(*args) - def _getParamsUICB(self, xmlui): + def _getParamsUICB(self, xml_ui): """Hide the parameters item if there's nothing to display""" - if not xmlui: + if not xml_ui: self.panel.menu.removeItemParams() def _ownBlogsFills(self, mblogs): @@ -503,7 +502,7 @@ _groups = set(mblog['groups'].split() if mblog['groups'] else []) else: _groups = None - mblog_entry = MicroblogItem(mblog) + mblog_entry = panels.MicroblogItem(mblog) self.mblog_cache.append((_groups, mblog_entry)) if len(self.mblog_cache) > MAX_MBLOG_CACHE: @@ -517,7 +516,7 @@ del self.init_cache def _getProfileJidCB(self, jid): - self.whoami = JID(jid) + self.whoami = jid.JID(jid) #we can now ask our status self.bridge.call('getPresenceStatuses', self._getPresenceStatusesCb) #the rooms where we are @@ -541,7 +540,7 @@ if not self.initialised: self.init_cache.append((sender, event_type, data)) return - sender = JID(sender).bare + sender = jid.JID(sender).bare if event_type == "MICROBLOG": if not 'content' in data: log.warning("No content found in microblog data") @@ -550,7 +549,7 @@ _groups = set(data['groups'].split() if data['groups'] else []) else: _groups = None - mblog_entry = MicroblogItem(data) + mblog_entry = panels.MicroblogItem(data) for lib_wid in self.libervia_widgets: if isinstance(lib_wid, panels.MicroblogPanel): @@ -588,7 +587,7 @@ @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""" + @param mblog_entry: panels.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) @@ -669,8 +668,8 @@ return lib_wid def _newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): - _from = JID(from_jid) - _to = JID(to_jid) + _from = jid.JID(from_jid) + _to = jid.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) @@ -691,7 +690,7 @@ self.contact_panel.setContactMessageWaiting(other.bare, True) def _presenceUpdateCb(self, entity, show, priority, statuses): - entity_jid = JID(entity) + entity_jid = 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: @@ -700,7 +699,7 @@ 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) + _target = jid.JID(room_jid) if _target not in self.room_list: self.room_list.append(_target) chat_panel = panels.ChatPanel(self, _target, type_='group') @@ -716,8 +715,8 @@ 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))' ????!! + # FIXME: room_list contains jid.JID instances so why MUST we do + # 'remove(room_jid)' and not 'remove(jid.JID(room_jid))' ????!! # This looks like a pyjamas bug --> check/report try: self.room_list.remove(room_jid) @@ -776,17 +775,17 @@ def _subscribeCb(self, sub_type, entity): if sub_type == 'subscribed': - dialog.InfoDialog('Subscription confirmation', 'The contact %s has added you to his/her contact list' % html_sanitize(entity)).show() + dialog.InfoDialog('Subscription confirmation', 'The contact %s has added you to his/her contact list' % html_tools.html_sanitize(entity)).show() self.getEntityMBlog(entity) elif sub_type == 'unsubscribed': - dialog.InfoDialog('Subscription refusal', 'The contact %s has refused to add you in his/her contact list' % html_sanitize(entity)).show() + dialog.InfoDialog('Subscription refusal', 'The contact %s has refused to add you in his/her contact list' % html_tools.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 %s want to add you in his/her contact list, do you accept ?' % html_sanitize(entity)) + msg = HTML('The contact %s want to add you in his/her contact list, do you accept ?' % html_tools.html_sanitize(entity)) def ok_cb(ignore): self.bridge.call('subscription', None, "subscribed", entity, '', _dialog.getSelectedGroups()) @@ -824,7 +823,7 @@ target = '@ALL@' nick = C.ALL_OCCUPANTS else: - from_jid = JID(from_jid_s) + from_jid = jid.JID(from_jid_s) target = from_jid.bare nick = from_jid.resource diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/list_manager.py --- a/src/browser/list_manager.py Mon Jun 09 20:37:28 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 - -# 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 . - -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 = 'x' - -# 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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/logging.py --- a/src/browser/logging.py Mon Jun 09 20:37:28 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 - -# 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 . -"""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 diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/menu.py --- a/src/browser/menu.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,271 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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...
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 = "%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("""Libervia, a Salut à Toi project
-
-You can contact the author at goffi@goffi.org
-Blog available (mainly in french) at http://www.goffi.org
-Project page: http://sat.goffi.org
-
-Any help welcome :) -

This project is dedicated to Roger Poisson

-""") - _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 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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/nativedom.py --- a/src/browser/nativedom.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,107 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -""" -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)) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/notification.py --- a/src/browser/notification.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -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.
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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/panels.py --- a/src/browser/panels.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1419 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# 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 . - -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.getUIParam('unibox') - self.setVisible(enable) - if enable and not self.unibox: - self.button = Button('') - 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" — ✍ " + "" % datetime.fromtimestamp(self.updated) - self.header.setHTML("""
- on - %(updated)s -
""" % {'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.
") - 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' - html = 'raw text' - title = _('Switch to raw text edition') - else: - image = '' - html = 'rich text' - 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 %s" - - 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) - elif not self.getNewMainEntry(): - addBox() - - def getNewMainEntry(self): - """Get the new entry being edited, or None if it doesn't exists. - - @return (MicroblogEntry): the new entry being edited. - """ - try: - first = self.vpanel.children[0] - except IndexError: - return None - assert(first.type == 'main_item') - return first if first.empty else None - - @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 comment 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""" - assert(isinstance(reverse, bool)) - 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 - condition_to_stop = child.empty or (child.published > entry.published) - if condition_to_stop != reverse: # != is XOR - 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 %s" % self.target - elif self.type == "group": - msg = "This message will be sent to all the participants of the multi-user room %s" % 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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/plugin_xep_0085.py --- a/src/browser/plugin_xep_0085.py Mon Jun 09 20:37:28 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 . - -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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/radiocol.py --- a/src/browser/radiocol.py Mon Jun 09 20:37:28 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 - -# 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 . - -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.
- Please do not submit files that are protected by copyright.
- Click here 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/register.py --- a/src/browser/register.py Mon Jun 09 20:37:28 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,255 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson -# Copyright (C) 2011, 2012 Adrien Vigneron - -# 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 . - -#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
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
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 == "PROFILE AUTH ERROR": - Window.alert(_('Your login and/or password is incorrect. Please try again')) - elif result == "XMPP AUTH ERROR": - # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus - Window.alert(_(u'Your SàT profile has been authenticated but the associated XMPP account failed to connect. Please use another SàT frontend to set another XMPP password.')) - 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,
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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/richtext.py --- a/src/browser/richtext.py Mon Jun 09 20:37:28 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 - -# 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 . - -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('' % - 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 = '

%s

%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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/__init__.py diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/base_panels.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/base_panels.py Mon Jun 09 22:15:26 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 + +# 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 . + +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 import strings + +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 + +import html_tools +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, "%(timestamp)s %(nick)s %(msg)s" % + {"timestamp": _date.strftime("%H:%M"), + "nick": "[%s]" % html_tools.sanitize(nick), + "msg_class": ' '.join(_msg_class), + "msg": strings.addURLToText(html_tools.sanitize(msg)) if not xhtml else html_tools.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("
%s%s%s
" % (html_tools.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 strings.addURLToText + - listen_keyboard: set to True to terminate the edition with or . + - 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_tools.sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.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 = strings.addURLToImage(text) + if self.options['enhance_display']: + text = strings.addURLToText(text) + self.display.setHTML(html_tools.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 + + 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/base_widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/base_widget.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,732 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +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, expect=True): + return self.getParent(WidgetsPanel, expect) + + def getParent(self, class_=None, expect=True): + """Return the closest ancestor of the specified class. + + 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 expect: set to True if the parent is expected (print a message if not found) + @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 expect: + 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/card_game.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/card_game.py Mon Jun 09 22:15:26 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 + +# 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 . + +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 + +import dialog +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: + dialog.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: + dialog.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.XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL']) + _dialog = 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 win !" + else: + title = "You loose :(" + body = xmlui.XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) + _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/constants.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,39 @@ +#!/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 . + +from libervia.common.constants import Const as C + + +# Auxiliary functions +param_to_bool = lambda value: value == 'true' + + +class Const(C): + """Add here the constants that are only used by the browser side.""" + + # Parameters that have an incidence on UI display/refresh: + # - they can be any parameter (not necessarily specific to Libervia) + # - 'cast_from_str' is a method used to eventually convert to a non string type + # - 'initial_value' is used to initialize the display before any profile connection + UI_PARAMS = {"unibox": {"name": C.ENABLE_UNIBOX_PARAM, + "category": C.COMPOSITION_KEY, + "cast_from_str": param_to_bool, + "initial_value": False + }, + } diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/contact.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,419 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +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 + +import base_panels +import base_widget +import panels +import html_tools + + +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(base_widget.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') + base_widget.DragLabel.__init__(self, group, "GROUP") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.MicroblogPanel, self.group) + + +class ContactLabel(base_widget.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') + base_widget.DragLabel.__init__(self, jid, "CONTACT") + if handleClick: + ClickHandler.__init__(self) + self.addClickListener(self) + + def _fill(self): + if self.waiting: + _wait_html = "(*) " + self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html, + 'name': html_tools.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(panels.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 [child.group for child 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 = base_panels.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 = panels.WebPanel(self.host, "/blog/%s" % node) + self.host.addTab("%s's blog" % node, web_panel) + else: + sender.onClick(sender) + + def add(self, jid_s, name=None): + """Add a contact + + @param jid_s (str): JID as unicode + @param name (str): nickname + """ + def item_cb(item): + self.context_menu.registerRightClickSender(item) + GenericContactList.add(self, jid_s, 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(base_widget.DragLabel, Label, ClickHandler): + def __init__(self, host, text): + Label.__init__(self, text) # , Element=DOM.createElement('div') + self.host = host + self.setStyleName('contactTitle') + base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE") + ClickHandler.__init__(self) + self.addClickListener(self) + + def onClick(self, sender): + self.host.getOrCreateLiberviaWidget(panels.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(), panels.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_s, attributes, groups): + """Add a contact to the panel if it doesn't exist, update it else + @param jid_s: jid userhost as unicode + @param attributes: cf SàT Bridge API's newContact + @param groups: list of groups""" + _current_groups = self.getContactGroups(jid_s) + _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_s) + 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_s) + + # We add the contact to contact list, it will check if contact already exists + self._contact_list.add(jid_s) + + 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_s): + """Get groups where contact is + @param group: string of single group, or list of string + @param contact_jid_s: jid to test, as unicode + """ + result = set() + for group in self.groups: + if self.isContactInGroup(group, contact_jid_s): + 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") diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/contact_group.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact_group.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 Adrien Cossa + +# 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 . + +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 + +import dialog +import list_manager +import contact + + +class ContactGroupManager(list_manager.ListManager): + """A manager for sub-panels to assign contacts to each group.""" + + def __init__(self, parent, keys_dict, contact_list, offsets, style): + list_manager.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: + list_manager.ListManager.removeContactKey(self, key) + self._parent.removeKeyFromAddGroupPanel(key) + + _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key) + _dialog.show() + + def removeFromRemainingList(self, contacts): + list_manager.ListManager.removeFromRemainingList(self, contacts) + self._parent.updateContactList(contacts=contacts) + + def addToRemainingList(self, contacts, ignore_key=None): + list_manager.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 = 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: + dialog.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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/dialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/dialog.py Mon Jun 09 22:15:26 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 + +# 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 . + +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 + +import base_panels + + +# 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 = "" + + 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 = base_panels.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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/file_tools.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/file_tools.py Mon Jun 09 22:15:26 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 + +# 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 . + +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': 'Submitting, please wait...', + '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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/html_tools.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/html_tools.py Mon Jun 09 22:15:26 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 + +# 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 . + +from sat_frontends.tools import xmltools + +import nativedom +import re + +dom = nativedom.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"^(
| |\s)+", "", html) + cleaned = re.sub(r"(
| |\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', '
') diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/jid.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/jid.py Mon Jun 09 22:15:26 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 + +# 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 . + + +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 diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/list_manager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/list_manager.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,608 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 Adrien Cossa + +# 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 . + +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 + +import base_panels +import base_widget + +# HTML content for the removal button (image or text) +REMOVE_BUTTON = 'x' + +# 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 = base_panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) + + +class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.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) + base_widget.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() + base_widget.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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/logging.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/logging.py Mon Jun 09 22:15:26 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 + +# 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 . +"""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 diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/menu.py Mon Jun 09 22:15:26 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 + +# 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 . + +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 + +import jid + +import file_tools +import xmlui +import panels +import dialog +import contact_group + + +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(file_tools.FileUploadPanel): + def __init__(self): + texts = {'ok_button': 'Upload avatar', + 'body': 'Please select an image to show as your avatar...
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!"} + file_tools.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 = "%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("""Libervia, a Salut à Toi project
+
+You can contact the author at goffi@goffi.org
+Blog available (mainly in french) at http://www.goffi.org
+Project page: http://sat.goffi.org
+
+Any help welcome :) +

This project is dedicated to Roger Poisson

+""") + _dialog = dialog.GenericDialog("About", _about) + _dialog.show() + + #Contact menu + def onManageContactGroups(self): + """Open the contact groups manager.""" + + def onCloseCallback(): + pass + + contact_group.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.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(xml_ui): + if not xml_ui: + return + body = xmlui.XMLUI(self.host, xml_ui) + _dialog = dialog.GenericDialog("Manage your account", body, options=['NO_CLOSE']) + body.setCloseCb(_dialog.close) + _dialog.show() + self.host.bridge.call('getAccountDialogUI', gotUI) + + def onParameters(self): + def gotParams(xml_ui): + if not xml_ui: + return + body = xmlui.XMLUI(self.host, xml_ui) + _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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/nativedom.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/nativedom.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +""" +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)) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/notification.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/notification.py Mon Jun 09 22:15:26 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.
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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/panels.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/panels.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,1419 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +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 + +import jid +import html_tools +import base_panels +import card_game +import radiocol +import menu +import dialog +import base_widget +import richtext +import contact +from constants import Const as C +import plugin_xep_0085 + + +# 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.getUIParam('unibox') + self.setVisible(enable) + if enable and not self.unibox: + self.button = Button('') + 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" — ✍ " + "" % datetime.fromtimestamp(self.updated) + self.header.setHTML("""
+ on + %(updated)s +
""" % {'author': html_tools.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 = base_panels.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.
") + 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' + html = 'raw text' + title = _('Switch to raw text edition') + else: + image = '' + html = 'rich text' + 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 %s" + + 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) + elif not self.getNewMainEntry(): + addBox() + + def getNewMainEntry(self): + """Get the new entry being edited, or None if it doesn't exists. + + @return (MicroblogEntry): the new entry being edited. + """ + try: + first = self.vpanel.children[0] + except IndexError: + return None + assert(first.type == 'main_item') + return first if first.empty else None + + @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 comment 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""" + assert(isinstance(reverse, bool)) + 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 + condition_to_stop = child.empty or (child.published > entry.published) + if condition_to_stop != reverse: # != is XOR + 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(base_panels.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 + base_panels.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(base_panels.HTMLTextEditor.getContent(self)) + + def setContent(self, content): + content = self.__cleanContent(content) + base_panels.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 = base_panels.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.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 = base_panels.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 = plugin_xep_0085.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.JID) else jid.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.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.JID) else jid.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 %s" % self.target + elif self.type == "group": + msg = "This message will be sent to all the participants of the multi-user room %s" % 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.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(base_panels.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": card_game.CardPanel, "RadioCol": 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.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() diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/plugin_xep_0085.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/plugin_xep_0085.py Mon Jun 09 22:15:26 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 . + +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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/radiocol.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/radiocol.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,321 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +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 + +import html_tools +import file_tools + + +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 = file_tools.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_tools.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.
+ Please do not submit files that are protected by copyright.
+ Click here 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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/register.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/register.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,255 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson +# Copyright (C) 2011, 2012 Adrien Vigneron + +# 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 . + +#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
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
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 == "PROFILE AUTH ERROR": + Window.alert(_('Your login and/or password is incorrect. Please try again')) + elif result == "XMPP AUTH ERROR": + # TODO: call stdui action CHANGE_XMPP_PASSWD_ID as it's done in primitivus + Window.alert(_(u'Your SàT profile has been authenticated but the associated XMPP account failed to connect. Please use another SàT frontend to set another XMPP password.')) + 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,
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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/richtext.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/richtext.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 Adrien Cossa + +# 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 . + +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 +import dialog +import base_panels +import list_manager +import html_tools +import panels + + +class RichTextEditor(base_panels.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() + base_panels.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""" + base_panels.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 = base_panels.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 = base_panels.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('' % + 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 + base_panels.BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed + if (edit and abort): + return # self.abortEdition is called by base_panels.BaseTextEditor.edit + self.setWysiwyg(False, init=True) # after base_panels.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 base_panels.HTMLTextEditor which is used for display/wysiwyg""" + content = self._original_content + text = content['text'] + if 'title' in content and content['title']: + text = '

%s

%s' % (html_tools.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 = 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 + dialog.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: + dialog.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(list_manager.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()) + list_manager.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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/sat_browser/xmlui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/xmlui.py Mon Jun 09 22:15:26 2014 +0200 @@ -0,0 +1,418 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# 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 . + +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 + +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, "
") + 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.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) diff -r 01880aa8ea2d -r 97c72fe4a5f2 src/browser/xmlui.py --- a/src/browser/xmlui.py Mon Jun 09 20:37:28 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 - -# 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 . - -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, "
") - 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)