Mercurial > libervia-web
changeset 467:97c72fe4a5f2
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.
line wrap: on
line diff
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ -from sat_frontends.tools.strings import addURLToText, addURLToImage - -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.StackPanel import StackPanel -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas import DOM - -from datetime import datetime -from time import time - -from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML -from constants import Const as C - - -class ChatText(HTMLPanel): - - def __init__(self, timestamp, nick, mymess, msg, xhtml=None): - _date = datetime.fromtimestamp(float(timestamp or time())) - _msg_class = ["chat_text_msg"] - if mymess: - _msg_class.append("chat_text_mymess") - HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" % - {"timestamp": _date.strftime("%H:%M"), - "nick": "[%s]" % html_sanitize(nick), - "msg_class": ' '.join(_msg_class), - "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)} # FIXME: images and external links must be removed according to preferences - ) - self.setStyleName('chatText') - - -class Occupant(HTML): - """Occupant of a MUC room""" - - def __init__(self, nick, state=None, special=""): - """ - @param nick: the user nickname - @param state: the user chate state (XEP-0085) - @param special: a string of symbols (e.g: for activities) - """ - HTML.__init__(self) - self.nick = nick - self._state = state - self.special = special - self._refresh() - - def __str__(self): - return self.nick - - def setState(self, state): - self._state = state - self._refresh() - - def addSpecial(self, special): - """@param special: unicode""" - if special not in self.special: - self.special += special - self._refresh() - - def removeSpecials(self, special): - """@param special: unicode or list""" - if not isinstance(special, list): - special = [special] - for symbol in special: - self.special = self.special.replace(symbol, "") - self._refresh() - - def _refresh(self): - state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' - special = "" if len(self.special) == 0 else " %s" % self.special - self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state)) - - -class OccupantsList(AbsolutePanel): - """Panel user to show occupants of a room""" - - def __init__(self): - AbsolutePanel.__init__(self) - self.occupants_list = {} - self.setStyleName('occupantsList') - - def addOccupant(self, nick): - _occupant = Occupant(nick) - self.occupants_list[nick] = _occupant - self.add(_occupant) - - def removeOccupant(self, nick): - try: - self.remove(self.occupants_list[nick]) - except KeyError: - log.error("trying to remove an unexisting nick") - - def clear(self): - self.occupants_list.clear() - AbsolutePanel.clear(self) - - def updateSpecials(self, occupants=[], html=""): - """Set the specified html "symbol" to the listed occupants, - and eventually remove it from the others (if they got it). - This is used for example to visualize who is playing a game. - @param occupants: list of the occupants that need the symbol - @param html: unicode symbol (actually one character or more) - or a list to assign different symbols of the same family. - """ - index = 0 - special = html - for occupant in self.occupants_list.keys(): - if occupant in occupants: - if isinstance(html, list): - special = html[index] - index = (index + 1) % len(html) - self.occupants_list[occupant].addSpecial(special) - else: - self.occupants_list[occupant].removeSpecials(html) - - -class PopupMenuPanel(PopupPanel): - """This implementation of a popup menu (context menu) allow you to assign - two special methods which are common to all the items, in order to hide - certain items and also easily define their callbacks. The menu can be - bound to any of the mouse button (left, middle, right). - """ - def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs): - """ - @param entries: a dict of dicts, where each sub-dict is representing - one menu item: the sub-dict key can be used as the item text and - description, but optional "title" and "desc" entries would be used - if they exists. The sub-dicts may be extended later to do - more complicated stuff or overwrite the common methods. - @param hide: function with 2 args: widget, key as string and - returns True if that item should be hidden from the context menu. - @param callback: function with 2 args: sender, key as string - @param vertical: True or False, to set the direction - @param item_style: alternative CSS class for the menu items - @param menu_style: supplementary CSS class for the sender widget - """ - PopupPanel.__init__(self, autoHide=True, **kwargs) - self._entries = entries - self._hide = hide - self._callback = callback - self.vertical = vertical - self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"} - if isinstance(style, dict): - self.style.update(style) - self._senders = {} - - def _show(self, sender): - """Popup the menu relative to this sender's position. - @param sender: the widget that has been clicked - """ - menu = VerticalPanel() if self.vertical is True else HorizontalPanel() - menu.setStyleName(self.style["menu"]) - - def button_cb(item): - """You can not put that method in the loop and rely - on _key, because it is overwritten by each step. - You can rely on item.key instead, which is copied - from _key after the item creation. - @param item: the menu item that has been clicked - """ - if self._callback is not None: - self._callback(sender=sender, key=item.key) - self.hide(autoClosed=True) - - for _key in self._entries.keys(): - entry = self._entries[_key] - if self._hide is not None and self._hide(sender=sender, key=_key) is True: - continue - title = entry["title"] if "title" in entry.keys() else _key - item = Button(title, button_cb) - item.key = _key - item.setStyleName(self.style["item"]) - item.setTitle(entry["desc"] if "desc" in entry.keys() else title) - menu.add(item) - if len(menu.getChildren()) == 0: - return - self.add(menu) - if self.vertical is True: - x = sender.getAbsoluteLeft() + sender.getOffsetWidth() - y = sender.getAbsoluteTop() - else: - x = sender.getAbsoluteLeft() - y = sender.getAbsoluteTop() + sender.getOffsetHeight() - self.setPopupPosition(x, y) - self.show() - if self.style["selected"]: - sender.addStyleDependentName(self.style["selected"]) - - def _onHide(popup): - if self.style["selected"]: - sender.removeStyleDependentName(self.style["selected"]) - return PopupPanel.onHideImpl(self, popup) - - self.onHideImpl = _onHide - - def registerClickSender(self, sender, button=BUTTON_LEFT): - """Bind the menu to the specified sender. - @param sender: the widget to which the menu should be bound - @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT - """ - self._senders.setdefault(sender, []) - self._senders[sender].append(button) - - if button == BUTTON_RIGHT: - # WARNING: to disable the context menu is a bit tricky... - # The following seems to work on Firefox 24.0, but: - # TODO: find a cleaner way to disable the context menu - sender.getElement().setAttribute("oncontextmenu", "return false") - - def _onBrowserEvent(event): - button = DOM.eventGetButton(event) - if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: - self._show(sender) - return sender.__class__.onBrowserEvent(sender, event) - - sender.onBrowserEvent = _onBrowserEvent - - def registerMiddleClickSender(self, sender): - self.registerClickSender(sender, BUTTON_MIDDLE) - - def registerRightClickSender(self, sender): - self.registerClickSender(sender, BUTTON_RIGHT) - - -class ToggleStackPanel(StackPanel): - """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be - visible at the same time, clicking a sub-panel header will not display it and hide - the others but only toggle its own visibility. The argument 'visibleStack' is ignored. - Note that the argument 'visible' has been added to listener's 'onStackChanged' method. - """ - - def __init__(self, **kwargs): - StackPanel.__init__(self, **kwargs) - - def onBrowserEvent(self, event): - if DOM.eventGetType(event) == "click": - index = self.getDividerIndex(DOM.eventGetTarget(event)) - if index != -1: - self.toggleStack(index) - - def add(self, widget, stackText="", asHTML=False, visible=False): - StackPanel.add(self, widget, stackText, asHTML) - self.setStackVisible(self.getWidgetCount() - 1, visible) - - def toggleStack(self, index): - if index >= self.getWidgetCount(): - return - visible = not self.getWidget(index).getVisible() - self.setStackVisible(index, visible) - for listener in self.stackListeners: - listener.onStackChanged(self, index, visible) - - -class TitlePanel(ToggleStackPanel): - """A toggle panel to set the message title""" - def __init__(self): - ToggleStackPanel.__init__(self, Width="100%") - self.text_area = TextArea() - self.add(self.text_area, _("Title")) - self.addStackChangeListener(self) - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - text = self.text_area.getText() - suffix = "" if (visible or not text) else (": %s" % text) - sender.setStackText(index, _("Title") + suffix) - - def getText(self): - return self.text_area.getText() - - def setText(self, text): - self.text_area.setText(text) - - -class BaseTextEditor(object): - """Basic definition of a text editor. The method edit gets a boolean parameter which - should be set to True when you want to edit the text and False to only display it.""" - - def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): - """ - Remark when inheriting this class: since the setContent method could be - overwritten by the child class, you should consider calling this __init__ - after all the parameters affecting this setContent method have been set. - @param content: dict with at least a 'text' key - @param strproc: method to be applied on strings to clean the content - @param modifiedCb: method to be called when the text has been modified. - If this method returns: - - True: the modification will be saved and afterEditCb called; - - False: the modification won't be saved and afterEditCb called; - - None: the modification won't be saved and afterEditCb not called. - @param afterEditCb: method to be called when the edition is done - """ - if content is None: - content = {'text': ''} - assert('text' in content) - if strproc is None: - def strproc(text): - try: - return text.strip() - except (TypeError, AttributeError): - return text - self.strproc = strproc - self.__modifiedCb = modifiedCb - self._afterEditCb = afterEditCb - self.initialized = False - self.edit_listeners = [] - self.setContent(content) - - def setContent(self, content=None): - """Set the editable content. The displayed content, which is set from the child class, could differ. - @param content: dict with at least a 'text' key - """ - if content is None: - content = {'text': ''} - elif not isinstance(content, dict): - content = {'text': content} - assert('text' in content) - self._original_content = {} - for key in content: - self._original_content[key] = self.strproc(content[key]) - - def getContent(self): - """Get the current edited or editable content. - @return: dict with at least a 'text' key - """ - raise NotImplementedError - - def setOriginalContent(self, content): - """Use this method with care! Content initialization should normally be - done with self.setContent. This method exists to let you trick the editor, - e.g. for self.modified to return True also when nothing has been modified. - @param content: dict - """ - self._original_content = content - - def getOriginalContent(self): - """ - @return the original content before modification (dict) - """ - return self._original_content - - def modified(self, content=None): - """Check if the content has been modified. - Remark: we don't use the direct comparison because we want to ignore empty elements - @content: content to be check against the original content or None to use the current content - @return: True if the content has been modified. - """ - if content is None: - content = self.getContent() - # the following method returns True if one non empty element exists in a but not in b - diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] - # the following method returns True if the values for the common keys are not equals - diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] - # finally the combination of both to return True if a difference is found - diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) - - return diff(content, self._original_content) - - def edit(self, edit, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if edit: - if not self.initialized: - self.syncToEditor() # e.g.: use the selected target and unibox content - self.setFocus(True) - if abort: - content = self.getContent() - if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation - self.edit(False, True, sync) - return - if sync: - self.syncFromEditor(content) # e.g.: save the content to unibox - return - else: - if not self.initialized: - return - content = self.getContent() - if abort: - self._afterEditCb(content) - return - if self.__modifiedCb and self.modified(content): - result = self.__modifiedCb(content) # e.g.: send a message or update something - if result is not None: - if self._afterEditCb: - self._afterEditCb(content) # e.g.: restore the display mode - if result is True: - self.setContent(content) - elif self._afterEditCb: - self._afterEditCb(content) - - self.initialized = True - - def setFocus(self, focus): - """ - @param focus: set to True to focus the editor - """ - raise NotImplementedError - - def syncToEditor(self): - pass - - def syncFromEditor(self, content): - pass - - def abortEdition(self, content): - return True - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - self.edit_listeners.append(listener) - - -class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): - """Base class for manage a simple text editor.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - """ - @param content - @param modifiedCb - @param afterEditCb - @param options: dict with the following value: - - no_xhtml: set to True to clean any xhtml content. - - enhance_display: if True, the display text will be enhanced with addURLToText - - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. - - listen_focus: set to True to terminate the edition when the focus is lost. - - listen_click: set to True to start the edition when you click on the widget. - """ - self.options = {'no_xhtml': False, - 'enhance_display': True, - 'listen_keyboard': True, - 'listen_focus': False, - 'listen_click': False - } - if options: - self.options.update(options) - self.__shift_down = False - if self.options['listen_focus']: - FocusHandler.__init__(self) - if self.options['listen_click']: - ClickHandler.__init__(self) - KeyboardHandler.__init__(self) - strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text) - BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) - self.textarea = self.display = None - - def setContent(self, content=None): - BaseTextEditor.setContent(self, content) - - def getContent(self): - raise NotImplementedError - - def edit(self, edit, abort=False, sync=False): - BaseTextEditor.edit(self, edit) - if edit: - if self.options['listen_focus'] and self not in self.textarea._focusListeners: - self.textarea.addFocusListener(self) - if self.options['listen_click']: - self.display.clearClickListener() - if self not in self.textarea._keyboardListeners: - self.textarea.addKeyboardListener(self) - else: - self.setDisplayContent() - if self.options['listen_focus']: - try: - self.textarea.removeFocusListener(self) - except ValueError: - pass - if self.options['listen_click'] and self not in self.display._clickListeners: - self.display.addClickListener(self) - try: - self.textarea.removeKeyboardListener(self) - except ValueError: - pass - - def setDisplayContent(self): - text = self._original_content['text'] - if not self.options['no_xhtml']: - text = addURLToImage(text) - if self.options['enhance_display']: - text = addURLToText(text) - self.display.setHTML(convertNewLinesToXHTML(text)) - - def setFocus(self, focus): - raise NotImplementedError - - def onKeyDown(self, sender, keycode, modifiers): - for listener in self.edit_listeners: - listener(self.textarea, keycode) - if not self.options['listen_keyboard']: - return - if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> - self.__shift_down = True - return - if keycode == KEY_ENTER: # finish the edition - self.textarea.setFocus(False) - if not self.options['listen_focus']: - self.edit(False) - - def onKeyUp(self, sender, keycode, modifiers): - if keycode == KEY_SHIFT: - self.__shift_down = False - - def onLostFocus(self, sender): - """Finish the edition when focus is lost""" - if self.options['listen_focus']: - self.edit(False) - - def onClick(self, sender=None): - """Start the edition when the widget is clicked""" - if self.options['listen_click']: - self.edit(True) - - def onBrowserEvent(self, event): - if self.options['listen_focus']: - FocusHandler.onBrowserEvent(self, event) - if self.options['listen_click']: - ClickHandler.onBrowserEvent(self, event) - KeyboardHandler.onBrowserEvent(self, event) - - -class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): - """Manage a simple text editor with the HTML 5 "contenteditable" property.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - HTML.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = self.display = self - - def getContent(self): - text = DOM.getInnerHTML(self.getElement()) - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setHTML(self._original_content['text']) - self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.getElement().focus() - else: - self.getElement().blur() - - -class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): - """Manage a simple text editor with a TextArea for editing, HTML for display.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - SimplePanel.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = TextArea() - self.display = HTML() - - def getContent(self): - text = self.textarea.getText() - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setText(self._original_content['text']) - self.setWidget(self.textarea if edit else self.display) - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.textarea.setCursorPos(len(self.textarea.getText())) - self.textarea.setFocus(focus)
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.Image import Image -from pyjamas.ui.Widget import Widget -from pyjamas.ui.DragWidget import DragWidget -from pyjamas.ui.DropWidget import DropWidget -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui import HasAlignment -from pyjamas import DOM -from pyjamas import Window -from __pyjamas__ import doc - -import dialog - - -class DragLabel(DragWidget): - - def __init__(self, text, _type): - DragWidget.__init__(self) - self._text = text - self._type = _type - - def onDragStart(self, event): - dt = event.dataTransfer - dt.setData('text/plain', "%s\n%s" % (self._text, self._type)) - dt.setDragImage(self.getElement(), 15, 15) - - -class LiberviaDragWidget(DragLabel): - """ A DragLabel which keep the widget being dragged as class value """ - current = None # widget currently dragged - - def __init__(self, text, _type, widget): - DragLabel.__init__(self, text, _type) - self.widget = widget - - def onDragStart(self, event): - LiberviaDragWidget.current = self.widget - DragLabel.onDragStart(self, event) - - def onDragEnd(self, event): - LiberviaDragWidget.current = None - - -class DropCell(DropWidget): - """Cell in the middle grid which replace itself with the dropped widget on DnD""" - drop_keys = {} - - def __init__(self, host): - DropWidget.__init__(self) - self.host = host - self.setStyleName('dropCell') - - @classmethod - def addDropKey(cls, key, callback): - DropCell.drop_keys[key] = callback - - def onDragEnter(self, event): - if self == LiberviaDragWidget.current: - return - self.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\ - event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: - # We check that we are inside widget's box, and we don't remove the style in this case because - # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we - # don't want that - self.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def _getCellAndRow(self, grid, event): - """Return cell and row index where the event is occuring""" - cell = grid.getEventTargetCell(event) - row = DOM.getParent(cell) - return (row.rowIndex, cell.cellIndex) - - def onDrop(self, event): - self.removeStyleName('dragover') - DOM.eventPreventDefault(event) - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - try: - item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed - if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and - item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report - # item_type = dt.getData("type") - log.debug("message: %s" % item) - log.debug("type: %s" % item_type) - except: - log.debug("no message found") - item = ' ' - item_type = None - if item_type == "WIDGET": - if not LiberviaDragWidget.current: - log.error("No widget registered in LiberviaDragWidget !") - return - _new_panel = LiberviaDragWidget.current - if self == _new_panel: # We can't drop on ourself - return - # we need to remove the widget from the panel as it will be inserted elsewhere - widgets_panel = _new_panel.getWidgetsPanel() - wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] - row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) - if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: - # the dropped widget is the only one in the same row - # as the target widget (self), we don't do anything - return - widgets_panel.removeWidget(_new_panel) - elif item_type in self.drop_keys: - _new_panel = self.drop_keys[item_type](self.host, item) - else: - log.warning("unmanaged item type") - return - if isinstance(self, LiberviaWidget): - self.host.unregisterWidget(self) - self.onQuit() - if not isinstance(_new_panel, LiberviaWidget): - log.warning("droping an object which is not a class of LiberviaWidget") - _flextable = self.getParent() - _widgetspanel = _flextable.getParent().getParent() - row_idx, cell_idx = self._getCellAndRow(_flextable, event) - if self.host.getSelected == self: - self.host.setSelected(None) - _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) - """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) - _width = 90/float(len(_unempty_panels) or 1) - #now we resize all the cell of the column - for panel in _unempty_panels: - td_elt = panel.getElement().parentNode - DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)""" - #FIXME: delete object ? Check the right way with pyjamas - - -class WidgetHeader(AbsolutePanel, LiberviaDragWidget): - - def __init__(self, parent, title): - AbsolutePanel.__init__(self) - self.add(title) - button_group_wrapper = SimplePanel() - button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') - button_group = HorizontalPanel() - button_group.setStyleName('widgetHeader_buttonGroup') - setting_button = Image("media/icons/misc/settings.png") - setting_button.setStyleName('widgetHeader_settingButton') - setting_button.addClickListener(parent.onSetting) - close_button = Image("media/icons/misc/close.png") - close_button.setStyleName('widgetHeader_closeButton') - close_button.addClickListener(parent.onClose) - button_group.add(setting_button) - button_group.add(close_button) - button_group_wrapper.setWidget(button_group) - self.add(button_group_wrapper) - self.addStyleName('widgetHeader') - LiberviaDragWidget.__init__(self, "", "WIDGET", parent) - - -class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): - """Libervia's widget which can replace itself with a dropped widget on DnD""" - - def __init__(self, host, title='', selectable=False): - """Init the widget - @param host: SatWebFrontend object - @param title: title show in the header of the widget - @param selectable: True is widget can be selected by user""" - VerticalPanel.__init__(self) - DropCell.__init__(self, host) - ClickHandler.__init__(self) - self.__selectable = selectable - self.__title_id = HTMLPanel.createUniqueId() - self.__setting_button_id = HTMLPanel.createUniqueId() - self.__close_button_id = HTMLPanel.createUniqueId() - self.__title = Label(title) - self.__title.setStyleName('widgetHeader_title') - self._close_listeners = [] - header = WidgetHeader(self, self.__title) - self.add(header) - self.setSize('100%', '100%') - self.addStyleName('widget') - if self.__selectable: - self.addClickListener(self) - - def onClose(sender): - """Check dynamically if the unibox is enable or not""" - if self.host.uni_box: - self.host.uni_box.onWidgetClosed(sender) - - self.addCloseListener(onClose) - self.host.registerWidget(self) - - def getDebugName(self): - return "%s (%s)" % (self, self.__title.getText()) - - def getWidgetsPanel(self, 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)
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_frontends.tools.games import TarotCard -from sat.core.i18n import _ - -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Label import Label -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui import HasAlignment -from pyjamas import Window -from pyjamas import DOM - -from dialog import ConfirmDialog, GenericDialog -from xmlui import XMLUI - - -CARD_WIDTH = 74 -CARD_HEIGHT = 136 -CARD_DELTA_Y = 30 -MIN_WIDTH = 950 # Minimum size of the panel -MIN_HEIGHT = 500 - - -class CardWidget(TarotCard, Image, MouseHandler): - """This class is used to represent a card, graphically and logically""" - - def __init__(self, parent, file_): - """@param file: path of the PNG file""" - self._parent = parent - Image.__init__(self, file_) - root_name = file_[file_.rfind("/") + 1:-4] - suit, value = root_name.split('_') - TarotCard.__init__(self, (suit, value)) - MouseHandler.__init__(self) - self.addMouseListener(self) - - def onMouseEnter(self, sender): - if self._parent.state == "ecart" or self._parent.state == "play": - DOM.setStyleAttribute(self.getElement(), "top", "0px") - - def onMouseLeave(self, sender): - if not self in self._parent.hand: - return - if not self in list(self._parent.selected): # FIXME: Workaround pyjs bug, must report it - DOM.setStyleAttribute(self.getElement(), "top", "%dpx" % CARD_DELTA_Y) - - def onMouseUp(self, sender, x, y): - if self._parent.state == "ecart": - if self not in list(self._parent.selected): - self._parent.addToSelection(self) - else: - self._parent.removeFromSelection(self) - elif self._parent.state == "play": - self._parent.playCard(self) - - -class CardPanel(DockPanel, ClickHandler): - - def __init__(self, parent, referee, player_nick, players): - DockPanel.__init__(self) - ClickHandler.__init__(self) - self._parent = parent - self._autoplay = None # XXX: use 0 to activate fake play, None else - self.referee = referee - self.players = players - self.player_nick = player_nick - self.bottom_nick = self.player_nick - idx = self.players.index(self.player_nick) - idx = (idx + 1) % len(self.players) - self.right_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.top_nick = self.players[idx] - idx = (idx + 1) % len(self.players) - self.left_nick = self.players[idx] - self.bottom_nick = player_nick - self.selected = set() # Card choosed by the player (e.g. during ecart) - self.hand_size = 13 # number of cards in a hand - self.hand = [] - self.to_show = [] - self.state = None - self.setSize("%dpx" % MIN_WIDTH, "%dpx" % MIN_HEIGHT) - self.setStyleName("cardPanel") - - # Now we set up the layout - _label = Label(self.top_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.NORTH) - self.setCellWidth(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_CENTER) - - self.hand_panel = AbsolutePanel() - self.add(self.hand_panel, DockPanel.SOUTH) - self.setCellWidth(self.hand_panel, '100%') - self.setCellHorizontalAlignment(self.hand_panel, HasAlignment.ALIGN_CENTER) - - _label = Label(self.left_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.WEST) - self.setCellHeight(_label, '100%') - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - _label = Label(self.right_nick) - _label.setStyleName('cardGamePlayerNick') - self.add(_label, DockPanel.EAST) - self.setCellHeight(_label, '100%') - self.setCellHorizontalAlignment(_label, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(_label, HasAlignment.ALIGN_MIDDLE) - - self.center_panel = DockPanel() - self.inner_left = SimplePanel() - self.inner_left.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_left, DockPanel.WEST) - self.center_panel.setCellHeight(self.inner_left, '100%') - self.center_panel.setCellHorizontalAlignment(self.inner_left, HasAlignment.ALIGN_RIGHT) - self.center_panel.setCellVerticalAlignment(self.inner_left, HasAlignment.ALIGN_MIDDLE) - - self.inner_right = SimplePanel() - self.inner_right.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_right, DockPanel.EAST) - self.center_panel.setCellHeight(self.inner_right, '100%') - self.center_panel.setCellVerticalAlignment(self.inner_right, HasAlignment.ALIGN_MIDDLE) - - self.inner_top = SimplePanel() - self.inner_top.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_top, DockPanel.NORTH) - self.center_panel.setCellHorizontalAlignment(self.inner_top, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_top, HasAlignment.ALIGN_BOTTOM) - - self.inner_bottom = SimplePanel() - self.inner_bottom.setSize("%dpx" % CARD_WIDTH, "%dpx" % CARD_HEIGHT) - self.center_panel.add(self.inner_bottom, DockPanel.SOUTH) - self.center_panel.setCellHorizontalAlignment(self.inner_bottom, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_bottom, HasAlignment.ALIGN_TOP) - - self.inner_center = SimplePanel() - self.center_panel.add(self.inner_center, DockPanel.CENTER) - self.center_panel.setCellHorizontalAlignment(self.inner_center, HasAlignment.ALIGN_CENTER) - self.center_panel.setCellVerticalAlignment(self.inner_center, HasAlignment.ALIGN_MIDDLE) - - self.add(self.center_panel, DockPanel.CENTER) - self.setCellWidth(self.center_panel, '100%') - self.setCellHeight(self.center_panel, '100%') - self.setCellVerticalAlignment(self.center_panel, HasAlignment.ALIGN_MIDDLE) - self.setCellHorizontalAlignment(self.center_panel, HasAlignment.ALIGN_CENTER) - - self.loadCards() - self.mouse_over_card = None # contain the card to highlight - self.visible_size = CARD_WIDTH / 2 # number of pixels visible for cards - self.addClickListener(self) - - def loadCards(self): - """Load all the cards in memory""" - def _getTarotCardsPathsCb(paths): - log.debug("_getTarotCardsPathsCb") - for file_ in paths: - log.debug("path:", file_) - card = CardWidget(self, file_) - log.debug("card:", card) - self.cards[(card.suit, card.value)] = card - self.deck.append(card) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - self.cards = {} - self.deck = [] - self.cards["atout"] = {} # As Tarot is a french game, it's more handy & logical to keep french names - self.cards["pique"] = {} # spade - self.cards["coeur"] = {} # heart - self.cards["carreau"] = {} # diamond - self.cards["trefle"] = {} # club - self._parent.host.bridge.call('getTarotCardsPaths', _getTarotCardsPathsCb) - - def onClick(self, sender): - if self.state == "chien": - self.to_show = [] - self.state = "wait" - self.updateToShow() - elif self.state == "wait_for_ecart": - self.state = "ecart" - self.hand.extend(self.to_show) - self.hand.sort() - self.to_show = [] - self.updateToShow() - self.updateHand() - - def tarotGameNew(self, hand): - """Start a new game, with given hand""" - if hand is []: # reset the display after the scores have been showed - self.selected.clear() - del self.hand[:] - del self.to_show[:] - self.state = None - #empty hand - self.updateHand() - #nothing on the table - self.updateToShow() - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - self._parent.host.bridge.call('tarotGameReady', None, self.player_nick, self.referee) - return - for suit, value in hand: - self.hand.append(self.cards[(suit, value)]) - self.hand.sort() - self.state = "init" - self.updateHand() - - def updateHand(self): - """Show the cards in the hand in the hand_panel (SOUTH panel)""" - self.hand_panel.clear() - self.hand_panel.setSize("%dpx" % (self.visible_size * (len(self.hand) + 1)), "%dpx" % (CARD_HEIGHT + CARD_DELTA_Y + 10)) - x_pos = 0 - y_pos = CARD_DELTA_Y - for card in self.hand: - self.hand_panel.add(card, x_pos, y_pos) - x_pos += self.visible_size - - def updateToShow(self): - """Show cards in the center panel""" - if not self.to_show: - _widget = self.inner_center.getWidget() - if _widget: - self.inner_center.remove(_widget) - return - panel = AbsolutePanel() - panel.setSize("%dpx" % ((CARD_WIDTH + 5) * len(self.to_show) - 5), "%dpx" % (CARD_HEIGHT)) - x_pos = 0 - y_pos = 0 - for card in self.to_show: - panel.add(card, x_pos, y_pos) - x_pos += CARD_WIDTH + 5 - self.inner_center.setWidget(panel) - - def _ecartConfirm(self, confirm): - if not confirm: - return - ecart = [] - for card in self.selected: - ecart.append((card.suit, card.value)) - self.hand.remove(card) - self.selected.clear() - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, ecart) - self.state = "wait" - self.updateHand() - - def addToSelection(self, card): - self.selected.add(card) - if len(self.selected) == 6: - ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameInvalidCards(self, phase, played_cards, invalid_cards): - """Invalid cards have been played - @param phase: phase of the game - @param played_cards: all the cards played - @param invalid_cards: cards which are invalid""" - - if phase == "play": - self.state = "play" - elif phase == "ecart": - self.state = "ecart" - else: - log.error("INTERNAL ERROR: unmanaged game phase") # FIXME: raise an exception here - - for suit, value in played_cards: - self.hand.append(self.cards[(suit, value)]) - - self.hand.sort() - self.updateHand() - if self._autoplay == None: # No dialog if there is autoplay - Window.alert('Cards played are invalid !') - self.__fakePlay() - - def removeFromSelection(self, card): - self.selected.remove(card) - if len(self.selected) == 6: - ConfirmDialog(self._ecartConfirm, "Put these cards into chien ?").show() - - def tarotGameChooseContrat(self, xml_data): - """Called when the player has to select his contrat - @param xml_data: SàT xml representation of the form""" - body = XMLUI(self._parent.host, xml_data, flags=['NO_CANCEL']) - _dialog = GenericDialog(_('Please choose your contrat'), body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - - def tarotGameShowCards(self, game_stage, cards, data): - """Display cards in the middle of the game (to show for e.g. chien ou poignée)""" - self.to_show = [] - for suit, value in cards: - self.to_show.append(self.cards[(suit, value)]) - self.updateToShow() - if game_stage == "chien" and data['attaquant'] == self.player_nick: - self.state = "wait_for_ecart" - else: - self.state = "chien" - - def getPlayerLocation(self, nick): - """return player location (top,bottom,left or right)""" - for location in ['top', 'left', 'bottom', 'right']: - if getattr(self, '%s_nick' % location) == nick: - return location - log.error("This line should not be reached") - - def tarotGameCardsPlayed(self, player, cards): - """A card has been played by player""" - if not len(cards): - log.warning("cards should not be empty") - return - if len(cards) > 1: - log.error("can't manage several cards played") - if self.to_show: - self.to_show = [] - self.updateToShow() - suit, value = cards[0] - player_pos = self.getPlayerLocation(player) - player_panel = getattr(self, "inner_%s" % player_pos) - - if player_panel.getWidget() != None: - #We have already cards on the table, we remove them - for pos in ['top', 'left', 'bottom', 'right']: - getattr(self, "inner_%s" % pos).setWidget(None) - - card = self.cards[(suit, value)] - DOM.setElemAttribute(card.getElement(), "style", "") - player_panel.setWidget(card) - - def tarotGameYourTurn(self): - """Called when we have to play :)""" - if self.state == "chien": - self.to_show = [] - self.updateToShow() - self.state = "play" - self.__fakePlay() - - def __fakePlay(self): - """Convenience method for stupid autoplay - /!\ don't forgot to comment any interactive dialog for invalid card""" - if self._autoplay == None: - return - if self._autoplay >= len(self.hand): - self._autoplay = 0 - card = self.hand[self._autoplay] - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - del self.hand[self._autoplay] - self.state = "wait" - self._autoplay += 1 - - def playCard(self, card): - self.hand.remove(card) - self._parent.host.bridge.call('tarotGamePlayCards', None, self.player_nick, self.referee, [(card.suit, card.value)]) - self.state = "wait" - self.updateHand() - - def tarotGameScore(self, xml_data, winners, loosers): - """Show score at the end of a round""" - if not winners and not loosers: - title = "Draw game" - else: - if self.player_nick in winners: - title = "You <b>win</b> !" - else: - title = "You <b>loose</b> :(" - body = XMLUI(self._parent.host, xml_data, title=title, flags=['NO_CANCEL']) - _dialog = GenericDialog(title, body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show()
--- 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 <http://www.gnu.org/licenses/>. - -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 - }, - }
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.ScrollPanel import ScrollPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas import Window -from pyjamas import DOM -from __pyjamas__ import doc - -from jid import JID - -from base_panels import PopupMenuPanel -from base_widget import DragLabel -from panels import ChatPanel, MicroblogPanel, WebPanel, UniBoxPanel -from html_tools import html_sanitize - - -def setPresenceStyle(element, presence, base_style="contact"): - """ - Set the CSS style of a contact's element according to its presence. - @param item: the UI element of the contact - @param presence: a value in ("", "chat", "away", "dnd", "xa"). - @param base_style: the base name of the style to apply - """ - if not hasattr(element, 'presence_style'): - element.presence_style = None - style = '%s-%s' % (base_style, presence or 'connected') - if style == element.presence_style: - return - if element.presence_style is not None: - element.removeStyleName(element.presence_style) - element.addStyleName(style) - element.presence_style = style - - -class GroupLabel(DragLabel, Label, ClickHandler): - def __init__(self, host, group): - self.group = group - self.host = host - Label.__init__(self, group) # , Element=DOM.createElement('div') - self.setStyleName('group') - DragLabel.__init__(self, group, "GROUP") - ClickHandler.__init__(self) - self.addClickListener(self) - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(MicroblogPanel, self.group) - - -class ContactLabel(DragLabel, HTML, ClickHandler): - def __init__(self, host, jid, name=None, handleClick=True): - HTML.__init__(self) - self.host = host - self.name = name or jid - self.waiting = False - self.jid = jid - self._fill() - self.setStyleName('contact') - DragLabel.__init__(self, jid, "CONTACT") - if handleClick: - ClickHandler.__init__(self) - self.addClickListener(self) - - def _fill(self): - if self.waiting: - _wait_html = "<b>(*)</b> " - self.setHTML("%(wait)s%(name)s" % {'wait': _wait_html, - 'name': html_sanitize(self.name)}) - - def setMessageWaiting(self, waiting): - """Show a visual indicator if message are waiting - @param waiting: True if message are waiting""" - self.waiting = waiting - self._fill() - - def onClick(self, sender): - self.host.getOrCreateLiberviaWidget(ChatPanel, self.jid) - - -class GroupList(VerticalPanel): - - def __init__(self, parent): - VerticalPanel.__init__(self) - self.setStyleName('groupList') - self._parent = parent - - def add(self, group): - _item = GroupLabel(self._parent.host, group) - _item.addMouseListener(self._parent) - DOM.setStyleAttribute(_item.getElement(), "cursor", "pointer") - index = 0 - for group_ in [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")
--- 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 <souliane@mailoo.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.DockPanel import DockPanel -from pyjamas.Timer import Timer -from pyjamas.ui.Button import Button -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui import HasAlignment - -from dialog import ConfirmDialog, InfoDialog -from list_manager import ListManager -import dialog -import contact - - -class ContactGroupManager(ListManager): - """A manager for sub-panels to assign contacts to each group.""" - - def __init__(self, parent, keys_dict, contact_list, offsets, style): - ListManager.__init__(self, parent, keys_dict, contact_list, offsets, style) - self.registerPopupMenuPanel(entries={"Remove group": {}}, - callback=lambda sender, key: Timer(5, lambda timer: self.removeContactKey(sender, key))) - - def removeContactKey(self, sender, key): - key = sender.getText() - - def confirm_cb(answer): - if answer: - ListManager.removeContactKey(self, key) - self._parent.removeKeyFromAddGroupPanel(key) - - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to delete the group '%s'?" % key) - _dialog.show() - - def removeFromRemainingList(self, contacts): - ListManager.removeFromRemainingList(self, contacts) - self._parent.updateContactList(contacts=contacts) - - def addToRemainingList(self, contacts, ignore_key=None): - ListManager.addToRemainingList(self, contacts, ignore_key) - self._parent.updateContactList(contacts=contacts) - - -class ContactGroupEditor(DockPanel): - """Panel for the contact groups manager.""" - - def __init__(self, host, parent=None, onCloseCallback=None): - DockPanel.__init__(self) - self.host = host - - # eventually display in a popup - if parent is None: - parent = DialogBox(autoHide=False, centered=True) - parent.setHTML("Manage contact groups") - self._parent = parent - self._on_close_callback = onCloseCallback - self.all_contacts = self.host.contact_panel.getContacts() - - groups_list = self.host.contact_panel.groups.keys() - groups_list.sort() - - self.add_group_panel = self.getAddGroupPanel(groups_list) - south_panel = self.getCloseSaveButtons() - center_panel = self.getContactGroupManager(groups_list) - east_panel = self.getContactList() - - self.add(self.add_group_panel, DockPanel.CENTER) - self.add(east_panel, DockPanel.EAST) - self.add(center_panel, DockPanel.NORTH) - self.add(south_panel, DockPanel.SOUTH) - - self.setCellHorizontalAlignment(center_panel, HasAlignment.ALIGN_LEFT) - self.setCellVerticalAlignment(center_panel, HasAlignment.ALIGN_TOP) - self.setCellHorizontalAlignment(east_panel, HasAlignment.ALIGN_RIGHT) - self.setCellVerticalAlignment(east_panel, HasAlignment.ALIGN_TOP) - self.setCellVerticalAlignment(self.add_group_panel, HasAlignment.ALIGN_BOTTOM) - self.setCellHorizontalAlignment(self.add_group_panel, HasAlignment.ALIGN_LEFT) - self.setCellVerticalAlignment(south_panel, HasAlignment.ALIGN_BOTTOM) - self.setCellHorizontalAlignment(south_panel, HasAlignment.ALIGN_CENTER) - - # need to be done after the contact list has been initialized - self.groups.setContacts(self.host.contact_panel.groups) - self.toggleContacts(showAll=True) - - # Hide the contacts list from the main panel to not confuse the user - self.restore_contact_panel = False - if self.host.contact_panel.getVisible(): - self.restore_contact_panel = True - self.host.panel._contactsSwitch() - - parent.add(self) - parent.setVisible(True) - if isinstance(parent, DialogBox): - parent.center() - - def getContactGroupManager(self, groups_list): - """Set the list manager for the groups""" - flex_table = FlexTable(len(groups_list), 2) - flex_table.addStyleName('contactGroupEditor') - # overwrite the default style which has been set for rich text editor - style = { - "keyItem": "group", - "popupMenuItem": "popupMenuItem", - "removeButton": "contactGroupRemoveButton", - "buttonCell": "contactGroupButtonCell", - "keyPanel": "contactGroupPanel" - } - self.groups = ContactGroupManager(flex_table, groups_list, self.all_contacts, style=style) - self.groups.createWidgets() # widgets are automatically added to FlexTable - # FIXME: clean that part which is dangerous - flex_table.updateContactList = self.updateContactList - flex_table.removeKeyFromAddGroupPanel = self.add_group_panel.groups.remove - return flex_table - - def getAddGroupPanel(self, groups_list): - """Add the 'Add group' panel to the FlexTable""" - - def add_group_cb(text): - self.groups.addContactKey(text) - self.add_group_panel.textbox.setFocus(True) - - add_group_panel = dialog.AddGroupPanel(groups_list, add_group_cb) - add_group_panel.addStyleName("addContactGroupPanel") - return add_group_panel - - def getCloseSaveButtons(self): - """Add the buttons to close the dialog / save the groups""" - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - buttons.add(Button("Save", listener=self.closeAndSave)) - buttons.add(Button("Cancel", listener=self.cancelWithoutSaving)) - return buttons - - def getContactList(self): - """Add the contact list to the DockPanel""" - self.toggle = Button("", self.toggleContacts) - self.toggle.addStyleName("toggleAssignedContacts") - self.contacts = contact.GenericContactList(self.host) - for contact_ in self.all_contacts: - self.contacts.add(contact_) - contact_panel = VerticalPanel() - contact_panel.add(self.toggle) - contact_panel.add(self.contacts) - return contact_panel - - def toggleContacts(self, sender=None, showAll=None): - """Callback for the toggle button""" - if sender is None: - sender = self.toggle - sender.showAll = showAll if showAll is not None else not sender.showAll - if sender.showAll: - sender.setText("Hide assigned") - else: - sender.setText("Show assigned") - self.updateContactList(sender) - - def updateContactList(self, sender=None, contacts=None): - """Update the contact list regarding the toggle button""" - if not hasattr(self, "toggle") or not hasattr(self.toggle, "showAll"): - return - sender = self.toggle - if contacts is not None: - if not isinstance(contacts, list): - contacts = [contacts] - for contact_ in contacts: - if contact_ not in self.all_contacts: - contacts.remove(contact_) - else: - contacts = self.all_contacts - for contact_ in contacts: - if sender.showAll: - self.contacts.getContactLabel(contact_).setVisible(True) - else: - if contact_ in self.groups.remaining_list: - self.contacts.getContactLabel(contact_).setVisible(True) - else: - self.contacts.getContactLabel(contact_).setVisible(False) - - def __close(self): - """Remove the widget from parent or close the popup.""" - if isinstance(self._parent, DialogBox): - self._parent.hide() - self._parent.remove(self) - if self._on_close_callback is not None: - self._on_close_callback() - if self.restore_contact_panel: - self.host.panel._contactsSwitch() - - def cancelWithoutSaving(self): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.__close() - - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to cancel without saving?") - _dialog.show() - - def closeAndSave(self): - """Call bridge methods to save the changes and close the dialog""" - map_ = {} - for contact_ in self.all_contacts: - map_[contact_] = set() - contacts = self.groups.getContacts() - for group in contacts.keys(): - for contact_ in contacts[group]: - try: - map_[contact_].add(group) - except KeyError: - InfoDialog("Invalid contact", - "The contact '%s' is not your contact list but it has been assigned to the group '%s'." % (contact_, group) + - "Your changes could not be saved: please check your assignments and save again.", Width="400px").center() - return - for contact_ in map_.keys(): - groups = map_[contact_] - current_groups = self.host.contact_panel.getContactGroups(contact_) - if groups != current_groups: - self.host.bridge.call('updateContact', None, contact_, '', list(groups)) - self.__close()
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_frontends.tools.misc import DEFAULT_MUC - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.Grid import Grid -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.Button import Button -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.RadioButton import RadioButton -from pyjamas.ui import HasAlignment -from pyjamas.ui.KeyboardListener import KEY_ESCAPE, KEY_ENTER -from pyjamas.ui.MouseListener import MouseWheelHandler -from pyjamas import Window - -from base_panels import ToggleStackPanel - - -# List here the patterns that are not allowed in contact group names -FORBIDDEN_PATTERNS_IN_GROUP = () - - -class RoomChooser(Grid): - """Select a room from the rooms you already joined, or create a new one""" - - GENERATE_MUC = "<use random name>" - - def __init__(self, host, default_room=DEFAULT_MUC): - Grid.__init__(self, 2, 2, Width='100%') - self.host = host - - self.new_radio = RadioButton("room", "Discussion room:") - self.new_radio.setChecked(True) - self.box = TextBox(Width='95%') - self.box.setText(self.GENERATE_MUC if default_room == "" else default_room) - self.exist_radio = RadioButton("room", "Already joined:") - self.rooms_list = ListBox(Width='95%') - - self.add(self.new_radio, 0, 0) - self.add(self.box, 0, 1) - self.add(self.exist_radio, 1, 0) - self.add(self.rooms_list, 1, 1) - - self.box.addFocusListener(self) - self.rooms_list.addFocusListener(self) - - self.exist_radio.setVisible(False) - self.rooms_list.setVisible(False) - self.setRooms() - - def onFocus(self, sender): - if sender == self.rooms_list: - self.exist_radio.setChecked(True) - elif sender == self.box: - if self.box.getText() == self.GENERATE_MUC: - self.box.setText("") - self.new_radio.setChecked(True) - - def onLostFocus(self, sender): - if sender == self.box: - if self.box.getText() == "": - self.box.setText(self.GENERATE_MUC) - - def setRooms(self): - for room in self.host.room_list: - self.rooms_list.addItem(room.bare) - if len(self.host.room_list) > 0: - self.exist_radio.setVisible(True) - self.rooms_list.setVisible(True) - self.exist_radio.setChecked(True) - - def getRoom(self): - if self.exist_radio.getChecked(): - values = self.rooms_list.getSelectedValues() - return "" if values == [] else values[0] - value = self.box.getText() - return "" if value == self.GENERATE_MUC else value - - -class ContactsChooser(VerticalPanel): - """Select one or several connected contacts""" - - def __init__(self, host, nb_contact=None, ok_button=None): - """ - @param host: SatWebFrontend instance - @param nb_contact: number of contacts that have to be selected, None for no limit - If a tuple is given instead of an integer, nb_contact[0] is the minimal and - nb_contact[1] is the maximal number of contacts to be chosen. - """ - self.host = host - if isinstance(nb_contact, tuple): - if len(nb_contact) == 0: - nb_contact = None - elif len(nb_contact) == 1: - nb_contact = (nb_contact[0], nb_contact[0]) - elif nb_contact is not None: - nb_contact = (nb_contact, nb_contact) - if nb_contact is None: - log.warning("Need to select as many contacts as you want") - else: - log.warning("Need to select between %d and %d contacts" % nb_contact) - self.nb_contact = nb_contact - self.ok_button = ok_button - VerticalPanel.__init__(self, Width='100%') - self.contacts_list = ListBox() - self.contacts_list.setMultipleSelect(True) - self.contacts_list.setWidth("95%") - self.contacts_list.addStyleName('contactsChooser') - self.contacts_list.addChangeListener(self.onChange) - self.add(self.contacts_list) - self.setContacts() - self.onChange() - - def onChange(self, sender=None): - if self.ok_button is None: - return - if self.nb_contact: - selected = len(self.contacts_list.getSelectedValues(True)) - if selected >= self.nb_contact[0] and selected <= self.nb_contact[1]: - self.ok_button.setEnabled(True) - else: - self.ok_button.setEnabled(False) - - def setContacts(self, selected=[]): - """Fill the list with the connected contacts - @param select: list of the contacts to select by default - """ - self.contacts_list.clear() - contacts = self.host.contact_panel.getConnected(filter_muc=True) - self.contacts_list.setVisibleItemCount(10 if len(contacts) > 5 else 5) - self.contacts_list.addItem("") - for contact in contacts: - if contact not in [room.bare for room in self.host.room_list]: - self.contacts_list.addItem(contact) - self.contacts_list.setItemTextSelection(selected) - - def getContacts(self): - return self.contacts_list.getSelectedValues(True) - - -class RoomAndContactsChooser(DialogBox): - """Select a room and some users to invite in""" - - def __init__(self, host, callback, nb_contact=None, ok_button="OK", title="Discussion groups", - title_room="Join room", title_invite="Invite contacts", visible=(True, True)): - DialogBox.__init__(self, centered=True) - self.host = host - self.callback = callback - self.title_room = title_room - self.title_invite = title_invite - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - ok_button = Button("OK", self.onOK) - button_panel.add(ok_button) - button_panel.add(Button("Cancel", self.onCancel)) - - self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) - self.contact_panel = ContactsChooser(host, nb_contact, ok_button) - - self.stack_panel = ToggleStackPanel(Width="100%") - self.stack_panel.add(self.room_panel, visible=visible[0]) - self.stack_panel.add(self.contact_panel, visible=visible[1]) - self.stack_panel.addStackChangeListener(self) - self.onStackChanged(self.stack_panel, 0, visible[0]) - self.onStackChanged(self.stack_panel, 1, visible[1]) - - main_panel = VerticalPanel() - main_panel.setStyleName("room-contact-chooser") - main_panel.add(self.stack_panel) - main_panel.add(button_panel) - - self.setWidget(main_panel) - self.setHTML(title) - self.show() - - # needed to update the contacts list when someone logged in/out - self.host.room_contacts_chooser = self - - def getRoom(self, asSuffix=False): - room = self.room_panel.getRoom() - if asSuffix: - return room if room == "" else ": %s" % room - else: - return room - - def getContacts(self, asSuffix=False): - contacts = self.contact_panel.getContacts() - if asSuffix: - return "" if contacts == [] else ": %s" % ", ".join(contacts) - else: - return contacts - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - if index == 0: - sender.setStackText(0, self.title_room + ("" if visible else self.getRoom(True))) - elif index == 1: - sender.setStackText(1, self.title_invite + ("" if visible else self.getContacts(True))) - - def resetContacts(self): - """Called when someone log in/out to update the list""" - self.contact_panel.setContacts(self.getContacts()) - - def onOK(self, sender): - room_jid = self.getRoom() - if room_jid != "" and "@" not in room_jid: - Window.alert('You must enter a room jid in the form room@chat.%s' % self.host._defaultDomain) - return - self.hide() - self.callback(room_jid, self.getContacts()) - - def onCancel(self, sender): - self.hide() - - def hide(self): - self.host.room_contacts_chooser = None - DialogBox.hide(self, autoClosed=True) - - -class GenericConfirmDialog(DialogBox): - - def __init__(self, widgets, callback, title='Confirmation', prompt=None, **kwargs): - """ - Dialog to confirm an action - @param widgets: widgets to attach - @param callback: method to call when a button is clicked - @param title: title of the dialog - @param prompt: textbox from which to retrieve the string value to be passed to the callback when - OK button is pressed. If None, OK button will return "True". Cancel button always returns "False". - """ - self.callback = callback - DialogBox.__init__(self, centered=True, **kwargs) - - content = VerticalPanel() - content.setWidth('100%') - for wid in widgets: - content.add(wid) - if wid == prompt: - wid.setWidth('100%') - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - self.confirm_button = Button("OK", self.onConfirm) - button_panel.add(self.confirm_button) - self.cancel_button = Button("Cancel", self.onCancel) - button_panel.add(self.cancel_button) - content.add(button_panel) - self.setHTML(title) - self.setWidget(content) - self.prompt = prompt - - def onConfirm(self, sender): - self.hide() - self.callback(self.prompt.getText() if self.prompt else True) - - def onCancel(self, sender): - self.hide() - self.callback(False) - - def show(self): - DialogBox.show(self) - if self.prompt: - self.prompt.setFocus(True) - - -class ConfirmDialog(GenericConfirmDialog): - - def __init__(self, callback, text='Are you sure ?', title='Confirmation', **kwargs): - GenericConfirmDialog.__init__(self, [HTML(text)], callback, title, **kwargs) - - -class GenericDialog(DialogBox): - """Dialog which just show a widget and a close button""" - - def __init__(self, title, main_widget, callback=None, options=None, **kwargs): - """Simple notice dialog box - @param title: HTML put in the header - @param main_widget: widget put in the body - @param callback: method to call on closing - @param options: one or more of the following options: - - NO_CLOSE: don't add a close button""" - DialogBox.__init__(self, centered=True, **kwargs) - self.callback = callback - if not options: - options = [] - _body = VerticalPanel() - _body.setSize('100%', '100%') - _body.setSpacing(4) - _body.add(main_widget) - _body.setCellWidth(main_widget, '100%') - _body.setCellHeight(main_widget, '100%') - if not 'NO_CLOSE' in options: - _close_button = Button("Close", self.onClose) - _body.add(_close_button) - _body.setCellHorizontalAlignment(_close_button, HasAlignment.ALIGN_CENTER) - self.setHTML(title) - self.setWidget(_body) - self.panel.setSize('100%', '100%') # Need this hack to have correct size in Gecko & Webkit - - def close(self): - """Same effect as clicking the close button""" - self.onClose(None) - - def onClose(self, sender): - self.hide() - if self.callback: - self.callback() - - -class InfoDialog(GenericDialog): - - def __init__(self, title, body, callback=None, options=None, **kwargs): - GenericDialog.__init__(self, title, HTML(body), callback, options, **kwargs) - - -class PromptDialog(GenericConfirmDialog): - - def __init__(self, callback, text='', title='User input', **kwargs): - prompt = TextBox() - prompt.setText(text) - GenericConfirmDialog.__init__(self, [prompt], callback, title, prompt, **kwargs) - - -class PopupPanelWrapper(PopupPanel): - """This wrapper catch Escape event to avoid request cancellation by Firefox""" - - def onEventPreview(self, event): - if event.type in ["keydown", "keypress", "keyup"] and event.keyCode == KEY_ESCAPE: - #needed to prevent request cancellation in Firefox - event.preventDefault() - return PopupPanel.onEventPreview(self, event) - - -class ExtTextBox(TextBox): - """Extended TextBox""" - - def __init__(self, *args, **kwargs): - if 'enter_cb' in kwargs: - self.enter_cb = kwargs['enter_cb'] - del kwargs['enter_cb'] - TextBox.__init__(self, *args, **kwargs) - self.addKeyboardListener(self) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onKeyPress(self, sender, keycode, modifiers): - if self.enter_cb and keycode == KEY_ENTER: - self.enter_cb(self) - - -class GroupSelector(DialogBox): - - def __init__(self, top_widgets, initial_groups, selected_groups, - ok_title="OK", ok_cb=None, cancel_cb=None): - DialogBox.__init__(self, centered=True) - main_panel = VerticalPanel() - self.ok_cb = ok_cb - self.cancel_cb = cancel_cb - - for wid in top_widgets: - main_panel.add(wid) - - main_panel.add(Label('Select in which groups your contact is:')) - self.list_box = ListBox() - self.list_box.setMultipleSelect(True) - self.list_box.setVisibleItemCount(5) - self.setAvailableGroups(initial_groups) - self.setGroupsSelected(selected_groups) - main_panel.add(self.list_box) - - def cb(text): - self.list_box.addItem(text) - self.list_box.setItemSelected(self.list_box.getItemCount() - 1, "selected") - - main_panel.add(AddGroupPanel(initial_groups, cb)) - - button_panel = HorizontalPanel() - button_panel.addStyleName("marginAuto") - button_panel.add(Button(ok_title, self.onOK)) - button_panel.add(Button("Cancel", self.onCancel)) - main_panel.add(button_panel) - - self.setWidget(main_panel) - - def getSelectedGroups(self): - """Return a list of selected groups""" - return self.list_box.getSelectedValues() - - def setAvailableGroups(self, groups): - _groups = list(set(groups)) - _groups.sort() - self.list_box.clear() - for group in _groups: - self.list_box.addItem(group) - - def setGroupsSelected(self, selected_groups): - self.list_box.setItemTextSelection(selected_groups) - - def onOK(self, sender): - self.hide() - if self.ok_cb: - self.ok_cb(self) - - def onCancel(self, sender): - self.hide() - if self.cancel_cb: - self.cancel_cb(self) - - -class AddGroupPanel(HorizontalPanel): - def __init__(self, groups, cb=None): - """ - @param groups: list of the already existing groups - """ - HorizontalPanel.__init__(self) - self.groups = groups - self.add(Label('Add group:')) - self.textbox = ExtTextBox(enter_cb=self.onGroupInput) - self.add(self.textbox) - self.add(Button("add", lambda sender: self.onGroupInput(self.textbox))) - self.cb = cb - - def onGroupInput(self, sender): - text = sender.getText() - if text == "": - return - for group in self.groups: - if text == group: - Window.alert("The group '%s' already exists." % text) - return - for pattern in FORBIDDEN_PATTERNS_IN_GROUP: - if pattern in text: - Window.alert("The pattern '%s' is not allowed in group names." % pattern) - return - sender.setText('') - self.groups.append(text) - if self.cb is not None: - self.cb(text) - - -class WheelTextBox(TextBox, MouseWheelHandler): - - def __init__(self, *args, **kwargs): - TextBox.__init__(self, *args, **kwargs) - MouseWheelHandler.__init__(self) - - -class IntSetter(HorizontalPanel): - """This class show a bar with button to set an int value""" - - def __init__(self, label, value=0, value_max=None, visible_len=3): - """initialize the intSetter - @param label: text shown in front of the setter - @param value: initial value - @param value_max: limit value, None or 0 for unlimited""" - HorizontalPanel.__init__(self) - self.value = value - self.value_max = value_max - _label = Label(label) - self.add(_label) - self.setCellWidth(_label, "100%") - minus_button = Button("-", self.onMinus) - self.box = WheelTextBox() - self.box.setVisibleLength(visible_len) - self.box.setText(str(value)) - self.box.addInputListener(self) - self.box.addMouseWheelListener(self) - plus_button = Button("+", self.onPlus) - self.add(minus_button) - self.add(self.box) - self.add(plus_button) - self.valueChangedListener = [] - - def addValueChangeListener(self, listener): - self.valueChangedListener.append(listener) - - def removeValueChangeListener(self, listener): - if listener in self.valueChangedListener: - self.valueChangedListener.remove(listener) - - def _callListeners(self): - for listener in self.valueChangedListener: - listener(self.value) - - def setValue(self, value): - """Change the value and fire valueChange listeners""" - self.value = value - self.box.setText(str(value)) - self._callListeners() - - def onMinus(self, sender, step=1): - self.value = max(0, self.value - step) - self.box.setText(str(self.value)) - self._callListeners() - - def onPlus(self, sender, step=1): - self.value += step - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(str(self.value)) - self._callListeners() - - def onInput(self, sender): - """Accept only valid integer && normalize print (no leading 0)""" - try: - self.value = int(self.box.getText()) if self.box.getText() else 0 - except ValueError: - pass - if self.value_max: - self.value = min(self.value, self.value_max) - self.box.setText(str(self.value)) - self._callListeners() - - def onMouseWheel(self, sender, velocity): - if velocity > 0: - self.onMinus(sender, 10) - else: - self.onPlus(sender, 10)
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.FileUpload import FileUpload -from pyjamas.ui.FormPanel import FormPanel -from pyjamas import Window -from pyjamas import DOM -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HTML import HTML -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label - - -class FilterFileUpload(FileUpload): - - def __init__(self, name, max_size, types=None): - """ - @param name: the input element name and id - @param max_size: maximum file size in MB - @param types: allowed types as a list of couples (x, y, z): - - x: MIME content type e.g. "audio/ogg" - - y: file extension e.g. "*.ogg" - - z: description for the user e.g. "Ogg Vorbis Audio" - If types is None, all file format are accepted - """ - FileUpload.__init__(self) - self.setName(name) - while DOM.getElementById(name): - name = "%s_" % name - self.setID(name) - self._id = name - self.max_size = max_size - self.types = types - - def getFileInfo(self): - from __pyjamas__ import JS - JS("var file = top.document.getElementById(this._id).files[0]; return [file.size, file.type]") - - def check(self): - if self.getFilename() == "": - return False - (size, filetype) = self.getFileInfo() - if self.types and filetype not in [x for (x, y, z) in self.types]: - types = [] - for type_ in ["- %s (%s)" % (z, y) for (x, y, z) in self.types]: - if type_ not in types: - types.append(type_) - Window.alert('This file type is not accepted.\nAccepted file types are:\n\n%s' % "\n".join(types)) - return False - if size > self.max_size * pow(2, 20): - Window.alert('This file is too big!\nMaximum file size: %d MB.' % self.max_size) - return False - return True - - -class FileUploadPanel(FormPanel): - - def __init__(self, action_url, input_id, max_size, texts=None, close_cb=None): - """Build a form panel to upload a file. - @param action_url: the form action URL - @param input_id: the input element name and id - @param max_size: maximum file size in MB - @param texts: a dict to ovewrite the default textual values - @param close_cb: the close button callback method - """ - FormPanel.__init__(self) - self.texts = {'ok_button': 'Upload file', - 'cancel_button': 'Cancel', - 'body': 'Please select a file.', - 'submitting': '<strong>Submitting, please wait...</strong>', - 'errback': "Your file has been rejected...", - 'body_errback': 'Please select another file.', - 'callback': "Your file has been accepted!"} - if isinstance(texts, dict): - self.texts.update(texts) - self.close_cb = close_cb - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction(action_url) - self.vPanel = VerticalPanel() - self.message = HTML(self.texts['body']) - self.vPanel.add(self.message) - - hPanel = HorizontalPanel() - hPanel.setSpacing(5) - hPanel.setStyleName('marginAuto') - self.file_upload = FilterFileUpload(input_id, max_size) - self.vPanel.add(self.file_upload) - - self.upload_btn = Button(self.texts['ok_button'], getattr(self, "onSubmitBtnClick")) - hPanel.add(self.upload_btn) - hPanel.add(Button(self.texts['cancel_button'], getattr(self, "onCloseBtnClick"))) - - self.status = Label() - hPanel.add(self.status) - - self.vPanel.add(hPanel) - - self.add(self.vPanel) - self.addFormHandler(self) - - def setCloseCb(self, close_cb): - self.close_cb = close_cb - - def onCloseBtnClick(self): - if self.close_cb: - self.close_cb() - else: - log.warning("no close method defined") - - def onSubmitBtnClick(self): - if not self.file_upload.check(): - return - self.message.setHTML(self.texts['submitting']) - self.upload_btn.setEnabled(False) - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result != "OK": - Window.alert(self.texts['errback']) - self.message.setHTML(self.texts['body_errback']) - self.upload_btn.setEnabled(True) - else: - Window.alert(self.texts['callback']) - self.close_cb()
--- a/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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.tools import xmltools - -from nativedom import NativeDOM -import re - -dom = NativeDOM() - - -def html_sanitize(html): - """Naive sanitization of HTML""" - return html.replace('<', '<').replace('>', '>') - - -def html_strip(html): - """Strip leading/trailing white spaces, HTML line breaks and sequences.""" - cleaned = re.sub(r"^(<br/?>| |\s)+", "", html) - cleaned = re.sub(r"(<br/?>| |\s)+$", "", cleaned) - return cleaned - - -def inlineRoot(xhtml): - """ make root element inline """ - doc = dom.parseString(xhtml) - return xmltools.inlineRoot(doc) - - -def convertNewLinesToXHTML(text): - return text.replace('\n', '<br/>')
--- 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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -class JID(object): - """This class help manage JID (Node@Domaine/Resource)""" - - def __init__(self, jid): - self.__raw = str(jid) - self.__parse() - - def __parse(self): - """find node domaine and resource""" - node_end = self.__raw.find('@') - if node_end < 0: - node_end = 0 - domain_end = self.__raw.find('/') - if domain_end < 1: - domain_end = len(self.__raw) - self.node = self.__raw[:node_end] - self.domain = self.__raw[(node_end + 1) if node_end else 0:domain_end] - self.resource = self.__raw[domain_end + 1:] - if not node_end: - self.bare = self.__raw - else: - self.bare = self.node + '@' + self.domain - - def __str__(self): - return self.__raw.__str__() - - def is_valid(self): - """return True if the jid is xmpp compliant""" - #FIXME: always return True for the moment - return True - - def __eq__(self, other): - """Redefine equality operator to implement the naturally expected test""" - return self.node == other.node and self.domain == other.domain and self.resource == other.resource
--- a/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('<br />') - 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 <b>%s</b> has added you to his/her contact list' % html_sanitize(entity)).show() + dialog.InfoDialog('Subscription confirmation', 'The contact <b>%s</b> 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 <b>%s</b> has refused to add you in his/her contact list' % html_sanitize(entity)).show() + dialog.InfoDialog('Subscription refusal', 'The contact <b>%s</b> 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 <b>%s</b> want to add you in his/her contact list, do you accept ?' % html_sanitize(entity)) + msg = HTML('The contact <b>%s</b> 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
--- 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 <souliane@mailoo.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.log import getLogger -log = getLogger(__name__) -from pyjamas.ui.Grid import Grid -from pyjamas.ui.Button import Button -from pyjamas.ui.ListBox import ListBox -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.AutoComplete import AutoCompleteTextBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.KeyboardListener import KEY_ENTER -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.ui.DropWidget import DropWidget -from pyjamas.Timer import Timer -from pyjamas import DOM - -from base_panels import PopupMenuPanel -from base_widget import DragLabel - -# HTML content for the removal button (image or text) -REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>' - -# Item to be considered for an empty list box selection. -# Could be whatever which doesn't look like a JID or a group name. -EMPTY_SELECTION_ITEM = "" - - -class ListManager(): - """A manager for sub-panels to assign elements to lists.""" - - def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}): - """ - @param parent: FlexTable parent widget for the manager - @param keys_dict: dict with the contact keys mapped to data - @param contact_list: list of string (the contact JID userhosts) - @param offsets: dict to set widget positions offset within parent - - "x_first": the x offset for the first widget's row on the grid - - "x": the x offset for all widgets rows, except the first one if "x_first" is defined - - "y": the y offset for all widgets columns on the grid - """ - self._parent = parent - if isinstance(keys_dict, set) or isinstance(keys_dict, list): - tmp = {} - for key in keys_dict: - tmp[key] = {} - keys_dict = tmp - self.__keys_dict = keys_dict - if isinstance(contact_list, set): - contact_list = list(contact_list) - self.__list = contact_list - self.__list.sort() - # store the list of contacts that are not assigned yet - self.__remaining_list = [] - self.__remaining_list.extend(self.__list) - # mark a change to sort the list before it's used - self.__remaining_list_sorted = True - - self.offsets = {"x_first": 0, "x": 0, "y": 0} - if "x" in offsets and not "x_first" in offsets: - offsets["x_first"] = offsets["x"] - self.offsets.update(offsets) - - self.style = { - "keyItem": "recipientTypeItem", - "popupMenuItem": "recipientTypeItem", - "buttonCell": "recipientButtonCell", - "dragoverPanel": "dragover-recipientPanel", - "keyPanel": "recipientPanel", - "textBox": "recipientTextBox", - "textBox-invalid": "recipientTextBox-invalid", - "removeButton": "recipientRemoveButton", - } - self.style.update(style) - - def createWidgets(self, title_format="%s"): - """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" - self.__children = {} - for key in self.__keys_dict: - self.addContactKey(key, title_format=title_format) - - def addContactKey(self, key, dict_={}, title_format="%s"): - if key not in self.__keys_dict: - self.__keys_dict[key] = dict_ - # copy the key to its associated sub-map - self.__keys_dict[key]["title"] = key - self._addChild(self.__keys_dict[key], title_format) - - def removeContactKey(self, key): - """Remove a list panel and all its associated data.""" - contacts = self.__children[key]["panel"].getContacts() - (y, x) = self._parent.getIndex(self.__children[key]["button"]) - self._parent.removeRow(y) - del self.__children[key] - del self.__keys_dict[key] - self.addToRemainingList(contacts) - - def _addChild(self, entry, title_format): - """Add a button and FlowPanel for the corresponding map entry.""" - button = Button(title_format % entry["title"]) - button.setStyleName(self.style["keyItem"]) - if hasattr(entry, "desc"): - button.setTitle(entry["desc"]) - if not "optional" in entry: - entry["optional"] = False - button.setVisible(not entry["optional"]) - y = len(self.__children) + self.offsets["y"] - x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] - - self._parent.insertRow(y) - self._parent.setWidget(y, x, button) - self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) - - _child = ListPanel(self, entry, self.style) - self._parent.setWidget(y, x + 1, _child) - - self.__children[entry["title"]] = {} - self.__children[entry["title"]]["button"] = button - self.__children[entry["title"]]["panel"] = _child - - if hasattr(self, "popup_menu"): - # this is done if self.registerPopupMenuPanel has been called yet - self.popup_menu.registerClickSender(button) - - def _refresh(self, visible=True): - """Set visible the sub-panels that are non optional or non empty, hide the rest.""" - for key in self.__children: - self.setContactPanelVisible(key, False) - if not visible: - return - _map = self.getContacts() - for key in _map: - if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: - self.setContactPanelVisible(key, True) - - def setVisible(self, visible): - self._refresh(visible) - - def setContactPanelVisible(self, key, visible=True, sender=None): - """Do not remove the "sender" param as it is needed for the context menu.""" - self.__children[key]["button"].setVisible(visible) - self.__children[key]["panel"].setVisible(visible) - - @property - def list(self): - """Return the full list of potential contacts.""" - return self.__list - - @property - def keys(self): - return self.__keys_dict.keys() - - @property - def keys_dict(self): - return self.__keys_dict - - @property - def remaining_list(self): - """Return the contacts that have not been selected yet.""" - if not self.__remaining_list_sorted: - self.__remaining_list_sorted = True - self.__remaining_list.sort() - return self.__remaining_list - - def setRemainingListUnsorted(self): - """Mark a change (deletion) so the list will be sorted before it's used.""" - self.__remaining_list_sorted = False - - def removeFromRemainingList(self, contacts): - """Remove contacts after they have been added to a sub-panel.""" - if not isinstance(contacts, list): - contacts = [contacts] - for contact_ in contacts: - if contact_ in self.__remaining_list: - self.__remaining_list.remove(contact_) - - def addToRemainingList(self, contacts, ignore_key=None): - """Add contacts after they have been removed from a sub-panel.""" - if not isinstance(contacts, list): - contacts = [contacts] - assigned_contacts = set() - assigned_map = self.getContacts() - for key_ in assigned_map.keys(): - if ignore_key is not None and key_ == ignore_key: - continue - assigned_contacts.update(assigned_map[key_]) - for contact_ in contacts: - if contact_ not in self.__list or contact_ in self.__remaining_list: - continue - if contact_ in assigned_contacts: - continue # the contact is assigned somewhere else - self.__remaining_list.append(contact_) - self.setRemainingListUnsorted() - - def setContacts(self, _map={}): - """Set the contacts for each contact key.""" - for key in self.__keys_dict: - if key in _map: - self.__children[key]["panel"].setContacts(_map[key]) - else: - self.__children[key]["panel"].setContacts([]) - self._refresh() - - def getContacts(self): - """Get the contacts for all the lists. - @return: a mapping between keys and contact lists.""" - _map = {} - for key in self.__children: - _map[key] = self.__children[key]["panel"].getContacts() - return _map - - @property - def target_drop_cell(self): - """@return: the panel where something has been dropped.""" - return self._target_drop_cell - - def setTargetDropCell(self, target_drop_cell): - """@param: target_drop_cell: the panel where something has been dropped.""" - self._target_drop_cell = target_drop_cell - - def registerPopupMenuPanel(self, entries, hide, callback): - "Register a popup menu panel that will be bound to all contact keys elements." - self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) - - -class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler): - """A draggable AutoCompleteTextBox which is used for representing a contact. - This class is NOT generic because of the onDragEnd method which call methods - from ListPanel. It's probably not reusable for another scenario. - """ - - def __init__(self, parent, event_cbs, style): - AutoCompleteTextBox.__init__(self) - DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type - self._parent = parent - self.event_cbs = event_cbs - self.style = style - self.addMouseListener(self) - self.addFocusListener(self) - self.addChangeListener(self) - self.addStyleName(style["textBox"]) - self.reset() - - def reset(self): - self.setText("") - self.setValid() - - def setValid(self, valid=True): - if self.getText() == "": - valid = True - if valid: - self.removeStyleName(self.style["textBox-invalid"]) - else: - self.addStyleName(self.style["textBox-invalid"]) - self.valid = valid - - def onDragStart(self, event): - self._text = self.getText() - DragLabel.onDragStart(self, event) - self._parent.setTargetDropCell(None) - self.setSelectionRange(len(self.getText()), 0) - - def onDragEnd(self, event): - target = self._parent.target_drop_cell # parent or another ListPanel - if self.getText() == "" or target is None: - return - self.event_cbs["drop"](self, target) - - def setRemoveButton(self): - - def remove_cb(sender): - """Callback for the button to remove this contact.""" - self._parent.remove(self) - self._parent.remove(self.remove_btn) - self.event_cbs["remove"](self) - - self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) - self.remove_btn.setStyleName(self.style["removeButton"]) - self._parent.add(self.remove_btn) - - def removeOrReset(self): - if hasattr(self, "remove_btn"): - self.remove_btn.click() - else: - self.reset() - - def onMouseMove(self, sender): - """Mouse enters the area of a DragAutoCompleteTextBox.""" - if hasattr(sender, "remove_btn"): - sender.remove_btn.setVisible(True) - - def onMouseLeave(self, sender): - """Mouse leaves the area of a DragAutoCompleteTextBox.""" - if hasattr(sender, "remove_btn"): - Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) - - def onFocus(self, sender): - sender.setSelectionRange(0, len(self.getText())) - self.event_cbs["focus"](sender) - - def validate(self): - self.setSelectionRange(len(self.getText()), 0) - self.event_cbs["validate"](self) - - def onChange(self, sender): - """The textbox or list selection is changed""" - if isinstance(sender, ListBox): - AutoCompleteTextBox.onChange(self, sender) - self.validate() - - def onClick(self, sender): - """The list is clicked""" - AutoCompleteTextBox.onClick(self, sender) - self.validate() - - def onKeyUp(self, sender, keycode, modifiers): - """Listen for ENTER key stroke""" - AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) - if keycode == KEY_ENTER: - self.validate() - - -class DropCell(DropWidget): - """A cell where you can drop widgets. This class is NOT generic because of - onDrop which uses methods from ListPanel. It has been created to - separate the drag and drop methods from the others and add a bit of - lisibility, but it's probably not reusable for another scenario. - """ - - def __init__(self, drop_cbs): - DropWidget.__init__(self) - self.drop_cbs = drop_cbs - - def onDragEnter(self, event): - self.addStyleName(self.style["dragoverPanel"]) - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\ - or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\ - or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: - # We check that we are inside widget's box, and we don't remove the style in this case because - # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed - self.removeStyleName(self.style["dragoverPanel"]) - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def onDrop(self, event): - DOM.eventPreventDefault(event) - dt = event.dataTransfer - # 'text', 'text/plain', and 'Text' are equivalent. - item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed - if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and - item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report - if item_type in self.drop_cbs.keys(): - self.drop_cbs[item_type](self, item) - self.removeStyleName(self.style["dragoverPanel"]) - - -VALID = 1 -INVALID = 2 -DELETE = 3 - - -class ListPanel(FlowPanel, DropCell): - """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel - is not fully implemented yet and can not be used with pyjamas.ui.Label.""" - - def __init__(self, parent, entry, style={}): - """Initialization with a button and a DragAutoCompleteTextBox.""" - FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) - drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), - "CONTACT": lambda panel, item: self.addContact(item), - "CONTACT_TITLE": lambda panel, item: self.addContact('@@'), - "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) - } - DropCell.__init__(self, drop_cbs) - self.style = style - self.addStyleName(self.style["keyPanel"]) - self._parent = parent - self.key = entry["title"] - self._addTextBox() - - def _addTextBox(self, switchPrevious=False): - """Add a text box to the last position. If switchPrevious is True, simulate - an insertion before the current last textbox by copying the text and valid state. - @return: the created textbox or the previous one if switchPrevious is True. - """ - if hasattr(self, "_last_textbox"): - if self._last_textbox.getText() == "": - return - self._last_textbox.setRemoveButton() - else: - switchPrevious = False - - def focus_cb(sender): - if sender != self._last_textbox: - # save the current value before it's being modified - self._parent.addToRemainingList(sender.getText(), ignore_key=self.key) - sender.setCompletionItems(self._parent.remaining_list) - - def remove_cb(sender): - """Callback for the button to remove this contact.""" - self._parent.addToRemainingList(sender.getText()) - self._parent.setRemainingListUnsorted() - self._last_textbox.setFocus(True) - - def drop_cb(sender, target): - """Callback when the textbox is drag-n-dropped.""" - parent = sender._parent - if target != parent and target.addContact(sender.getText()): - sender.removeOrReset() - else: - parent._parent.removeFromRemainingList(sender.getText()) - - events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} - textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) - self.add(textbox) - if switchPrevious: - textbox.setText(self._last_textbox.getText()) - textbox.setValid(self._last_textbox.valid) - self._last_textbox.reset() - previous = self._last_textbox - self._last_textbox = textbox - return previous if switchPrevious else textbox - - def _checkContact(self, contact, modify): - """ - @param contact: the contact to check - @param modify: True if the contact is being modified - @return: - - VALID if the contact is valid - - INVALID if the contact is not valid but can be displayed - - DELETE if the contact should not be displayed at all - """ - def countItemInList(list_, item): - """For some reason the built-in count function doesn't work...""" - count = 0 - for elem in list_: - if elem == item: - count += 1 - return count - if contact is None or contact == "": - return DELETE - if countItemInList(self.getContacts(), contact) > (1 if modify else 0): - return DELETE - return VALID if contact in self._parent.list else INVALID - - def addContact(self, contact, sender=None): - """The first parameter type is checked, so it is also possible to call addContact(sender). - If contact is not defined, sender.getText() is used. If sender is not defined, contact will - be written to the last textbox and a new textbox is added afterward. - @param contact: unicode - @param sender: DragAutoCompleteTextBox instance - """ - if isinstance(contact, DragAutoCompleteTextBox): - sender = contact - contact = sender.getText() - valid = self._checkContact(contact, sender is not None) - if sender is None: - # method has been called to modify but to add a contact - if valid == VALID: - # eventually insert before the last textbox if it's not empty - sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox - sender.setText(contact) - else: - sender.setValid(valid == VALID) - if valid != VALID: - if sender is not None and valid == DELETE: - sender.removeOrReset() - return False - if sender == self._last_textbox: - self._addTextBox() - try: - sender.setVisibleLength(len(contact)) - except: - # IndexSizeError: Index or size is negative or greater than the allowed amount - log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact))) - self._parent.removeFromRemainingList(contact) - self._last_textbox.setFocus(True) - return True - - def emptyContacts(self): - """Empty the list of contacts.""" - for child in self.getChildren(): - if hasattr(child, "remove_btn"): - child.remove_btn.click() - - def setContacts(self, tab): - """Set the contacts.""" - self.emptyContacts() - if isinstance(tab, set): - tab = list(tab) - tab.sort() - for contact in tab: - self.addContact(contact) - - def getContacts(self): - """Get the contacts - @return: an array of string""" - tab = [] - for widget in self.getChildren(): - if isinstance(widget, DragAutoCompleteTextBox): - # not to be mixed with EMPTY_SELECTION_ITEM - if widget.getText() != "": - tab.append(widget.getText()) - return tab - - @property - def target_drop_cell(self): - """@return: the panel where something has been dropped.""" - return self._parent.target_drop_cell - - def setTargetDropCell(self, target_drop_cell): - """ - XXX: Property setter here would not make it, you need a proper method! - @param target_drop_cell: the panel where something has been dropped.""" - self._parent.setTargetDropCell(target_drop_cell) - - -class ContactChooserPanel(DialogBox): - """Display the contacts chooser dialog. This has been implemented while - prototyping and is currently not used. Left for an eventual later use. - Replaced by the popup menu which allows to add a panel for Cc or Bcc. - """ - - def __init__(self, manager, **kwargs): - """Display a listbox for each contact key""" - DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) - self.setHTML("Select contacts") - self.manager = manager - self.listboxes = {} - self.contacts = manager.getContacts() - - container = VerticalPanel(Visible=True) - container.addStyleName("marginAuto") - - grid = Grid(2, len(self.manager.keys_dict)) - index = -1 - for key in self.manager.keys_dict: - index += 1 - grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index) - listbox = ListBox() - listbox.setMultipleSelect(True) - listbox.setVisibleItemCount(15) - listbox.addItem(EMPTY_SELECTION_ITEM) - for element in manager.list: - listbox.addItem(element) - self.listboxes[key] = listbox - grid.add(listbox, 1, index) - self._reset() - - buttons = HorizontalPanel() - buttons.addStyleName("marginAuto") - btn_close = Button("Cancel", self.hide) - buttons.add(btn_close) - btn_reset = Button("Reset", self._reset) - buttons.add(btn_reset) - btn_ok = Button("OK", self._validate) - buttons.add(btn_ok) - - container.add(grid) - container.add(buttons) - - self.add(container) - self.center() - - def _reset(self): - """Reset the selections.""" - for key in self.manager.keys_dict: - listbox = self.listboxes[key] - for i in xrange(0, listbox.getItemCount()): - if listbox.getItemText(i) in self.contacts[key]: - listbox.setItemSelected(i, "selected") - else: - listbox.setItemSelected(i, "") - - def _validate(self): - """Sets back the selected contacts to the good sub-panels.""" - _map = {} - for key in self.manager.keys_dict: - selections = self.listboxes[key].getSelectedItemText() - if EMPTY_SELECTION_ITEM in selections: - selections.remove(EMPTY_SELECTION_ITEM) - _map[key] = selections - self.manager.setContacts(_map) - self.hide()
--- a/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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -"""This module configure logs for Libervia browser side""" - -from __pyjamas__ import console -from constants import Const as C -from sat.core import log # XXX: we don't use core.log_config here to avoid the impossible imports in pyjamas - - -class LiberviaLogger(log.Logger): - - def out(self, message, level=None): - if level == C.LOG_LVL_DEBUG: - console.debug(message) - elif level == C.LOG_LVL_INFO: - console.info(message) - elif level == C.LOG_LVL_WARNING: - console.warn(message) - else: - console.error(message) - - -def configure(): - fmt = '[%(name)s] %(message)s' - log.configure(C.LOG_BACKEND_CUSTOM, - logger_class = LiberviaLogger, - level = C.LOG_LVL_DEBUG, - fmt = fmt, - output = None, - logger = None, - colors = False, - force_colors = False) - # FIXME: workaround for Pyjamas, need to be removed when Pyjamas is fixed - LiberviaLogger.fmt = fmt
--- a/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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _ - -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.MenuBar import MenuBar -from pyjamas.ui.MenuItem import MenuItem -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Frame import Frame -from pyjamas import Window - -from jid import JID - -from file_tools import FileUploadPanel -from xmlui import XMLUI -import panels -import dialog -from contact_group import ContactGroupEditor - - -class MenuCmd: - - def __init__(self, object_, handler): - self._object = object_ - self._handler = handler - - def execute(self): - handler = getattr(self._object, self._handler) - handler() - - -class PluginMenuCmd: - - def __init__(self, host, action_id): - self.host = host - self.action_id = action_id - - def execute(self): - self.host.launchAction(self.action_id, None) - - -class LiberviaMenuBar(MenuBar): - - def __init__(self): - MenuBar.__init__(self, vertical=False) - self.setStyleName('gwt-MenuBar-horizontal') # XXX: workaround for the Pyjamas' class name fix (it's now "gwt-MenuBar gwt-MenuBar-horizontal") - # TODO: properly adapt CSS to the new class name - - def doItemAction(self, item, fireCommand): - MenuBar.doItemAction(self, item, fireCommand) - if item == self.items[-1] and self.popup: - self.popup.setPopupPosition(Window.getClientWidth() - - self.popup.getOffsetWidth() - 22, - self.getAbsoluteTop() + - self.getOffsetHeight() - 1) - self.popup.addStyleName('menuLastPopup') - - -class AvatarUpload(FileUploadPanel): - def __init__(self): - texts = {'ok_button': 'Upload avatar', - 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.', - 'errback': "Can't open image... did you actually submit an image?", - 'body_errback': 'Please select another image file.', - 'callback': "Your new profile picture has been set!"} - FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts) - - -class Menu(SimplePanel): - - def __init__(self, host): - self.host = host - SimplePanel.__init__(self) - self.setStyleName('menuContainer') - - def createMenus(self, add_menus): - _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s" - menus_dict = {} - menus_order = [] - - def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd): - """ add a menu to menu_dict """ - log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd)) - try: - menu_bar = menus_dict[menu_name] - except KeyError: - menu_bar = menus_dict[menu_name] = MenuBar(vertical=True) - menus_order.append((menu_name, menu_name_i18n, icon)) - if item_name_i18n and menu_cmd: - menu_bar.addItem(item_name_i18n, menu_cmd) - - addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget")) - addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect")) - addMenu("Contacts", _("Contacts"), None, 'social', None) - addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom")) - addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio")) - addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame")) - addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame")) - - # additional menus - for action_id, type_, path, path_i18n in add_menus: - if not path: - log.warning("skipping menu without path") - continue - if len(path) != len(path_i18n): - log.error("inconsistency between menu paths") - continue - menu_name = path[0] - menu_name_i18n = path_i18n[0] - item_name = path[1:] - if not item_name: - log.warning("skipping menu with a path of lenght 1 [%s]" % path[0]) - continue - item_name_i18n = ' | '.join(path_i18n[1:]) - addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id)) - - # menu items that should be displayed after the automatically added ones - addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups")) - - menus_order.append(None) # we add separator - - addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract")) - addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout")) - addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount")) - addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters")) - - # XXX: temporary, will change when a full profile will be managed in SàT - addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload")) - - menubar = LiberviaMenuBar() - - for menu_data in menus_order: - if menu_data is None: - _separator = MenuItem('', None) - _separator.setStyleName('menuSeparator') - menubar.addItem(_separator, None) - else: - menu_name, menu_name_i18n, icon = menu_data - menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name])) - - self.add(menubar) - - #General menu - def onWebWidget(self): - web_panel = panels.WebPanel(self.host, "http://www.goffi.org") - self.host.addWidget(web_panel) - self.host.setSelected(web_panel) - - def onDisconnect(self): - def confirm_cb(answer): - if answer: - log.info("disconnection") - self.host.bridge.call('disconnect', None) - _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?") - _dialog.show() - - def onSocialContract(self): - _frame = Frame('contrat_social.html') - _frame.setStyleName('infoFrame') - _dialog = dialog.GenericDialog("Contrat Social", _frame) - _dialog.setSize('80%', '80%') - _dialog.show() - - def onAbout(self): - _about = HTML("""<b>Libervia</b>, a Salut à Toi project<br /> -<br /> -You can contact the author at <a href="mailto:goffi@goffi.org">goffi@goffi.org</a><br /> -Blog available (mainly in french) at <a href="http://www.goffi.org" target="_blank">http://www.goffi.org</a><br /> -Project page: <a href="http://sat.goffi.org"target="_blank">http://sat.goffi.org</a><br /> -<br /> -Any help welcome :) -<p style='font-size:small;text-align:center'>This project is dedicated to Roger Poisson</p> -""") - _dialog = dialog.GenericDialog("About", _about) - _dialog.show() - - #Contact menu - def onManageContactGroups(self): - """Open the contact groups manager.""" - - def onCloseCallback(): - pass - - ContactGroupEditor(self.host, None, onCloseCallback) - - #Group menu - def onJoinRoom(self): - - def invite(room_jid, contacts): - for contact in contacts: - self.host.bridge.call('inviteMUC', None, contact, room_jid) - - def join(room_jid, contacts): - if self.host.whoami: - nick = self.host.whoami.node - if room_jid not in [room.bare for room in self.host.room_list]: - self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid, nick) - else: - self.host.getOrCreateLiberviaWidget(panels.ChatPanel, (room_jid, "group"), True, JID(room_jid).bare) - invite(room_jid, contacts) - - dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False)) - - def onCollectiveRadio(self): - def callback(room_jid, contacts): - self.host.bridge.call('launchRadioCollective', None, contacts, room_jid) - dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True)) - - #Game menu - def onTarotGame(self): - def onPlayersSelected(room_jid, other_players): - self.host.bridge.call('launchTarotGame', None, other_players, room_jid) - dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True)) - - def onXiangqiGame(self): - Window.alert("A Xiangqi game is planed, but not available yet") - - #Settings menu - - def onAccount(self): - def gotUI(xmlui): - if not xmlui: - return - body = XMLUI(self.host, xmlui) - _dialog = dialog.GenericDialog("Manage your account", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.show() - self.host.bridge.call('getAccountDialogUI', gotUI) - - def onParameters(self): - def gotParams(xmlui): - if not xmlui: - return - body = XMLUI(self.host, xmlui) - _dialog = dialog.GenericDialog("Parameters", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setSize('80%', '80%') - _dialog.show() - self.host.bridge.call('getParamsUI', gotParams) - - def removeItemParams(self): - """Remove the Parameters item from the Settings menu bar.""" - self.menu_settings.removeItem(self.item_params) - - def onAvatarUpload(self): - body = AvatarUpload() - _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE']) - body.setCloseCb(_dialog.close) - _dialog.setWidth('40%') - _dialog.show()
--- a/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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -""" -This class provide basic DOM parsing based on native javascript parser -__init__ code comes from Tim Down at http://stackoverflow.com/a/8412989 -""" - -from __pyjamas__ import JS - - -class Node(): - - def __init__(self, js_node): - self._node = js_node - - def _jsNodesList2List(self, js_nodes_list): - ret = [] - for i in range(len(js_nodes_list)): - #ret.append(Element(js_nodes_list.item(i))) - ret.append(self.__class__(js_nodes_list.item(i))) # XXX: Ugly, but used to word around a Pyjamas's bug - return ret - - @property - def nodeName(self): - return self._node.nodeName - - @property - def wholeText(self): - return self._node.wholeText - - @property - def childNodes(self): - return self._jsNodesList2List(self._node.childNodes) - - def getAttribute(self, attr): - return self._node.getAttribute(attr) - - def setAttribute(self, attr, value): - return self._node.setAttribute(attr, value) - - def hasAttribute(self, attr): - return self._node.hasAttribute(attr) - - def toxml(self): - return JS("""this._node.outerHTML || new XMLSerializer().serializeToString(this._node);""") - - -class Element(Node): - - def __init__(self, js_node): - Node.__init__(self, js_node) - - def getElementsByTagName(self, tagName): - return self._jsNodesList2List(self._node.getElementsByTagName(tagName)) - - -class Document(Node): - - def __init__(self, js_document): - Node.__init__(self, js_document) - - @property - def documentElement(self): - return Element(self._node.documentElement) - - -class NativeDOM: - - def __init__(self): - JS(""" - - if (typeof window.DOMParser != "undefined") { - this.parseXml = function(xmlStr) { - return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); - }; - } else if (typeof window.ActiveXObject != "undefined" && - new window.ActiveXObject("Microsoft.XMLDOM")) { - this.parseXml = function(xmlStr) { - var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(xmlStr); - return xmlDoc; - }; - } else { - throw new Error("No XML parser found"); - } - """) - - def parseString(self, xml): - return Document(self.parseXml(xml))
--- a/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.<br/>To activate notifications, click on the favicon on the left of the address bar')).show() - return - - log.info("==> Installing Chromium notifications request workaround <==") - self._old_click = wnd().onclick - wnd().onclick = self._chromiumWorkaround - - def _chromiumWorkaround(self): - log.info("Activating workaround") - JS(""" - Notification.requestPermission(function(permission){ - if (permission !== 'granted') - return; - self.enabled = true; //need to use self instead of this - }); - """) - wnd().onclick = self._old_click - - def onFocus(self): - Window.setTitle(self._orig_title) - self._notif_count = 0 - - # def onBlur(self): - # pass - - def isHidden(self): - JS("""return document.hidden;""") - - def _notify(self, title, body, icon): - if not self.enabled: - return - notification = None - JS(""" - notification = new Notification(title, {body: body, icon: icon}); - // Probably won’t work, but it doesn’t hurt to try. - notification.addEventListener('click', function() { - window.focus(); - }); - """) - notification.onshow = lambda: Timer(TIMER_DELAY, lambda timer: notification.close()) - - def highlightTab(self): - self._notif_count += 1 - Window.setTitle("%s (%d)" % (self._orig_title, self._notif_count)) - - def notify(self, title, body, icon='/media/icons/apps/48/sat.png'): - if self.isHidden(): - self._notify(title, body, icon) - self.highlightTab()
--- a/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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat_frontends.tools.strings import addURLToText -from sat_frontends.tools.games import SYMBOLS -from sat.core.i18n import _ - -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.HTMLPanel import HTMLPanel -from pyjamas.ui.Frame import Frame -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.Image import Image -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN, KeyboardHandler -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.FocusListener import FocusHandler -from pyjamas.Timer import Timer -from pyjamas import DOM -from pyjamas import Window -from __pyjamas__ import doc - -from datetime import datetime -from time import time - -from jid import JID -from html_tools import html_sanitize -from base_panels import ChatText, OccupantsList, PopupMenuPanel, BaseTextEditor, LightTextEditor, HTMLTextEditor -from card_game import CardPanel -from radiocol import RadioColPanel -from menu import Menu -import dialog -import base_widget -import richtext -import contact -from constants import Const as C -from plugin_xep_0085 import ChatStateMachine - - -# TODO: at some point we should decide which behaviors to keep and remove these two constants -TOGGLE_EDITION_USE_ICON = False # set to True to use an icon inside the "toggle syntax" button -NEW_MESSAGE_USE_BUTTON = False # set to True to display the "New message" button instead of an empty entry - - -class UniBoxPanel(HorizontalPanel): - """Panel containing the UniBox""" - - def __init__(self, host): - HorizontalPanel.__init__(self) - self.host = host - self.setStyleName('uniBoxPanel') - self.unibox = None - - def refresh(self): - """Enable or disable this panel. Contained widgets are created when necessary.""" - enable = self.host.getUIParam('unibox') - self.setVisible(enable) - if enable and not self.unibox: - self.button = Button('<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>') - self.button.setTitle('Open the rich text editor') - self.button.addStyleName('uniBoxButton') - self.add(self.button) - self.unibox = UniBox(self.host) - self.add(self.unibox) - self.setCellWidth(self.unibox, '100%') - self.button.addClickListener(self.openRichMessageEditor) - self.unibox.addKey("@@: ") - self.unibox.onSelectedChange(self.host.getSelected()) - - def openRichMessageEditor(self): - """Open the rich text editor.""" - self.button.setVisible(False) - self.unibox.setVisible(False) - self.setCellWidth(self.unibox, '0px') - self.host.panel._contactsMove(self) - - def afterEditCb(): - Window.removeWindowResizeListener(self) - self.host.panel._contactsMove(self.host.panel._hpanel) - self.setCellWidth(self.unibox, '100%') - self.button.setVisible(True) - self.unibox.setVisible(True) - self.host.resize() - - richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) - Window.addWindowResizeListener(self) - self.host.resize() - - def onWindowResized(self, width, height): - right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() - left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() - ideal_width = right - left - 40 - self.host.richtext.setWidth("%spx" % ideal_width) - - -class MessageBox(TextArea): - """A basic text area for entering messages""" - - def __init__(self, host): - TextArea.__init__(self) - self.host = host - self.__size = (0, 0) - self.setStyleName('messageBox') - self.addKeyboardListener(self) - MouseHandler.__init__(self) - self.addMouseListener(self) - self._selected_cache = None - - def onBrowserEvent(self, event): - # XXX: woraroung a pyjamas bug: self.currentEvent is not set - # so the TextBox's cancelKey doens't work. This is a workaround - # FIXME: fix the bug upstream - self.currentEvent = event - TextArea.onBrowserEvent(self, event) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - - def history_cb(text): - self.setText(text) - Timer(5, lambda timer: self.setCursorPos(len(text))) - - if keycode == KEY_ENTER: - if _txt: - self._selected_cache.onTextEntered(_txt) - self.host._updateInputHistory(_txt) - self.setText('') - sender.cancelKey() - elif keycode == KEY_UP: - self.host._updateInputHistory(_txt, -1, history_cb) - elif keycode == KEY_DOWN: - self.host._updateInputHistory(_txt, +1, history_cb) - else: - self.__onComposing() - - def __onComposing(self): - """Callback when the user is composing a text.""" - if hasattr(self._selected_cache, "target"): - self._selected_cache.state_machine._onEvent("composing") - - def onMouseUp(self, sender, x, y): - size = (self.getOffsetWidth(), self.getOffsetHeight()) - if size != self.__size: - self.__size = size - self.host.resize() - - def onSelectedChange(self, selected): - self._selected_cache = selected - - -class UniBox(MessageBox, MouseHandler): # AutoCompleteTextBox): - """This text box is used as a main typing point, for message, microblog, etc""" - - def __init__(self, host): - MessageBox.__init__(self, host) - #AutoCompleteTextBox.__init__(self) - self.setStyleName('uniBox') - host.addSelectedListener(self.onSelectedChange) - - def addKey(self, key): - return - #self.getCompletionItems().completions.append(key) - - def removeKey(self, key): - return - # TODO: investigate why AutoCompleteTextBox doesn't work here, - # maybe it can work on a TextBox but no TextArea. Remove addKey - # and removeKey methods if they don't serve anymore. - try: - self.getCompletionItems().completions.remove(key) - except KeyError: - log.warning("trying to remove an unknown key") - - def _getTarget(self, txt): - """ Say who will receive the messsage - @return: a tuple (selected, target_type, target info) with: - - target_hook: None if we use the selected widget, (msg, data) if we have a hook (e.g. "@@: " for a public blog), where msg is the parsed message (i.e. without the "hook key: "@@: bla" become ("bla", None)) - - target_type: one of PUBLIC, GROUP, ONE2ONE, STATUS, MISC - - msg: HTML message which will appear in the privacy warning banner """ - target = self._selected_cache - - def getSelectedOrStatus(): - if target and target.isSelectable(): - _type, msg = target.getWarningData() - target_hook = None # we use the selected widget, not a hook - else: - _type, msg = "STATUS", "This will be your new status message" - target_hook = (txt, None) - return (target_hook, _type, msg) - - if not txt.startswith('@'): - target_hook, _type, msg = getSelectedOrStatus() - elif txt.startswith('@@: '): - _type = "PUBLIC" - msg = MicroblogPanel.warning_msg_public - target_hook = (txt[4:], None) - elif txt.startswith('@'): - _end = txt.find(': ') - if _end == -1: - target_hook, _type, msg = getSelectedOrStatus() - else: - group = txt[1:_end] # only one target group is managed for the moment - if not group or not group in self.host.contact_panel.getGroups(): - # the group doesn't exists, we ignore the key - group = None - target_hook, _type, msg = getSelectedOrStatus() - else: - _type = "GROUP" - msg = MicroblogPanel.warning_msg_group % group - target_hook = (txt[_end + 2:], group) - else: - log.error("Unknown target") - target_hook, _type, msg = getSelectedOrStatus() - - return (target_hook, _type, msg) - - def onKeyPress(self, sender, keycode, modifiers): - _txt = self.getText() - target_hook, type_, msg = self._getTarget(_txt) - - if keycode == KEY_ENTER: - if _txt: - if target_hook: - parsed_txt, data = target_hook - self.host.send([(type_, data)], parsed_txt) - self.host._updateInputHistory(_txt) - self.setText('') - self.host.showWarning(None, None) - else: - self.host.showWarning(type_, msg) - MessageBox.onKeyPress(self, sender, keycode, modifiers) - - def getTargetAndData(self): - """For external use, to get information about the (hypothetical) message - that would be sent if we press Enter right now in the unibox. - @return a tuple (target, data) with: - - data: what would be the content of the message (body) - - target: JID, group with the prefix "@" or the public entity "@@" - """ - _txt = self.getText() - target_hook, _type, _msg = self._getTarget(_txt) - if target_hook: - data, target = target_hook - if target is None: - return target_hook - return (data, "@%s" % (target if target != "" else "@")) - if isinstance(self._selected_cache, MicroblogPanel): - groups = self._selected_cache.accepted_groups - target = "@%s" % (groups[0] if len(groups) > 0 else "@") - if len(groups) > 1: - Window.alert("Sole the first group of the selected panel is taken in consideration: '%s'" % groups[0]) - elif isinstance(self._selected_cache, ChatPanel): - target = self._selected_cache.target - else: - target = None - return (_txt, target) - - def onWidgetClosed(self, lib_wid): - """Called when a libervia widget is closed""" - if self._selected_cache == lib_wid: - self.onSelectedChange(None) - - """def complete(self): - - #self.visible=False #XXX: self.visible is not unset in pyjamas when ENTER is pressed and a completion is done - #XXX: fixed directly on pyjamas, if the patch is accepted, no need to walk around this - return AutoCompleteTextBox.complete(self)""" - - -class WarningPopup(): - - def __init__(self): - self._popup = None - self._timer = Timer(notify=self._timeCb) - - def showWarning(self, type_=None, msg=None, duration=2000): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - If type_ is None, a popup being currently displayed will be hidden. - @type_: a type determining the CSS style to be applied (see __showWarning) - @msg: message to be displayed - """ - if type_ is None: - self.__removeWarning() - return - if not self._popup: - self.__showWarning(type_, msg) - elif (type_, msg) != self._popup.target_data: - self._timeCb(None) # we remove the popup - self.__showWarning(type_, msg) - - self._timer.schedule(duration) - - def __showWarning(self, type_, msg): - """Display a popup information message, e.g. to notify the recipient of a message being composed. - @type_: a type determining the CSS style to be applied. For now the defined styles are - "NONE" (will do nothing), "PUBLIC", "GROUP", "STATUS" and "ONE2ONE". - @msg: message to be displayed - """ - if type_ == "NONE": - return - if not msg: - log.warning("no msg set uniBox warning") - return - if type_ == "PUBLIC": - style = "targetPublic" - elif type_ == "GROUP": - style = "targetGroup" - elif type_ == "STATUS": - style = "targetStatus" - elif type_ == "ONE2ONE": - style = "targetOne2One" - else: - log.error("unknown message type") - return - contents = HTML(msg) - - self._popup = dialog.PopupPanelWrapper(autoHide=False, modal=False) - self._popup.target_data = (type_, msg) - self._popup.add(contents) - self._popup.setStyleName("warningPopup") - if style: - self._popup.addStyleName(style) - - left = 0 - top = 0 # max(0, self.getAbsoluteTop() - contents.getOffsetHeight() - 2) - self._popup.setPopupPosition(left, top) - self._popup.show() - - def _timeCb(self, timer): - if self._popup: - self._popup.hide() - del self._popup - self._popup = None - - def __removeWarning(self): - """Remove the popup""" - self._timeCb(None) - - -class MicroblogItem(): - # XXX: should be moved in a separated module - - def __init__(self, data): - self.id = data['id'] - self.type = data.get('type', 'main_item') - self.empty = data.get('new', False) - self.title = data.get('title', '') - self.title_xhtml = data.get('title_xhtml', '') - self.content = data.get('content', '') - self.content_xhtml = data.get('content_xhtml', '') - self.author = data['author'] - self.updated = float(data.get('updated', 0)) # XXX: int doesn't work here - self.published = float(data.get('published', self.updated)) # XXX: int doesn't work here - self.service = data.get('service', '') - self.node = data.get('node', '') - self.comments = data.get('comments', False) - self.comments_service = data.get('comments_service', '') - self.comments_node = data.get('comments_node', '') - - -class MicroblogEntry(SimplePanel, ClickHandler, FocusHandler, KeyboardHandler): - - def __init__(self, blog_panel, data): - """ - @param blog_panel: the parent panel - @param data: dict containing the blog item data, or a MicroblogItem instance. - """ - self._base_item = data if isinstance(data, MicroblogItem) else MicroblogItem(data) - for attr in ['id', 'type', 'empty', 'title', 'title_xhtml', 'content', 'content_xhtml', - 'author', 'updated', 'published', 'comments', 'service', 'node', - 'comments_service', 'comments_node']: - getter = lambda attr: lambda inst: getattr(inst._base_item, attr) - setter = lambda attr: lambda inst, value: setattr(inst._base_item, attr, value) - setattr(MicroblogEntry, attr, property(getter(attr), setter(attr))) - - SimplePanel.__init__(self) - self._blog_panel = blog_panel - - self.panel = FlowPanel() - self.panel.setStyleName('mb_entry') - - self.header = HTMLPanel('') - self.panel.add(self.header) - - self.entry_actions = VerticalPanel() - self.entry_actions.setStyleName('mb_entry_actions') - self.panel.add(self.entry_actions) - - entry_avatar = SimplePanel() - entry_avatar.setStyleName('mb_entry_avatar') - self.avatar = Image(self._blog_panel.host.getAvatar(self.author)) - entry_avatar.add(self.avatar) - self.panel.add(entry_avatar) - - if TOGGLE_EDITION_USE_ICON: - self.entry_dialog = HorizontalPanel() - else: - self.entry_dialog = VerticalPanel() - self.entry_dialog.setStyleName('mb_entry_dialog') - self.panel.add(self.entry_dialog) - - self.add(self.panel) - ClickHandler.__init__(self) - self.addClickListener(self) - - self.__pub_data = (self.service, self.node, self.id) - self.__setContent() - - def __setContent(self): - """Actually set the entry content (header, icons, bubble...)""" - self.delete_label = self.update_label = self.comment_label = None - self.bubble = self._current_comment = None - self.__setHeader() - self.__setBubble() - self.__setIcons() - - def __setHeader(self): - """Set the entry header""" - if self.empty: - return - update_text = u" — ✍ " + "<span class='mb_entry_timestamp'>%s</span>" % datetime.fromtimestamp(self.updated) - self.header.setHTML("""<div class='mb_entry_header'> - <span class='mb_entry_author'>%(author)s</span> on - <span class='mb_entry_timestamp'>%(published)s</span>%(updated)s - </div>""" % {'author': html_sanitize(self.author), - 'published': datetime.fromtimestamp(self.published), - 'updated': update_text if self.published != self.updated else '' - } - ) - - def __setIcons(self): - """Set the entry icons (delete, update, comment)""" - if self.empty: - return - - def addIcon(label, title): - label = Label(label) - label.setTitle(title) - label.addClickListener(self) - self.entry_actions.add(label) - return label - - if self.comments: - self.comment_label = addIcon(u"↶", "Comment this message") - self.comment_label.setStyleName('mb_entry_action_larger') - is_publisher = self.author == self._blog_panel.host.whoami.bare - if is_publisher: - self.update_label = addIcon(u"✍", "Edit this message") - if is_publisher or str(self.node).endswith(self._blog_panel.host.whoami.bare): - self.delete_label = addIcon(u"✗", "Delete this message") - - def updateAvatar(self, new_avatar): - """Change the avatar of the entry - @param new_avatar: path to the new image""" - self.avatar.setUrl(new_avatar) - - def onClick(self, sender): - if sender == self: - try: # prevent re-selection of the main entry after a comment has been focused - if self.__ignoreNextEvent: - self.__ignoreNextEvent = False - return - except AttributeError: - pass - self._blog_panel.setSelectedEntry(self) - elif sender == self.delete_label: - self._delete() - elif sender == self.update_label: - self.edit(True) - elif sender == self.comment_label: - self.__ignoreNextEvent = True - self._comment() - - def __modifiedCb(self, content): - """Send the new content to the backend - @return: False to restore the original content if a deletion has been cancelled - """ - if not content['text']: # previous content has been emptied - self._delete(True) - return False - extra = {'published': str(self.published)} - if isinstance(self.bubble, richtext.RichTextEditor): - # TODO: if the user change his parameters after the message edition started, - # the message syntax could be different then the current syntax: pass the - # message syntax in extra for the frontend to use it instead of current syntax. - extra.update({'content_rich': content['text'], 'title': content['title']}) - if self.empty: - if self.type == 'main_item': - self._blog_panel.host.bridge.call('sendMblog', None, None, self._blog_panel.accepted_groups, content['text'], extra) - else: - self._blog_panel.host.bridge.call('sendMblogComment', None, self._parent_entry.comments, content['text'], extra) - else: - self._blog_panel.host.bridge.call('updateMblog', None, self.__pub_data, self.comments, content['text'], extra) - return True - - def __afterEditCb(self, content): - """Remove the entry if it was an empty one (used for creating a new blog post). - Data for the actual new blog post will be received from the bridge""" - if self.empty: - self._blog_panel.removeEntry(self.type, self.id) - if self.type == 'main_item': # restore the "New message" button - self._blog_panel.refresh() - else: # allow to create a new comment - self._parent_entry._current_comment = None - self.entry_dialog.setWidth('auto') - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - - def __setBubble(self, edit=False): - """Set the bubble displaying the initial content.""" - content = {'text': self.content_xhtml if self.content_xhtml else self.content, - 'title': self.title_xhtml if self.title_xhtml else self.title} - if self.content_xhtml: - content.update({'syntax': C.SYNTAX_XHTML}) - if self.author != self._blog_panel.host.whoami.bare: - options = ['read_only'] - else: - options = [] if self.empty else ['update_msg'] - self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) - else: # assume raw text message have no title - self.bubble = LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) - self.bubble.addStyleName("bubble") - try: - self.toggle_syntax_button.removeFromParent() - except TypeError: - pass - self.entry_dialog.add(self.bubble) - self.edit(edit) - self.bubble.addEditListener(self.__showWarning) - - def __showWarning(self, sender, keycode): - if keycode == KEY_ENTER: - self._blog_panel.host.showWarning(None, None) - else: - self._blog_panel.host.showWarning(*self._blog_panel.getWarningData(self.type == 'comment')) - - def _delete(self, empty=False): - """Ask confirmation for deletion. - @return: False if the deletion has been cancelled.""" - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('deleteMblog', None, self.__pub_data, self.comments) - else: # restore the text if it has been emptied during the edition - self.bubble.setContent(self.bubble._original_content) - - if self.empty: - text = _("New ") + (_("message") if self.comments else _("comment")) + _(" without body has been cancelled.") - dialog.InfoDialog(_("Information"), text).show() - return - text = "" - if empty: - text = (_("Message") if self.comments else _("Comment")) + _(" body has been emptied.<br/>") - target = _('message and all its comments') if self.comments else _('comment') - text += _("Do you really want to delete this %s?") % target - dialog.ConfirmDialog(confirm_cb, text=text).show() - - def _comment(self): - """Add an empty entry for a new comment""" - if self._current_comment: - self._current_comment.bubble.setFocus(True) - self._blog_panel.setSelectedEntry(self._current_comment) - return - data = {'id': str(time()), - 'new': True, - 'type': 'comment', - 'author': self._blog_panel.host.whoami.bare, - 'service': self.comments_service, - 'node': self.comments_node - } - entry = self._blog_panel.addEntry(data) - if entry is None: - log.info("The entry of id %s can not be commented" % self.id) - return - entry._parent_entry = self - self._current_comment = entry - self.edit(True, entry) - self._blog_panel.setSelectedEntry(entry) - - def edit(self, edit, entry=None): - """Toggle the bubble between display and edit mode - @edit: boolean value - @entry: MicroblogEntry instance, or None to use self - """ - if entry is None: - entry = self - try: - entry.toggle_syntax_button.removeFromParent() - except TypeError: - pass - entry.bubble.edit(edit) - if edit: - if isinstance(entry.bubble, richtext.RichTextEditor): - image = '<a class="richTextIcon">A</a>' - html = '<a style="color: blue;">raw text</a>' - title = _('Switch to raw text edition') - else: - image = '<img src="media/icons/tango/actions/32/format-text-italic.png" class="richTextIcon"/>' - html = '<a style="color: blue;">rich text</a>' - title = _('Switch to rich text edition') - if TOGGLE_EDITION_USE_ICON: - entry.entry_dialog.setWidth('80%') - entry.toggle_syntax_button = Button(image, entry.toggleContentSyntax) - entry.toggle_syntax_button.setTitle(title) - entry.entry_dialog.add(entry.toggle_syntax_button) - else: - entry.toggle_syntax_button = HTML(html) - entry.toggle_syntax_button.addClickListener(entry.toggleContentSyntax) - entry.toggle_syntax_button.addStyleName('mb_entry_toggle_syntax') - entry.entry_dialog.add(entry.toggle_syntax_button) - entry.toggle_syntax_button.setStyleAttribute('top', '-20px') # XXX: need to force CSS - entry.toggle_syntax_button.setStyleAttribute('left', '-20px') - - def toggleContentSyntax(self): - """Toggle the editor between raw and rich text""" - original_content = self.bubble.getOriginalContent() - rich = not isinstance(self.bubble, richtext.RichTextEditor) - if rich: - original_content['syntax'] = C.SYNTAX_XHTML - - def setBubble(text): - self.content = text - self.content_xhtml = text if rich else '' - self.content_title = self.content_title_xhtml = '' - self.bubble.removeFromParent() - self.__setBubble(True) - self.bubble.setOriginalContent(original_content) - if rich: - self.bubble.setDisplayContent() # needed in case the edition is aborted, to not end with an empty bubble - - text = self.bubble.getContent()['text'] - if not text: - setBubble(' ') # something different than empty string is needed to initialize the rich text editor - return - if not rich: - def confirm_cb(answer): - if answer: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_CURRENT, C.SYNTAX_TEXT) - dialog.ConfirmDialog(confirm_cb, text=_("Do you really want to lose the title and text formatting?")).show() - else: - self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) - - -class MicroblogPanel(base_widget.LiberviaWidget): - warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" - warning_msg_group = "This message will be published for all the people of the group <span class='warningTarget'>%s</span>" - - def __init__(self, host, accepted_groups): - """Panel used to show microblog - @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts - """ - base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) - self.setAcceptedGroup(accepted_groups) - self.host = host - self.entries = {} - self.comments = {} - self.selected_entry = None - self.vpanel = VerticalPanel() - self.vpanel.setStyleName('microblogPanel') - self.setWidget(self.vpanel) - - def refresh(self): - """Refresh the display of this widget. If the unibox is disabled, - display the 'New message' button or an empty bubble on top of the panel""" - if hasattr(self, 'new_button'): - self.new_button.setVisible(self.host.uni_box is None) - return - if self.host.uni_box is None: - def addBox(): - if hasattr(self, 'new_button'): - self.new_button.setVisible(False) - data = {'id': str(time()), - 'new': True, - 'author': self.host.whoami.bare, - } - entry = self.addEntry(data) - entry.edit(True) - if NEW_MESSAGE_USE_BUTTON: - self.new_button = Button("New message", listener=addBox) - self.new_button.setStyleName("microblogNewButton") - self.vpanel.insert(self.new_button, 0) - 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 <span class='warningTarget'>comment</span> and keep the initial post visibility, so it is potentialy public") - elif not self._accepted_groups: - # we have a meta MicroblogPanel, we publish publicly - return ("PUBLIC", self.warning_msg_public) - else: - # we only accept one group at the moment - # FIXME: manage several groups - return ("GROUP", self.warning_msg_group % self._accepted_groups[0]) - - def onTextEntered(self, text): - if self.selected_entry: - # we are entering a comment - comments_url = self.selected_entry.comments - if not comments_url: - raise Exception("ERROR: the comments URL is empty") - target = ("COMMENT", comments_url) - elif not self._accepted_groups: - # we are entering a public microblog - target = ("PUBLIC", None) - else: - # we are entering a microblog restricted to a group - # FIXME: manage several groups - target = ("GROUP", self._accepted_groups[0]) - self.host.send([target], text) - - def accept_all(self): - return not self._accepted_groups # we accept every microblog only if we are not filtering by groups - - def getEntries(self): - """Ask all the entries for the currenly accepted groups, - and fill the panel""" - - def massiveInsert(self, mblogs): - """Insert several microblogs at once - @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs - """ - log.debug("Massive insertion of %d microblogs" % len(mblogs)) - for publisher in mblogs: - log.debug("adding blogs for [%s]" % publisher) - for mblog in mblogs[publisher]: - if not "content" in mblog: - log.warning("No content found in microblog [%s]", mblog) - continue - self.addEntry(mblog) - - def mblogsInsert(self, mblogs): - """ Insert several microblogs at once - @param mblogs: list of microblogs - """ - for mblog in mblogs: - if not "content" in mblog: - log.warning("No content found in microblog [%s]", mblog) - continue - self.addEntry(mblog) - - def _chronoInsert(self, vpanel, entry, reverse=True): - """ Insert an entry in chronological order - @param vpanel: VerticalPanel instance - @param entry: MicroblogEntry - @param reverse: more recent entry on top if True, chronological order else""" - 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 <span class='warningTarget'>%s</span>" % self.target - elif self.type == "group": - msg = "This message will be sent to all the participants of the multi-user room <span class='warningTarget'>%s</span>" % self.target - return ("ONE2ONE" if self.type == "one2one" else "GROUP", msg) - - def onTextEntered(self, text): - self.host.send([("groupchat" if self.type == 'group' else "chat", str(self.target))], text) - self.state_machine._onEvent("active") - - def onQuit(self): - base_widget.LiberviaWidget.onQuit(self) - if self.type == 'group': - self.host.bridge.call('mucLeave', None, self.target.bare) - - def setUserNick(self, nick): - """Set the nick of the user, usefull for e.g. change the color of the user""" - self.nick = nick - - def setPresents(self, nicks): - """Set the users presents in this room - @param occupants: list of nicks (string)""" - self.occupants_list.clear() - for nick in nicks: - self.occupants_list.addOccupant(nick) - - def userJoined(self, nick, data): - self.occupants_list.addOccupant(nick) - self.printInfo("=> %s has joined the room" % nick) - - def userLeft(self, nick, data): - self.occupants_list.removeOccupant(nick) - self.printInfo("<= %s has left the room" % nick) - - def changeUserNick(self, old_nick, new_nick): - assert(self.type == "group") - self.occupants_list.removeOccupant(old_nick) - self.occupants_list.addOccupant(new_nick) - self.printInfo(_("%(old_nick)s is now known as %(new_nick)s") % {'old_nick': old_nick, 'new_nick': new_nick}) - - def historyPrint(self, size=20): - """Print the initial history""" - def getHistoryCB(history): - # display day change - day_format = "%A, %d %b %Y" - previous_day = datetime.now().strftime(day_format) - for line in history: - timestamp, from_jid, to_jid, message, mess_type, extra = line - message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) - if previous_day != message_day: - self.printInfo("* " + message_day) - previous_day = message_day - self.printMessage(from_jid, message, extra, timestamp) - self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, self.target.bare, size, True) - - def printInfo(self, msg, type_='normal', link_cb=None): - """Print general info - @param msg: message to print - @param type_: one of: - "normal": general info like "toto has joined the room" - "link": general info that is clickable like "click here to join the main room" - "me": "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - @param link_cb: method to call when the info is clicked, ignored if type_ is not 'link' - """ - _wid = HTML(msg) if type_ == 'link' else Label(msg) - if type_ == 'normal': - _wid.setStyleName('chatTextInfo') - elif type_ == 'link': - _wid.setStyleName('chatTextInfo-link') - if link_cb: - _wid.addClickListener(link_cb) - elif type_ == 'me': - _wid.setStyleName('chatTextMe') - else: - _wid.setStyleName('chatTextInfo') - self.content.add(_wid) - - def printMessage(self, from_jid, msg, extra, timestamp=None): - """Print message in chat window. Must be implemented by child class""" - _jid = JID(from_jid) - nick = _jid.node if self.type == 'one2one' else _jid.resource - mymess = _jid.resource == self.nick if self.type == "group" else _jid.bare == self.host.whoami.bare # mymess = True if message comes from local user - if msg.startswith('/me '): - self.printInfo('* %s %s' % (nick, msg[4:]), type_='me') - return - self.content.add(ChatText(timestamp, nick, mymess, msg, extra.get('xhtml'))) - self.content_scroll.scrollToBottom() - - def startGame(self, game_type, waiting, referee, players, *args): - """Configure the chat window to start a game""" - classes = {"Tarot": CardPanel, "RadioCol": RadioColPanel} - if game_type not in classes.keys(): - return # unknown game - attr = game_type.lower() - self.occupants_list.updateSpecials(players, SYMBOLS[attr]) - if waiting or not self.nick in players: - return # waiting for player or not playing - attr = "%s_panel" % attr - if hasattr(self, attr): - return - log.info("%s Game Started \o/" % game_type) - panel = classes[game_type](self, referee, self.nick, players, *args) - setattr(self, attr, panel) - self.vpanel.insert(panel, 0) - self.vpanel.setCellHeight(panel, panel.getHeight()) - - def getGame(self, game_type): - """Return class managing the game type""" - # TODO: check that the game is launched, and manage errors - if game_type == "Tarot": - return self.tarot_panel - elif game_type == "RadioCol": - return self.radiocol_panel - - def setState(self, state, nick=None): - """Set the chat state (XEP-0085) of the contact. Leave nick to None - to set the state for a one2one conversation, or give a nickname or - C.ALL_OCCUPANTS to set the state of a participant within a MUC. - @param state: the new chat state - @param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS - """ - if nick: - assert(self.type == 'group') - occupants = self.occupants_list.occupants_list.keys() if nick == C.ALL_OCCUPANTS else [nick] - for occupant in occupants: - self.occupants_list.occupants_list[occupant].setState(state) - else: - assert(self.type == 'one2one') - self._state = state - self.refreshTitle() - self.state_machine.started = not not state # start to send "composing" state from now - - def refreshTitle(self): - """Refresh the title of this ChatPanel dialog""" - if self._state: - self.setTitle(self.target.bare + " (" + self._state + ")") - else: - self.setTitle(self.target.bare) - - -class WebPanel(base_widget.LiberviaWidget): - """ (mini)browser like widget """ - - def __init__(self, host, url=None): - """ - @param host: SatWebFrontend instance - """ - base_widget.LiberviaWidget.__init__(self, host) - self._vpanel = VerticalPanel() - self._vpanel.setSize('100%', '100%') - self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) - self._url.setText(url or "") - self._url.setWidth('100%') - hpanel = HorizontalPanel() - hpanel.add(self._url) - btn = Button("Go", self.onUrlClick) - hpanel.setCellWidth(self._url, "100%") - #self.setCellWidth(btn, "10%") - hpanel.add(self._url) - hpanel.add(btn) - self._vpanel.add(hpanel) - self._vpanel.setCellHeight(hpanel, '20px') - self._frame = Frame(url or "") - self._frame.setSize('100%', '100%') - DOM.setStyleAttribute(self._frame.getElement(), "position", "relative") - self._vpanel.add(self._frame) - self.setWidget(self._vpanel) - - def onUrlClick(self, sender): - self._frame.setUrl(self._url.getText()) - - -class MainPanel(AbsolutePanel): - - def __init__(self, host): - self.host = host - AbsolutePanel.__init__(self) - - # menu - self.menu = Menu(host) - - # unibox - self.unibox_panel = UniBoxPanel(host) - self.unibox_panel.setVisible(False) - - # contacts - self._contacts = HorizontalPanel() - self._contacts.addStyleName('globalLeftArea') - self.contacts_switch = Button(u'«', self._contactsSwitch) - self.contacts_switch.addStyleName('contactsSwitch') - self._contacts.add(self.contacts_switch) - self._contacts.add(self.host.contact_panel) - - # tabs - self.tab_panel = base_widget.MainTabPanel(host) - self.discuss_panel = base_widget.WidgetsPanel(self.host, locked=True) - self.tab_panel.add(self.discuss_panel, "Discussions") - self.tab_panel.selectTab(0) - - self.header = AbsolutePanel() - self.header.add(self.menu) - self.header.add(self.unibox_panel) - self.header.add(self.host.status_panel) - self.header.setStyleName('header') - self.add(self.header) - - self._hpanel = HorizontalPanel() - self._hpanel.add(self._contacts) - self._hpanel.add(self.tab_panel) - self.add(self._hpanel) - - self.setWidth("100%") - Window.addWindowResizeListener(self) - - def _contactsSwitch(self, btn=None): - """ (Un)hide contacts panel """ - if btn is None: - btn = self.contacts_switch - cpanel = self.host.contact_panel - cpanel.setVisible(not cpanel.getVisible()) - btn.setText(u"«" if cpanel.getVisible() else u"»") - self.host.resize() - - def _contactsMove(self, parent): - """Move the contacts container (containing the contact list and - the "hide/show" button) to another parent, but always as the - first child position (insert at index 0). - """ - if self._contacts.getParent(): - if self._contacts.getParent() == parent: - return - self._contacts.removeFromParent() - parent.insert(self._contacts, 0) - - def onWindowResized(self, width, height): - _elts = doc().getElementsByClassName('gwt-TabBar') - if not _elts.length: - tab_bar_h = 0 - else: - tab_bar_h = _elts.item(0).offsetHeight - ideal_height = Window.getClientHeight() - tab_bar_h - self.setHeight("%s%s" % (ideal_height, "px")) - - def refresh(self): - """Refresh the main panel""" - self.unibox_panel.refresh()
--- a/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 <http://www.gnu.org/licenses/>. - -from pyjamas.Timer import Timer - - -# Copy of the map from sat/src/plugins/plugin_xep_0085 -TRANSITIONS = {"active": {"next_state": "inactive", "delay": 120}, - "inactive": {"next_state": "gone", "delay": 480}, - "gone": {"next_state": "", "delay": 0}, - "composing": {"next_state": "paused", "delay": 30}, - "paused": {"next_state": "inactive", "delay": 450} - } - - -class ChatStateMachine: - """This is an adapted version of the ChatStateMachine from sat/src/plugins/plugin_xep_0085 - which manage a timer on the web browser and keep it synchronized with the timer that runs - on the backend. This is only needed to avoid calling the bridge method chatStateComposing - too often ; accuracy is not needed so we can ignore the delay of the communication between - the web browser and the backend (the timer on the web browser always starts a bit before). - /!\ Keep this file up to date if you modify the one in the sat plugins directory. - """ - def __init__(self, host, target_s): - - self.host = host - self.target_s = target_s - self.started = False - self.state = None - self.timer = None - - def _onEvent(self, state): - """Pyjamas callback takes no extra argument so we need this trick""" - # Here we should check the value of the parameter "Send chat state notifications" - # but this costs two messages. It's even better to call chatStateComposing - # with a doubt, it will be checked by the back-end anyway before sending - # the actual notifications to the other client. - if state == "composing" and not self.started: - return - self.started = True - self.next_state = state - self.__onEvent(None) - - def __onEvent(self, timer): - # print "on event %s" % self.next_state - state = self.next_state - self.next_state = "" - if state != self.state and state == "composing": - self.host.bridge.call('chatStateComposing', None, self.target_s) - self.state = state - if not self.timer is None: - self.timer.cancel() - - if not state in TRANSITIONS: - return - if not "next_state" in TRANSITIONS[state]: - return - if not "delay" in TRANSITIONS[state]: - return - next_state = TRANSITIONS[state]["next_state"] - delay = TRANSITIONS[state]["delay"] - if next_state == "" or delay < 0: - return - self.next_state = next_state - # pyjamas timer in milliseconds - self.timer = Timer(delay * 1000, self.__onEvent)
--- a/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 <goffi@goffi.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat_frontends.tools.misc import DEFAULT_MUC -from sat.core.i18n import _ - -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Label import Label -from pyjamas.ui.Button import Button -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.Hidden import Hidden -from pyjamas.ui.CaptionPanel import CaptionPanel -from pyjamas.media.Audio import Audio -from pyjamas import Window -from pyjamas.Timer import Timer - -from html_tools import html_sanitize -from file_tools import FilterFileUpload - - -class MetadataPanel(FlexTable): - - def __init__(self): - FlexTable.__init__(self) - title_lbl = Label("title:") - title_lbl.setStyleName('radiocol_metadata_lbl') - artist_lbl = Label("artist:") - artist_lbl.setStyleName('radiocol_metadata_lbl') - album_lbl = Label("album:") - album_lbl.setStyleName('radiocol_metadata_lbl') - self.title = Label("") - self.title.setStyleName('radiocol_metadata') - self.artist = Label("") - self.artist.setStyleName('radiocol_metadata') - self.album = Label("") - self.album.setStyleName('radiocol_metadata') - self.setWidget(0, 0, title_lbl) - self.setWidget(1, 0, artist_lbl) - self.setWidget(2, 0, album_lbl) - self.setWidget(0, 1, self.title) - self.setWidget(1, 1, self.artist) - self.setWidget(2, 1, self.album) - self.setStyleName("radiocol_metadata_pnl") - - def setTitle(self, title): - self.title.setText(title) - - def setArtist(self, artist): - self.artist.setText(artist) - - def setAlbum(self, album): - self.album.setText(album) - - -class ControlPanel(FormPanel): - """Panel used to show controls to add a song, or vote for the current one""" - - def __init__(self, parent): - FormPanel.__init__(self) - self.setEncoding(FormPanel.ENCODING_MULTIPART) - self.setMethod(FormPanel.METHOD_POST) - self.setAction("upload_radiocol") - self.timer_on = False - self._parent = parent - vPanel = VerticalPanel() - - types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'), - ('video/ogg', '*.ogv', 'Ogg Vorbis Video'), - ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'), - ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'), - ('audio/mp3', '*.mp3', 'MPEG-Layer 3'), - ] - self.file_upload = FilterFileUpload("song", 10, types) - vPanel.add(self.file_upload) - - hPanel = HorizontalPanel() - self.upload_btn = Button("Upload song", getattr(self, "onBtnClick")) - hPanel.add(self.upload_btn) - self.status = Label() - self.updateStatus() - hPanel.add(self.status) - #We need to know the filename and the referee - self.filename_field = Hidden('filename', '') - hPanel.add(self.filename_field) - referee_field = Hidden('referee', self._parent.referee) - hPanel.add(self.filename_field) - hPanel.add(referee_field) - vPanel.add(hPanel) - - self.add(vPanel) - self.addFormHandler(self) - - def updateStatus(self): - if self.timer_on: - return - # TODO: the status should be different if a song is being played or not - queue = self._parent.getQueueSize() - queue_data = self._parent.queue_data - if queue < queue_data[0]: - left = queue_data[0] - queue - self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else "")) - elif queue < queue_data[1]: - left = queue_data[1] - queue - self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else "")) - elif queue >= queue_data[1]: - self.status.setText("[The queue is currently full]") - self.status.setStyleName('radiocol_status') - - def onBtnClick(self): - if self.file_upload.check(): - self.status.setText('[Submitting, please wait...]') - self.filename_field.setValue(self.file_upload.getFilename()) - if self.file_upload.getFilename().lower().endswith('.mp3'): - self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000) - self.submit() - self.file_upload.setFilename("") - - def onSubmit(self, event): - pass - - def blockUpload(self): - self.file_upload.setVisible(False) - self.upload_btn.setEnabled(False) - - def unblockUpload(self): - self.file_upload.setVisible(True) - self.upload_btn.setEnabled(True) - - def setTemporaryStatus(self, text, style): - self.status.setText(text) - self.status.setStyleName('radiocol_upload_status_%s' % style) - self.timer_on = True - - def cb(timer): - self.timer_on = False - self.updateStatus() - - Timer(5000, cb) - - def onSubmitComplete(self, event): - result = event.getResults() - if result == "OK": - # the song can still be rejected (not readable, full queue...) - self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok") - elif result == "KO": - self.setTemporaryStatus('[Something went wrong during your song upload]', "ko") - self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted.")) - # TODO: would be great to re-use the original Exception class and message - # but it is lost in the middle of the traceback and encapsulated within - # a DBusException instance --> extract the data from the traceback? - else: - Window.alert('Submit error: %s' % result) - self.status.setText('') - - -class Player(Audio): - - def __init__(self, player_id, metadata_panel): - Audio.__init__(self) - self._id = player_id - self.metadata = metadata_panel - self.timestamp = "" - self.title = "" - self.artist = "" - self.album = "" - self.filename = None - self.played = False # True when the song is playing/has played, becomes False on preload - self.setAutobuffer(True) - self.setAutoplay(False) - self.setVisible(False) - - def preload(self, timestamp, filename, title, artist, album): - """preload the song but doesn't play it""" - self.timestamp = timestamp - self.filename = filename - self.title = title - self.artist = artist - self.album = album - self.played = False - self.setSrc("radiocol/%s" % html_sanitize(filename)) - log.debug("preloading %s in %s" % (title, self._id)) - - def play(self, play=True): - """Play or pause the song - @param play: set to True to play or to False to pause - """ - if play: - self.played = True - self.metadata.setTitle(self.title) - self.metadata.setArtist(self.artist) - self.metadata.setAlbum(self.album) - Audio.play(self) - else: - self.pause() - - -class RadioColPanel(HorizontalPanel, ClickHandler): - - def __init__(self, parent, referee, player_nick, players, queue_data): - """ - @param parent - @param referee - @param player_nick - @param players - @param queue_data: list of integers (queue to start, queue limit) - """ - # We need to set it here and not in the CSS :( - HorizontalPanel.__init__(self, Height="90px") - ClickHandler.__init__(self) - self._parent = parent - self.referee = referee - self.queue_data = queue_data - self.setStyleName("radiocolPanel") - - # Now we set up the layout - self.metadata_panel = MetadataPanel() - self.add(CaptionPanel("Now playing", self.metadata_panel)) - self.playlist_panel = VerticalPanel() - self.add(CaptionPanel("Songs queue", self.playlist_panel)) - self.control_panel = ControlPanel(self) - self.add(CaptionPanel("Controls", self.control_panel)) - - self.next_songs = [] - self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)] - self.current_player = None - for player in self.players: - self.add(player) - self.addClickListener(self) - - help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br /> - Please do not submit files that are protected by copyright.<br /> - Click <a style="color: red;">here</a> if you need some support :)""" - link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick) - self._parent.printInfo(help_msg, type_='link', link_cb=link_cb) - - def pushNextSong(self, title): - """Add a song to the left panel's next songs queue""" - next_song = Label(title) - next_song.setStyleName("radiocol_next_song") - self.next_songs.append(next_song) - self.playlist_panel.append(next_song) - self.control_panel.updateStatus() - - def popNextSong(self): - """Remove the first song of next songs list - should be called when the song is played""" - #FIXME: should check that the song we remove is the one we play - next_song = self.next_songs.pop(0) - self.playlist_panel.remove(next_song) - self.control_panel.updateStatus() - - def getQueueSize(self): - return len(self.playlist_panel.getChildren()) - - def radiocolCheckPreload(self, timestamp): - for player in self.players: - if player.timestamp == timestamp: - return False - return True - - def radiocolPreload(self, timestamp, filename, title, artist, album, sender): - if not self.radiocolCheckPreload(timestamp): - return # song already preloaded - preloaded = False - for player in self.players: - if not player.filename or \ - (player.played and player != self.current_player): - #if player has no file loaded, or it has already played its song - #we use it to preload the next one - player.preload(timestamp, filename, title, artist, album) - preloaded = True - break - if not preloaded: - log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1]) - else: - self.pushNextSong(title) - self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title}) - - def radiocolPlay(self, filename): - found = False - for player in self.players: - if not found and player.filename == filename: - player.play() - self.popNextSong() - self.current_player = player - found = True - else: - player.play(False) # in case the previous player was not sync - if not found: - log.error("Song not found in queue, can't play it. This should not happen") - - def radiocolNoUpload(self): - self.control_panel.blockUpload() - - def radiocolUploadOk(self): - self.control_panel.unblockUpload() - - def radiocolSongRejected(self, reason): - Window.alert("Song rejected: %s" % reason)
--- 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 <goffi@goffi.org> -# Copyright (C) 2011, 2012 Adrien Vigneron <adrienvigneron@mailoo.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -#This page manage subscription and new account creation - -import pyjd # this is dummy in pyjs -from sat.core.i18n import _ - -from pyjamas.ui.SimplePanel import SimplePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TabPanel import TabPanel -from pyjamas.ui.TabBar import TabBar -from pyjamas.ui.PasswordTextBox import PasswordTextBox -from pyjamas.ui.TextBox import TextBox -from pyjamas.ui.FormPanel import FormPanel -from pyjamas.ui.Button import Button -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.PopupPanel import PopupPanel -from pyjamas.ui.Image import Image -from pyjamas.ui.Hidden import Hidden -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KEY_ENTER -from pyjamas.Timer import Timer - -import re - -from constants import Const as C - - -class RegisterPanel(FormPanel): - - def __init__(self, callback): - """ - @param callback: method to call if login successful - """ - FormPanel.__init__(self) - self.setSize('600px', '350px') - self.callback = callback - self.setMethod(FormPanel.METHOD_POST) - main_panel = HorizontalPanel() - main_panel.setStyleName('registerPanel_main') - left_side = Image("media/libervia/register_left.png") - main_panel.add(left_side) - - ##TabPanel## - tab_bar = TabBar() - tab_bar.setStyleName('registerPanel_tabs') - self.right_side = TabPanel(tab_bar) - self.right_side.setStyleName('registerPanel_right_side') - main_panel.add(self.right_side) - main_panel.setCellWidth(self.right_side, '100%') - - ##Login tab## - login_tab = SimplePanel() - login_tab.setStyleName('registerPanel_content') - login_vpanel = VerticalPanel() - login_tab.setWidget(login_vpanel) - - self.login_warning_msg = Label('') - self.login_warning_msg.setVisible(False) - self.login_warning_msg.setStyleName('formWarning') - login_vpanel.add(self.login_warning_msg) - - login_label = Label('Login:') - self.login_box = TextBox() - self.login_box.setName("login") - self.login_box.addKeyboardListener(self) - login_pass_label = Label('Password:') - self.login_pass_box = PasswordTextBox() - self.login_pass_box.setName("login_password") - self.login_pass_box.addKeyboardListener(self) - - login_vpanel.add(login_label) - login_vpanel.add(self.login_box) - login_vpanel.add(login_pass_label) - login_vpanel.add(self.login_pass_box) - login_but = Button("Log in", getattr(self, "onLogin")) - login_but.setStyleName('button') - login_but.addStyleName('red') - login_vpanel.add(login_but) - - #The hidden submit_type field - self.submit_type = Hidden('submit_type') - login_vpanel.add(self.submit_type) - - ##Register tab## - register_tab = SimplePanel() - register_tab.setStyleName('registerPanel_content') - register_vpanel = VerticalPanel() - register_tab.setWidget(register_vpanel) - - self.register_warning_msg = HTML('') - self.register_warning_msg.setVisible(False) - self.register_warning_msg.setStyleName('formWarning') - register_vpanel.add(self.register_warning_msg) - - register_login_label = Label('Login:') - self.register_login_box = TextBox() - self.register_login_box.setName("register_login") - self.register_login_box.addKeyboardListener(self) - email_label = Label('E-mail:') - self.email_box = TextBox() - self.email_box.setName("email") - self.email_box.addKeyboardListener(self) - register_pass_label = Label('Password:') - self.register_pass_box = PasswordTextBox() - self.register_pass_box.setName("register_password") - self.register_pass_box.addKeyboardListener(self) - register_vpanel.add(register_login_label) - register_vpanel.add(self.register_login_box) - register_vpanel.add(email_label) - register_vpanel.add(self.email_box) - register_vpanel.add(register_pass_label) - register_vpanel.add(self.register_pass_box) - - register_but = Button("Register", getattr(self, "onRegister")) - register_but.setStyleName('button') - register_but.addStyleName('red') - register_vpanel.add(register_but) - - self.right_side.add(login_tab, 'Login') - self.right_side.add(register_tab, 'Register') - self.right_side.addTabListener(self) - self.right_side.selectTab(1) - login_tab.setWidth(None) - register_tab.setWidth(None) - - self.add(main_panel) - self.addFormHandler(self) - self.setAction('register_api/login') - - def onBeforeTabSelected(self, sender, tabIndex): - return True - - def onTabSelected(self, sender, tabIndex): - if tabIndex == 0: - self.login_box.setFocus(True) - elif tabIndex == 1: - self.register_login_box.setFocus(True) - - def onKeyPress(self, sender, keycode, modifiers): - if keycode == KEY_ENTER: - # Browsers offer an auto-completion feature to any - # text box, but the selected value is not set when - # the widget looses the focus. Using a timer with - # any delay value > 0 would do the trick. - if sender == self.login_box: - Timer(5, lambda timer: self.login_pass_box.setFocus(True)) - elif sender == self.login_pass_box: - self.onLogin(None) - elif sender == self.register_login_box: - Timer(5, lambda timer: self.email_box.setFocus(True)) - elif sender == self.email_box: - Timer(5, lambda timer: self.register_pass_box.setFocus(True)) - elif sender == self.register_pass_box: - self.onRegister(None) - - def onKeyUp(self, sender, keycode, modifiers): - pass - - def onKeyDown(self, sender, keycode, modifiers): - pass - - def onLogin(self, button): - if not re.match(r'^[a-z0-9_-]+$', self.login_box.getText(), re.IGNORECASE): - self.login_warning_msg.setText('Invalid login, valid characters are a-z A-Z 0-9 _ -') - self.login_warning_msg.setVisible(True) - else: - self.submit_type.setValue('login') - self.submit() - - def onRegister(self, button): - if not re.match(r'^[a-z0-9_-]+$', self.register_login_box.getText(), re.IGNORECASE): - self.register_warning_msg.setHTML(_('Invalid login, valid characters<br>are a-z A-Z 0-9 _ -')) - self.register_warning_msg.setVisible(True) - elif not re.match(r'^.+@.+\..+', self.email_box.getText(), re.IGNORECASE): - self.register_warning_msg.setHTML(_('Invalid email address')) - self.register_warning_msg.setVisible(True) - elif len(self.register_pass_box.getText()) < C.PASSWORD_MIN_LENGTH: - self.register_warning_msg.setHTML(_('Your password must contain<br>at least %d characters') % C.PASSWORD_MIN_LENGTH) - self.register_warning_msg.setVisible(True) - else: - self.register_warning_msg.setVisible(False) - self.submit_type.setValue('register') - self.submit() - - def onSubmit(self, event): - pass - - def onSubmitComplete(self, event): - result = event.getResults() - if result == "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,<br>please choose another one')) - self.register_warning_msg.setVisible(True) - elif result == "INTERNAL": - self.register_warning_msg.setHTML(_('SERVER ERROR: something went wrong during registration process, please contact the server administrator')) - self.register_warning_msg.setVisible(True) - elif result == "REGISTRATION": - self.login_warning_msg.setVisible(False) - self.register_warning_msg.setVisible(False) - self.login_box.setText(self.register_login_box.getText()) - self.login_pass_box.setText('') - self.register_login_box.setText('') - self.register_pass_box.setText('') - self.email_box.setText('') - self.right_side.selectTab(0) - self.login_pass_box.setFocus(True) - Window.alert(_('An email has been sent to you with your login informations\nPlease remember that this is ONLY A TECHNICAL DEMO')) - else: - Window.alert(_('Submit error: %s' % result)) - - -class RegisterBox(PopupPanel): - - def __init__(self, callback, *args, **kwargs): - PopupPanel.__init__(self, *args, **kwargs) - self._form = RegisterPanel(callback) - self.setWidget(self._form) - - def onWindowResized(self, width, height): - super(RegisterBox, self).onWindowResized(width, height) - self.centerBox() - - def show(self): - super(RegisterBox, self).show() - self.centerBox() - self._form.login_box.setFocus(True)
--- a/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 <souliane@mailoo.org> - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.tools import composition -from sat.core.i18n import _ - -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Button import Button -from pyjamas.ui.CheckBox import CheckBox -from pyjamas.ui.DialogBox import DialogBox -from pyjamas.ui.Label import Label -from pyjamas.ui.HTML import HTML -from pyjamas.ui.FlexTable import FlexTable -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas import Window -from pyjamas.ui.KeyboardListener import KeyboardHandler -from __pyjamas__ import doc - -from constants import Const as C -from dialog import ConfirmDialog, InfoDialog -from base_panels import TitlePanel, BaseTextEditor, HTMLTextEditor -from list_manager import ListManager -from html_tools import html_sanitize -import panels - - -class RichTextEditor(BaseTextEditor, FlexTable): - """Panel for the rich text editor.""" - - def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None): - """ - @param host: the SatWebFrontend instance - @param content: dict with at least a 'text' key - @param modifiedCb: method to be called when the text has been modified - @param afterEditCb: method to be called when the edition is done - @param options: list of UI options (see self.readOptions) - """ - self.host = host - self._debug = False # TODO: don't forget to set it False before commit - self.wysiwyg = False - self.__readOptions(options) - self.style = {'main': 'richTextEditor', - 'title': 'richTextTitle', - 'toolbar': 'richTextToolbar', - 'textarea': 'richTextArea'} - if isinstance(style, dict): - self.style.update(style) - self._prepareUI() - BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) - - def __readOptions(self, options): - """Set the internal flags according to the given options.""" - if options is None: - options = [] - self.read_only = 'read_only' in options - self.update_msg = 'update_msg' in options - self.no_title = 'no_title' in options or self.read_only - self.no_command = 'no_command' in options or self.read_only - - def _prepareUI(self, y_offset=0): - """Prepare the UI to host title panel, toolbar, text area... - @param y_offset: Y offset to start from (extra rows on top)""" - if not self.read_only: - self.title_offset = y_offset - self.toolbar_offset = self.title_offset + (0 if self.no_title else 1) - self.content_offset = self.toolbar_offset + (len(composition.RICH_SYNTAXES) if self._debug else 1) - self.command_offset = self.content_offset + 1 - else: - self.title_offset = self.toolbar_offset = self.content_offset = y_offset - self.command_offset = self.content_offset + 1 - FlexTable.__init__(self, self.command_offset + (0 if self.no_command else 1), 2) - self.addStyleName(self.style['main']) - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - BaseTextEditor.addEditListener(self, listener) - if hasattr(self, 'display'): - self.display.addEditListener(listener) - - def refresh(self, edit=None): - """Refresh the UI for edition/display mode - @param edit: set to True to display the edition mode""" - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - - for widget in ['title_panel', 'command']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if hasattr(self, 'toolbar'): - self.toolbar.setVisible(False) - if not hasattr(self, 'display'): - self.display = HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode - for listener in self.edit_listeners: - self.display.addEditListener(listener) - if not self.read_only and not hasattr(self, 'textarea'): - self.textarea = EditTextArea(self) # for edition mode - self.textarea.addStyleName(self.style['textarea']) - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if edit and not self.wysiwyg: - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - self.setWidget(self.content_offset, 0, self.textarea) - else: - self.setWidget(self.content_offset, 0, self.display) - if not edit: - return - - if not self.no_title and not hasattr(self, 'title_panel'): - self.title_panel = TitlePanel() - self.title_panel.addStyleName(self.style['title']) - self.getFlexCellFormatter().setColSpan(self.title_offset, 0, 2) - self.setWidget(self.title_offset, 0, self.title_panel) - - if not self.no_command and not hasattr(self, 'command'): - self.command = HorizontalPanel() - self.command.addStyleName("marginAuto") - self.command.add(Button("Cancel", lambda: self.edit(True, True))) - self.command.add(Button("Update" if self.update_msg else "Send message", lambda: self.edit(False))) - self.getFlexCellFormatter().setColSpan(self.command_offset, 0, 2) - self.setWidget(self.command_offset, 0, self.command) - - def setToolBar(self, syntax): - """This method is called asynchronously after the parameter - holding the rich text syntax is retrieved. It is called at - each call of self.edit(True) because the user may - have change his setting since the last time.""" - if syntax is None or syntax not in composition.RICH_SYNTAXES.keys(): - syntax = composition.RICH_SYNTAXES.keys()[0] - if hasattr(self, "toolbar") and self.toolbar.syntax == syntax: - self.toolbar.setVisible(True) - return - count = 0 - for syntax in composition.RICH_SYNTAXES.keys() if self._debug else [syntax]: - self.toolbar = HorizontalPanel() - self.toolbar.syntax = syntax - self.toolbar.addStyleName(self.style['toolbar']) - for key in composition.RICH_SYNTAXES[syntax].keys(): - self.addToolbarButton(syntax, key) - self.wysiwyg_button = CheckBox(_('WYSIWYG edition')) - wysiywgCb = lambda sender: self.setWysiwyg(sender.getChecked()) - self.wysiwyg_button.addClickListener(wysiywgCb) - self.toolbar.add(self.wysiwyg_button) - self.syntax_label = Label(_("Syntax: %s") % syntax) - self.syntax_label.addStyleName("richTextSyntaxLabel") - self.toolbar.add(self.syntax_label) - self.toolbar.setCellWidth(self.syntax_label, "100%") - self.getFlexCellFormatter().setColSpan(self.toolbar_offset + count, 0, 2) - self.setWidget(self.toolbar_offset + count, 0, self.toolbar) - count += 1 - - def setWysiwyg(self, wysiwyg, init=False): - """Toggle the edition mode between rich content syntax and wysiwyg. - @param wysiwyg: boolean value - @param init: set to True to re-init without switching the widgets.""" - def setWysiwyg(): - self.wysiwyg = wysiwyg - try: - self.wysiwyg_button.setChecked(wysiwyg) - except TypeError: - pass - try: - if wysiwyg: - self.syntax_label.addStyleName('transparent') - else: - self.syntax_label.removeStyleName('transparent') - except TypeError: - pass - if not wysiwyg: - self.display.removeStyleName('richTextWysiwyg') - - if init: - setWysiwyg() - return - - self.getFlexCellFormatter().setColSpan(self.content_offset, 0, 2) - if wysiwyg: - def syntaxConvertCb(text): - self.display.setContent({'text': text}) - self.textarea.removeFromParent() # XXX: force as it is not always done... - self.setWidget(self.content_offset, 0, self.display) - self.display.addStyleName('richTextWysiwyg') - self.display.edit(True) - content = self.getContent() - if content['text'] and content['syntax'] != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax'], C.SYNTAX_XHTML) - else: - syntaxConvertCb(content['text']) - else: - syntaxConvertCb = lambda text: self.textarea.setText(text) - text = self.display.getContent()['text'] - if text and self.toolbar.syntax != C.SYNTAX_XHTML: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, text) - else: - syntaxConvertCb(text) - self.setWidget(self.content_offset, 0, self.textarea) - self.textarea.setWidth('100%') # CSS width doesn't do it, don't know why - - setWysiwyg() # do it in the end because it affects self.getContent - - def addToolbarButton(self, syntax, key): - """Add a button with the defined parameters.""" - button = Button('<img src="%s" class="richTextIcon" />' % - composition.RICH_BUTTONS[key]["icon"]) - button.setTitle(composition.RICH_BUTTONS[key]["tip"]) - button.addStyleName('richTextToolButton') - self.toolbar.add(button) - - def buttonCb(): - """Generic callback for a toolbar button.""" - text = self.textarea.getText() - cursor_pos = self.textarea.getCursorPos() - selection_length = self.textarea.getSelectionLength() - data = composition.RICH_SYNTAXES[syntax][key] - if selection_length == 0: - middle_text = data[1] - else: - middle_text = text[cursor_pos:cursor_pos + selection_length] - self.textarea.setText(text[:cursor_pos] - + data[0] - + middle_text - + data[2] - + text[cursor_pos + selection_length:]) - self.textarea.setCursorPos(cursor_pos + len(data[0]) + len(middle_text)) - self.textarea.setFocus(True) - self.textarea.onKeyDown() - - def wysiwygCb(): - """Callback for a toolbar button while wysiwyg mode is enabled.""" - data = composition.COMMANDS[key] - - def execCommand(command, arg): - self.display.setFocus(True) - doc().execCommand(command, False, arg.strip() if arg else '') - # use Window.prompt instead of dialog.PromptDialog to not loose the focus - prompt = lambda command, text: execCommand(command, Window.prompt(text)) - if isinstance(data, tuple) or isinstance(data, list): - if data[1]: - prompt(data[0], data[1]) - else: - execCommand(data[0], data[2]) - else: - execCommand(data, False, '') - self.textarea.onKeyDown() - - button.addClickListener(lambda: wysiwygCb() if self.wysiwyg else buttonCb()) - - def getContent(self): - assert(hasattr(self, 'textarea')) - assert(hasattr(self, 'toolbar')) - if self.wysiwyg: - content = {'text': self.display.getContent()['text'], 'syntax': C.SYNTAX_XHTML} - else: - content = {'text': self.strproc(self.textarea.getText()), 'syntax': self.toolbar.syntax} - if hasattr(self, 'title_panel'): - content.update({'title': self.strproc(self.title_panel.getText())}) - return content - - def edit(self, edit=False, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if not (edit and abort): - self.refresh(edit) # not when we are asking for a confirmation - BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed - if (edit and abort): - return # self.abortEdition is called by BaseTextEditor.edit - self.setWysiwyg(False, init=True) # after BaseTextEditor (it affects self.getContent) - if sync: - return - # the following must NOT be done at each UI refresh! - content = self._original_content - if edit: - def getParamCb(syntax): - # set the editable text in the current user-selected syntax - def syntaxConvertCb(text=None): - if text is not None: - # Important: this also update self._original_content - content.update({'text': text}) - content.update({'syntax': syntax}) - self.textarea.setText(content['text']) - if hasattr(self, 'title_panel') and 'title' in content: - self.title_panel.setText(content['title']) - self.title_panel.setStackVisible(0, content['title'] != '') - self.setToolBar(syntax) - if content['text'] and content['syntax'] != syntax: - self.host.bridge.call('syntaxConvert', syntaxConvertCb, content['text'], content['syntax']) - else: - syntaxConvertCb() - self.host.bridge.call('asyncGetParamA', getParamCb, composition.PARAM_NAME_SYNTAX, composition.PARAM_KEY_COMPOSITION) - else: - if not self.initialized: - # set the display text in XHTML only during init because a new MicroblogEntry instance is created after each modification - self.setDisplayContent() - self.display.edit(False) - - def setDisplayContent(self): - """Set the content of the HTMLTextEditor which is used for display/wysiwyg""" - content = self._original_content - text = content['text'] - if 'title' in content and content['title']: - text = '<h1>%s</h1>%s' % (html_sanitize(content['title']), content['text']) - self.display.setContent({'text': text}) - - def setFocus(self, focus): - self.textarea.setFocus(focus) - - def abortEdition(self, content): - """Ask for confirmation before closing the dialog.""" - def confirm_cb(answer): - if answer: - self.edit(False, True) - _dialog = ConfirmDialog(confirm_cb, text="Do you really want to %s?" % ("cancel your changes" if self.update_msg else "cancel this message")) - _dialog.cancel_button.setText(_("No")) - _dialog.show() - - -class RichMessageEditor(RichTextEditor): - """Use the rich text editor for sending messages with extended addressing. - Recipient panels are on top and data may be synchronized from/to the unibox.""" - - @classmethod - def getOrCreate(cls, host, parent=None, callback=None): - """Get or create the message editor associated to that host. - Add it to parent if parent is not None, otherwise display it - in a popup dialog. - @param host: the host - @param parent: parent panel (or None to display in a popup). - @return: the RichTextEditor instance if parent is not None, - otherwise a popup DialogBox containing the RichTextEditor. - """ - if not hasattr(host, 'richtext'): - modifiedCb = lambda content: True - - def afterEditCb(content): - if hasattr(host.richtext, 'popup'): - host.richtext.popup.hide() - else: - host.richtext.setVisible(False) - callback() - options = ['no_title'] - style = {'main': 'richMessageEditor', 'textarea': 'richMessageArea'} - host.richtext = RichMessageEditor(host, None, modifiedCb, afterEditCb, options, style) - - def add(widget, parent): - if widget.getParent() is not None: - if widget.getParent() != parent: - widget.removeFromParent() - parent.add(widget) - else: - parent.add(widget) - widget.setVisible(True) - widget.initialized = False # fake a new creation - widget.edit(True) - - if parent is None: - if not hasattr(host.richtext, 'popup'): - host.richtext.popup = DialogBox(autoHide=False, centered=True) - host.richtext.popup.setHTML("Compose your message") - host.richtext.popup.add(host.richtext) - add(host.richtext, host.richtext.popup) - host.richtext.popup.center() - else: - add(host.richtext, parent) - return host.richtext.popup if parent is None else host.richtext - - def _prepareUI(self, y_offset=0): - """Prepare the UI to host recipients panel, toolbar, text area... - @param y_offset: Y offset to start from (extra rows on top)""" - self.recipient_offset = y_offset - self.recipient_spacer_offset = self.recipient_offset + len(composition.RECIPIENT_TYPES) - RichTextEditor._prepareUI(self, self.recipient_spacer_offset + 1) - - def refresh(self, edit=None): - """Refresh the UI between edition/display mode - @param edit: set to True to display the edition mode""" - if edit is None: - edit = hasattr(self, 'textarea') and self.textarea.getVisible() - RichTextEditor.refresh(self, edit) - - for widget in ['recipient', 'recipient_spacer']: - if hasattr(self, widget): - getattr(self, widget).setVisible(edit) - - if not edit: - return - - if not hasattr(self, 'recipient'): - # recipient types sub-panels are automatically added by the manager - self.recipient = RecipientManager(self, self.recipient_offset) - self.recipient.createWidgets(title_format="%s: ") - self.recipient_spacer = HTML('') - self.recipient_spacer.setStyleName('recipientSpacer') - self.getFlexCellFormatter().setColSpan(self.recipient_spacer_offset, 0, 2) - self.setWidget(self.recipient_spacer_offset, 0, self.recipient_spacer) - - if not hasattr(self, 'sync_button'): - self.sync_button = Button("Back to quick box", lambda: self.edit(True, sync=True)) - self.command.insert(self.sync_button, 1) - - def syncToEditor(self): - """Synchronize from unibox.""" - def setContent(target, data): - if hasattr(self, 'recipient'): - self.recipient.setContacts({"To": [target]} if target else {}) - self.setContent({'text': data if data else '', 'syntax': ''}) - self.textarea.setText(data if data else '') - data, target = self.host.uni_box.getTargetAndData() if self.host.uni_box else (None, None) - setContent(target, data) - - def __syncToUniBox(self, recipients=None, emptyText=False): - """Synchronize to unibox if a maximum of one recipient is set. - @return True if the sync could be done, False otherwise""" - if not self.host.uni_box: - return - setText = lambda: self.host.uni_box.setText("" if emptyText else self.getContent()['text']) - if not hasattr(self, 'recipient'): - setText() - return True - if recipients is None: - recipients = self.recipient.getContacts() - target = "" - # we could eventually allow more in the future - allowed = 1 - for key in recipients: - count = len(recipients[key]) - if count == 0: - continue - allowed -= count - if allowed < 0: - return False - # TODO: change this if later more then one recipients are allowed - target = recipients[key][0] - setText() - if target == "": - return True - if target.startswith("@"): - _class = panels.MicroblogPanel - target = None if target == "@@" else target[1:] - else: - _class = panels.ChatPanel - self.host.getOrCreateLiberviaWidget(_class, target) - return True - - def syncFromEditor(self, content): - """Synchronize to unibox and close the dialog afterward. Display - a message and leave the dialog open if the sync was not possible.""" - if self.__syncToUniBox(): - self._afterEditCb(content) - return - InfoDialog("Too many recipients", - "A message with more than one direct recipient (To)," + - " or with any special recipient (Cc or Bcc), could not be" + - " stored in the quick box.\n\nPlease finish your composing" + - " in the rich text editor, and send your message directly" + - " from here.", Width="400px").center() - - def edit(self, edit=True, abort=False, sync=False): - if not edit and not abort and not sync: # force sending message even when the text has not been modified - if not self.__sendMessage(): # message has not been sent (missing information), do nothing - return - RichTextEditor.edit(self, edit, abort, sync) - - def __sendMessage(self): - """Send the message.""" - recipients = self.recipient.getContacts() - targets = [] - for addr in recipients: - for recipient in recipients[addr]: - if recipient.startswith("@"): - targets.append(("PUBLIC", None, addr) if recipient == "@@" else ("GROUP", recipient[1:], addr)) - else: - targets.append(("chat", recipient, addr)) - # check that we actually have a message target and data - content = self.getContent() - if content['text'] == "" or len(targets) == 0: - InfoDialog("Missing information", - "Some information are missing and the message hasn't been sent.", Width="400px").center() - return None - self.__syncToUniBox(recipients, emptyText=True) - extra = {'content_rich': content['text']} - if hasattr(self, 'title_panel'): - extra.update({'title': content['title']}) - self.host.send(targets, content['text'], extra=extra) - return True - - -class RecipientManager(ListManager): - """A manager for sub-panels to set the recipients for each recipient type.""" - - def __init__(self, parent, y_offset=0): - # TODO: be sure we also display empty groups and disconnected contacts + their groups - # store the full list of potential recipients (groups and contacts) - list_ = [] - list_.append("@@") - list_.extend("@%s" % group for group in parent.host.contact_panel.getGroups()) - list_.extend(contact for contact in parent.host.contact_panel.getContacts()) - ListManager.__init__(self, parent, composition.RECIPIENT_TYPES, list_, {'y': y_offset}) - - self.registerPopupMenuPanel(entries=composition.RECIPIENT_TYPES, - hide=lambda sender, key: self.__children[key]["panel"].isVisible(), - callback=self.setContactPanelVisible) - - -class EditTextArea(TextArea, KeyboardHandler): - def __init__(self, _parent): - TextArea.__init__(self) - self._parent = _parent - KeyboardHandler.__init__(self) - self.addKeyboardListener(self) - - def onKeyDown(self, sender=None, keycode=None, modifiers=None): - for listener in self._parent.edit_listeners: - listener(self, keycode)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/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 <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ +from sat_frontends.tools 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, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" % + {"timestamp": _date.strftime("%H:%M"), + "nick": "[%s]" % html_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("<div class='occupant'>%s%s%s</div>" % (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 <enter> or <escape>. + - listen_focus: set to True to terminate the edition when the focus is lost. + - listen_click: set to True to start the edition when you click on the widget. + """ + self.options = {'no_xhtml': False, + 'enhance_display': True, + 'listen_keyboard': True, + 'listen_focus': False, + 'listen_click': False + } + if options: + self.options.update(options) + self.__shift_down = False + if self.options['listen_focus']: + FocusHandler.__init__(self) + if self.options['listen_click']: + ClickHandler.__init__(self) + KeyboardHandler.__init__(self) + strproc = lambda text: html_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 <shift> + <enter> + self.__shift_down = True + return + if keycode == KEY_ENTER: # finish the edition + self.textarea.setFocus(False) + if not self.options['listen_focus']: + self.edit(False) + + def onKeyUp(self, sender, keycode, modifiers): + if keycode == KEY_SHIFT: + self.__shift_down = False + + def onLostFocus(self, sender): + """Finish the edition when focus is lost""" + if self.options['listen_focus']: + self.edit(False) + + def onClick(self, sender=None): + """Start the edition when the widget is clicked""" + if self.options['listen_click']: + self.edit(True) + + def onBrowserEvent(self, event): + if self.options['listen_focus']: + FocusHandler.onBrowserEvent(self, event) + if self.options['listen_click']: + ClickHandler.onBrowserEvent(self, event) + KeyboardHandler.onBrowserEvent(self, event) + + +class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): + """Manage a simple text editor with the HTML 5 "contenteditable" property.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + HTML.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = self.display = self + + def getContent(self): + text = DOM.getInnerHTML(self.getElement()) + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setHTML(self._original_content['text']) + self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.getElement().focus() + else: + self.getElement().blur() + + +class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): + """Manage a simple text editor with a TextArea for editing, HTML for display.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + SimplePanel.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = TextArea() + self.display = HTML() + + def getContent(self): + text = self.textarea.getText() + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setText(self._original_content['text']) + self.setWidget(self.textarea if edit else self.display) + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.textarea.setCursorPos(len(self.textarea.getText())) + self.textarea.setFocus(focus)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/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 <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.TabPanel import TabPanel +from pyjamas.ui.HTMLPanel import HTMLPanel +from pyjamas.ui.Label import Label +from pyjamas.ui.Button import Button +from pyjamas.ui.Image import Image +from pyjamas.ui.Widget import Widget +from pyjamas.ui.DragWidget import DragWidget +from pyjamas.ui.DropWidget import DropWidget +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui import HasAlignment +from pyjamas import DOM +from pyjamas import Window +from __pyjamas__ import doc + +import dialog + + +class DragLabel(DragWidget): + + def __init__(self, text, _type): + DragWidget.__init__(self) + self._text = text + self._type = _type + + def onDragStart(self, event): + dt = event.dataTransfer + dt.setData('text/plain', "%s\n%s" % (self._text, self._type)) + dt.setDragImage(self.getElement(), 15, 15) + + +class LiberviaDragWidget(DragLabel): + """ A DragLabel which keep the widget being dragged as class value """ + current = None # widget currently dragged + + def __init__(self, text, _type, widget): + DragLabel.__init__(self, text, _type) + self.widget = widget + + def onDragStart(self, event): + LiberviaDragWidget.current = self.widget + DragLabel.onDragStart(self, event) + + def onDragEnd(self, event): + LiberviaDragWidget.current = None + + +class DropCell(DropWidget): + """Cell in the middle grid which replace itself with the dropped widget on DnD""" + drop_keys = {} + + def __init__(self, host): + DropWidget.__init__(self) + self.host = host + self.setStyleName('dropCell') + + @classmethod + def addDropKey(cls, key, callback): + DropCell.drop_keys[key] = callback + + def onDragEnter(self, event): + if self == LiberviaDragWidget.current: + return + self.addStyleName('dragover') + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop() or\ + event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: + # We check that we are inside widget's box, and we don't remove the style in this case because + # if the mouse is over a widget inside the DropWidget, if will leave the DropWidget, and we + # don't want that + self.removeStyleName('dragover') + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def _getCellAndRow(self, grid, event): + """Return cell and row index where the event is occuring""" + cell = grid.getEventTargetCell(event) + row = DOM.getParent(cell) + return (row.rowIndex, cell.cellIndex) + + def onDrop(self, event): + self.removeStyleName('dragover') + DOM.eventPreventDefault(event) + dt = event.dataTransfer + # 'text', 'text/plain', and 'Text' are equivalent. + try: + item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed + if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and + item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report + # item_type = dt.getData("type") + log.debug("message: %s" % item) + log.debug("type: %s" % item_type) + except: + log.debug("no message found") + item = ' ' + item_type = None + if item_type == "WIDGET": + if not LiberviaDragWidget.current: + log.error("No widget registered in LiberviaDragWidget !") + return + _new_panel = LiberviaDragWidget.current + if self == _new_panel: # We can't drop on ourself + return + # we need to remove the widget from the panel as it will be inserted elsewhere + widgets_panel = _new_panel.getWidgetsPanel() + wid_row = widgets_panel.getWidgetCoords(_new_panel)[0] + row_wids = widgets_panel.getLiberviaRowWidgets(wid_row) + if len(row_wids) == 1 and wid_row == widgets_panel.getWidgetCoords(self)[0]: + # the dropped widget is the only one in the same row + # as the target widget (self), we don't do anything + return + widgets_panel.removeWidget(_new_panel) + elif item_type in self.drop_keys: + _new_panel = self.drop_keys[item_type](self.host, item) + else: + log.warning("unmanaged item type") + return + if isinstance(self, LiberviaWidget): + self.host.unregisterWidget(self) + self.onQuit() + if not isinstance(_new_panel, LiberviaWidget): + log.warning("droping an object which is not a class of LiberviaWidget") + _flextable = self.getParent() + _widgetspanel = _flextable.getParent().getParent() + row_idx, cell_idx = self._getCellAndRow(_flextable, event) + if self.host.getSelected == self: + self.host.setSelected(None) + _widgetspanel.changeWidget(row_idx, cell_idx, _new_panel) + """_unempty_panels = filter(lambda wid:not isinstance(wid,EmptyWidget),list(_flextable)) + _width = 90/float(len(_unempty_panels) or 1) + #now we resize all the cell of the column + for panel in _unempty_panels: + td_elt = panel.getElement().parentNode + DOM.setStyleAttribute(td_elt, "width", "%s%%" % _width)""" + #FIXME: delete object ? Check the right way with pyjamas + + +class WidgetHeader(AbsolutePanel, LiberviaDragWidget): + + def __init__(self, parent, title): + AbsolutePanel.__init__(self) + self.add(title) + button_group_wrapper = SimplePanel() + button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') + button_group = HorizontalPanel() + button_group.setStyleName('widgetHeader_buttonGroup') + setting_button = Image("media/icons/misc/settings.png") + setting_button.setStyleName('widgetHeader_settingButton') + setting_button.addClickListener(parent.onSetting) + close_button = Image("media/icons/misc/close.png") + close_button.setStyleName('widgetHeader_closeButton') + close_button.addClickListener(parent.onClose) + button_group.add(setting_button) + button_group.add(close_button) + button_group_wrapper.setWidget(button_group) + self.add(button_group_wrapper) + self.addStyleName('widgetHeader') + LiberviaDragWidget.__init__(self, "", "WIDGET", parent) + + +class LiberviaWidget(DropCell, VerticalPanel, ClickHandler): + """Libervia's widget which can replace itself with a dropped widget on DnD""" + + def __init__(self, host, title='', selectable=False): + """Init the widget + @param host: SatWebFrontend object + @param title: title show in the header of the widget + @param selectable: True is widget can be selected by user""" + VerticalPanel.__init__(self) + DropCell.__init__(self, host) + ClickHandler.__init__(self) + self.__selectable = selectable + self.__title_id = HTMLPanel.createUniqueId() + self.__setting_button_id = HTMLPanel.createUniqueId() + self.__close_button_id = HTMLPanel.createUniqueId() + self.__title = Label(title) + self.__title.setStyleName('widgetHeader_title') + self._close_listeners = [] + header = WidgetHeader(self, self.__title) + self.add(header) + self.setSize('100%', '100%') + self.addStyleName('widget') + if self.__selectable: + self.addClickListener(self) + + def onClose(sender): + """Check dynamically if the unibox is enable or not""" + if self.host.uni_box: + self.host.uni_box.onWidgetClosed(sender) + + self.addCloseListener(onClose) + self.host.registerWidget(self) + + def getDebugName(self): + return "%s (%s)" % (self, self.__title.getText()) + + def getWidgetsPanel(self, 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)
--- /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 <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools.games import TarotCard +from sat.core.i18n import _ + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.DockPanel import DockPanel +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.Image import Image +from pyjamas.ui.Label import Label +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.ui import HasAlignment +from pyjamas import Window +from pyjamas import DOM + +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 <b>win</b> !" + else: + title = "You <b>loose</b> :(" + 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()
--- /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 <http://www.gnu.org/licenses/>. + +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 + }, + }
--- /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 <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas import Window +from pyjamas import DOM +from __pyjamas__ import doc + +from jid import JID + +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 = "<b>(*)</b> " + 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':