Mercurial > libervia-web
diff src/browser/base_panels.py @ 449:981ed669d3b3
/!\ reorganize all the file hierarchy, move the code and launching script to src:
- browser_side --> src/browser
- public --> src/browser_side/public
- libervia.py --> src/browser/libervia_main.py
- libervia_server --> src/server
- libervia_server/libervia.sh --> src/libervia.sh
- twisted --> src/twisted
- new module src/common
- split constants.py in 3 files:
- src/common/constants.py
- src/browser/constants.py
- src/server/constants.py
- output --> html (generated by pyjsbuild during the installation)
- new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css
- setup.py installs libervia to the following paths:
- src/common --> <LIB>/libervia/common
- src/server --> <LIB>/libervia/server
- src/twisted --> <LIB>/twisted
- html --> <SHARE>/libervia/html
- server_side --> <SHARE>libervia/server_side
- LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation:
- clean: remove previous installation directories
- purge: remove building and previous installation directories
You may need to update your sat.conf and/or launching script to update the following options/parameters:
- ssl_certificate
- data_dir
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 20 May 2014 06:41:16 +0200 |
parents | browser_side/base_panels.py@d52f529a6d42 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/base_panels.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,609 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org> + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ +from sat_frontends.tools.strings import addURLToText, addURLToImage + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.HTMLPanel import HTMLPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.PopupPanel import PopupPanel +from pyjamas.ui.StackPanel import StackPanel +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT +from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KeyboardHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas import DOM + +from datetime import datetime +from time import time + +from html_tools import html_sanitize, html_strip, inlineRoot, convertNewLinesToXHTML +from constants import Const as C + + +class ChatText(HTMLPanel): + + def __init__(self, timestamp, nick, mymess, msg, xhtml=None): + _date = datetime.fromtimestamp(float(timestamp or time())) + _msg_class = ["chat_text_msg"] + if mymess: + _msg_class.append("chat_text_mymess") + HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" % + {"timestamp": _date.strftime("%H:%M"), + "nick": "[%s]" % html_sanitize(nick), + "msg_class": ' '.join(_msg_class), + "msg": addURLToText(html_sanitize(msg)) if not xhtml else inlineRoot(xhtml)} # FIXME: images and external links must be removed according to preferences + ) + self.setStyleName('chatText') + + +class Occupant(HTML): + """Occupant of a MUC room""" + + def __init__(self, nick, state=None, special=""): + """ + @param nick: the user nickname + @param state: the user chate state (XEP-0085) + @param special: a string of symbols (e.g: for activities) + """ + HTML.__init__(self) + self.nick = nick + self._state = state + self.special = special + self._refresh() + + def __str__(self): + return self.nick + + def setState(self, state): + self._state = state + self._refresh() + + def addSpecial(self, special): + """@param special: unicode""" + if special not in self.special: + self.special += special + self._refresh() + + def removeSpecials(self, special): + """@param special: unicode or list""" + if not isinstance(special, list): + special = [special] + for symbol in special: + self.special = self.special.replace(symbol, "") + self._refresh() + + def _refresh(self): + state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' + special = "" if len(self.special) == 0 else " %s" % self.special + self.setHTML("<div class='occupant'>%s%s%s</div>" % (html_sanitize(self.nick), special, state)) + + +class OccupantsList(AbsolutePanel): + """Panel user to show occupants of a room""" + + def __init__(self): + AbsolutePanel.__init__(self) + self.occupants_list = {} + self.setStyleName('occupantsList') + + def addOccupant(self, nick): + _occupant = Occupant(nick) + self.occupants_list[nick] = _occupant + self.add(_occupant) + + def removeOccupant(self, nick): + try: + self.remove(self.occupants_list[nick]) + except KeyError: + log.error("trying to remove an unexisting nick") + + def clear(self): + self.occupants_list.clear() + AbsolutePanel.clear(self) + + def updateSpecials(self, occupants=[], html=""): + """Set the specified html "symbol" to the listed occupants, + and eventually remove it from the others (if they got it). + This is used for example to visualize who is playing a game. + @param occupants: list of the occupants that need the symbol + @param html: unicode symbol (actually one character or more) + or a list to assign different symbols of the same family. + """ + index = 0 + special = html + for occupant in self.occupants_list.keys(): + if occupant in occupants: + if isinstance(html, list): + special = html[index] + index = (index + 1) % len(html) + self.occupants_list[occupant].addSpecial(special) + else: + self.occupants_list[occupant].removeSpecials(html) + + +class PopupMenuPanel(PopupPanel): + """This implementation of a popup menu (context menu) allow you to assign + two special methods which are common to all the items, in order to hide + certain items and also easily define their callbacks. The menu can be + bound to any of the mouse button (left, middle, right). + """ + def __init__(self, entries, hide=None, callback=None, vertical=True, style=None, **kwargs): + """ + @param entries: a dict of dicts, where each sub-dict is representing + one menu item: the sub-dict key can be used as the item text and + description, but optional "title" and "desc" entries would be used + if they exists. The sub-dicts may be extended later to do + more complicated stuff or overwrite the common methods. + @param hide: function with 2 args: widget, key as string and + returns True if that item should be hidden from the context menu. + @param callback: function with 2 args: sender, key as string + @param vertical: True or False, to set the direction + @param item_style: alternative CSS class for the menu items + @param menu_style: supplementary CSS class for the sender widget + """ + PopupPanel.__init__(self, autoHide=True, **kwargs) + self._entries = entries + self._hide = hide + self._callback = callback + self.vertical = vertical + self.style = {"selected": None, "menu": "recipientTypeMenu", "item": "popupMenuItem"} + if isinstance(style, dict): + self.style.update(style) + self._senders = {} + + def _show(self, sender): + """Popup the menu relative to this sender's position. + @param sender: the widget that has been clicked + """ + menu = VerticalPanel() if self.vertical is True else HorizontalPanel() + menu.setStyleName(self.style["menu"]) + + def button_cb(item): + """You can not put that method in the loop and rely + on _key, because it is overwritten by each step. + You can rely on item.key instead, which is copied + from _key after the item creation. + @param item: the menu item that has been clicked + """ + if self._callback is not None: + self._callback(sender=sender, key=item.key) + self.hide(autoClosed=True) + + for _key in self._entries.keys(): + entry = self._entries[_key] + if self._hide is not None and self._hide(sender=sender, key=_key) is True: + continue + title = entry["title"] if "title" in entry.keys() else _key + item = Button(title, button_cb) + item.key = _key + item.setStyleName(self.style["item"]) + item.setTitle(entry["desc"] if "desc" in entry.keys() else title) + menu.add(item) + if len(menu.getChildren()) == 0: + return + self.add(menu) + if self.vertical is True: + x = sender.getAbsoluteLeft() + sender.getOffsetWidth() + y = sender.getAbsoluteTop() + else: + x = sender.getAbsoluteLeft() + y = sender.getAbsoluteTop() + sender.getOffsetHeight() + self.setPopupPosition(x, y) + self.show() + if self.style["selected"]: + sender.addStyleDependentName(self.style["selected"]) + + def _onHide(popup): + if self.style["selected"]: + sender.removeStyleDependentName(self.style["selected"]) + return PopupPanel.onHideImpl(self, popup) + + self.onHideImpl = _onHide + + def registerClickSender(self, sender, button=BUTTON_LEFT): + """Bind the menu to the specified sender. + @param sender: the widget to which the menu should be bound + @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT + """ + self._senders.setdefault(sender, []) + self._senders[sender].append(button) + + if button == BUTTON_RIGHT: + # WARNING: to disable the context menu is a bit tricky... + # The following seems to work on Firefox 24.0, but: + # TODO: find a cleaner way to disable the context menu + sender.getElement().setAttribute("oncontextmenu", "return false") + + def _onBrowserEvent(event): + button = DOM.eventGetButton(event) + if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: + self._show(sender) + return sender.__class__.onBrowserEvent(sender, event) + + sender.onBrowserEvent = _onBrowserEvent + + def registerMiddleClickSender(self, sender): + self.registerClickSender(sender, BUTTON_MIDDLE) + + def registerRightClickSender(self, sender): + self.registerClickSender(sender, BUTTON_RIGHT) + + +class ToggleStackPanel(StackPanel): + """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be + visible at the same time, clicking a sub-panel header will not display it and hide + the others but only toggle its own visibility. The argument 'visibleStack' is ignored. + Note that the argument 'visible' has been added to listener's 'onStackChanged' method. + """ + + def __init__(self, **kwargs): + StackPanel.__init__(self, **kwargs) + + def onBrowserEvent(self, event): + if DOM.eventGetType(event) == "click": + index = self.getDividerIndex(DOM.eventGetTarget(event)) + if index != -1: + self.toggleStack(index) + + def add(self, widget, stackText="", asHTML=False, visible=False): + StackPanel.add(self, widget, stackText, asHTML) + self.setStackVisible(self.getWidgetCount() - 1, visible) + + def toggleStack(self, index): + if index >= self.getWidgetCount(): + return + visible = not self.getWidget(index).getVisible() + self.setStackVisible(index, visible) + for listener in self.stackListeners: + listener.onStackChanged(self, index, visible) + + +class TitlePanel(ToggleStackPanel): + """A toggle panel to set the message title""" + def __init__(self): + ToggleStackPanel.__init__(self, Width="100%") + self.text_area = TextArea() + self.add(self.text_area, _("Title")) + self.addStackChangeListener(self) + + def onStackChanged(self, sender, index, visible=None): + if visible is None: + visible = sender.getWidget(index).getVisible() + text = self.text_area.getText() + suffix = "" if (visible or not text) else (": %s" % text) + sender.setStackText(index, _("Title") + suffix) + + def getText(self): + return self.text_area.getText() + + def setText(self, text): + self.text_area.setText(text) + + +class BaseTextEditor(object): + """Basic definition of a text editor. The method edit gets a boolean parameter which + should be set to True when you want to edit the text and False to only display it.""" + + def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): + """ + Remark when inheriting this class: since the setContent method could be + overwritten by the child class, you should consider calling this __init__ + after all the parameters affecting this setContent method have been set. + @param content: dict with at least a 'text' key + @param strproc: method to be applied on strings to clean the content + @param modifiedCb: method to be called when the text has been modified. + If this method returns: + - True: the modification will be saved and afterEditCb called; + - False: the modification won't be saved and afterEditCb called; + - None: the modification won't be saved and afterEditCb not called. + @param afterEditCb: method to be called when the edition is done + """ + if content is None: + content = {'text': ''} + assert('text' in content) + if strproc is None: + def strproc(text): + try: + return text.strip() + except (TypeError, AttributeError): + return text + self.strproc = strproc + self.__modifiedCb = modifiedCb + self._afterEditCb = afterEditCb + self.initialized = False + self.edit_listeners = [] + self.setContent(content) + + def setContent(self, content=None): + """Set the editable content. The displayed content, which is set from the child class, could differ. + @param content: dict with at least a 'text' key + """ + if content is None: + content = {'text': ''} + elif not isinstance(content, dict): + content = {'text': content} + assert('text' in content) + self._original_content = {} + for key in content: + self._original_content[key] = self.strproc(content[key]) + + def getContent(self): + """Get the current edited or editable content. + @return: dict with at least a 'text' key + """ + raise NotImplementedError + + def setOriginalContent(self, content): + """Use this method with care! Content initialization should normally be + done with self.setContent. This method exists to let you trick the editor, + e.g. for self.modified to return True also when nothing has been modified. + @param content: dict + """ + self._original_content = content + + def getOriginalContent(self): + """ + @return the original content before modification (dict) + """ + return self._original_content + + def modified(self, content=None): + """Check if the content has been modified. + Remark: we don't use the direct comparison because we want to ignore empty elements + @content: content to be check against the original content or None to use the current content + @return: True if the content has been modified. + """ + if content is None: + content = self.getContent() + # the following method returns True if one non empty element exists in a but not in b + diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] + # the following method returns True if the values for the common keys are not equals + diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] + # finally the combination of both to return True if a difference is found + diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) + + return diff(content, self._original_content) + + def edit(self, edit, abort=False, sync=False): + """ + Remark: the editor must be visible before you call this method. + @param edit: set to True to edit the content or False to only display it + @param abort: set to True to cancel the edition and loose the changes. + If edit and abort are both True, self.abortEdition can be used to ask for a + confirmation. When edit is False and abort is True, abortion is actually done. + @param sync: set to True to cancel the edition after the content has been saved somewhere else + """ + if edit: + if not self.initialized: + self.syncToEditor() # e.g.: use the selected target and unibox content + self.setFocus(True) + if abort: + content = self.getContent() + if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation + self.edit(False, True, sync) + return + if sync: + self.syncFromEditor(content) # e.g.: save the content to unibox + return + else: + if not self.initialized: + return + content = self.getContent() + if abort: + self._afterEditCb(content) + return + if self.__modifiedCb and self.modified(content): + result = self.__modifiedCb(content) # e.g.: send a message or update something + if result is not None: + if self._afterEditCb: + self._afterEditCb(content) # e.g.: restore the display mode + if result is True: + self.setContent(content) + elif self._afterEditCb: + self._afterEditCb(content) + + self.initialized = True + + def setFocus(self, focus): + """ + @param focus: set to True to focus the editor + """ + raise NotImplementedError + + def syncToEditor(self): + pass + + def syncFromEditor(self, content): + pass + + def abortEdition(self, content): + return True + + def addEditListener(self, listener): + """Add a method to be called whenever the text is edited. + @param listener: method taking two arguments: sender, keycode""" + self.edit_listeners.append(listener) + + +class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): + """Base class for manage a simple text editor.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + """ + @param content + @param modifiedCb + @param afterEditCb + @param options: dict with the following value: + - no_xhtml: set to True to clean any xhtml content. + - enhance_display: if True, the display text will be enhanced with addURLToText + - listen_keyboard: set to True to terminate the edition with <enter> or <escape>. + - listen_focus: set to True to terminate the edition when the focus is lost. + - listen_click: set to True to start the edition when you click on the widget. + """ + self.options = {'no_xhtml': False, + 'enhance_display': True, + 'listen_keyboard': True, + 'listen_focus': False, + 'listen_click': False + } + if options: + self.options.update(options) + self.__shift_down = False + if self.options['listen_focus']: + FocusHandler.__init__(self) + if self.options['listen_click']: + ClickHandler.__init__(self) + KeyboardHandler.__init__(self) + strproc = lambda text: html_sanitize(html_strip(text)) if self.options['no_xhtml'] else html_strip(text) + BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) + self.textarea = self.display = None + + def setContent(self, content=None): + BaseTextEditor.setContent(self, content) + + def getContent(self): + raise NotImplementedError + + def edit(self, edit, abort=False, sync=False): + BaseTextEditor.edit(self, edit) + if edit: + if self.options['listen_focus'] and self not in self.textarea._focusListeners: + self.textarea.addFocusListener(self) + if self.options['listen_click']: + self.display.clearClickListener() + if self not in self.textarea._keyboardListeners: + self.textarea.addKeyboardListener(self) + else: + self.setDisplayContent() + if self.options['listen_focus']: + try: + self.textarea.removeFocusListener(self) + except ValueError: + pass + if self.options['listen_click'] and self not in self.display._clickListeners: + self.display.addClickListener(self) + try: + self.textarea.removeKeyboardListener(self) + except ValueError: + pass + + def setDisplayContent(self): + text = self._original_content['text'] + if not self.options['no_xhtml']: + text = addURLToImage(text) + if self.options['enhance_display']: + text = addURLToText(text) + self.display.setHTML(convertNewLinesToXHTML(text)) + + def setFocus(self, focus): + raise NotImplementedError + + def onKeyDown(self, sender, keycode, modifiers): + for listener in self.edit_listeners: + listener(self.textarea, keycode) + if not self.options['listen_keyboard']: + return + if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with <shift> + <enter> + self.__shift_down = True + return + if keycode == KEY_ENTER: # finish the edition + self.textarea.setFocus(False) + if not self.options['listen_focus']: + self.edit(False) + + def onKeyUp(self, sender, keycode, modifiers): + if keycode == KEY_SHIFT: + self.__shift_down = False + + def onLostFocus(self, sender): + """Finish the edition when focus is lost""" + if self.options['listen_focus']: + self.edit(False) + + def onClick(self, sender=None): + """Start the edition when the widget is clicked""" + if self.options['listen_click']: + self.edit(True) + + def onBrowserEvent(self, event): + if self.options['listen_focus']: + FocusHandler.onBrowserEvent(self, event) + if self.options['listen_click']: + ClickHandler.onBrowserEvent(self, event) + KeyboardHandler.onBrowserEvent(self, event) + + +class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): + """Manage a simple text editor with the HTML 5 "contenteditable" property.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + HTML.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = self.display = self + + def getContent(self): + text = DOM.getInnerHTML(self.getElement()) + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setHTML(self._original_content['text']) + self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.getElement().focus() + else: + self.getElement().blur() + + +class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): + """Manage a simple text editor with a TextArea for editing, HTML for display.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + SimplePanel.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = TextArea() + self.display = HTML() + + def getContent(self): + text = self.textarea.getText() + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setText(self._original_content['text']) + self.setWidget(self.textarea if edit else self.display) + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.textarea.setCursorPos(len(self.textarea.getText())) + self.textarea.setFocus(focus)