# HG changeset patch # User Goffi # Date 1424970654 -3600 # Node ID 6d3142b782c35e6567060cc2b6228118b4478e25 # Parent e0021d571eef4893f612a331e25ab7150c4d6fe6 browser_side: classes reorganisation: - moved widgets in dedicated modules (base, contact, editor, libervia) and a widget module for single classes - same thing for panels (base, main, contact) - libervia_widget mix main panels and widget and drag n drop for technical reasons (see comments) - renamed WebPanel to WebWidget diff -r e0021d571eef -r 6d3142b782c3 src/browser/libervia_main.py --- a/src/browser/libervia_main.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/libervia_main.py Thu Feb 26 18:10:54 2015 +0100 @@ -42,13 +42,14 @@ from sat_browser import json from sat_browser import register from sat_browser.contact_list import ContactList -from sat_browser import base_widget -from sat_browser import panels +from sat_browser import widget +from sat_browser import main_panel from sat_browser import blog from sat_browser import dialog from sat_browser import xmlui from sat_browser import html_tools from sat_browser import notification +from sat_browser import libervia_widget from sat_browser.constants import Const as C import os.path @@ -69,7 +70,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 base_widget.WidgetsPanel, or replaced to the expected +# and added to new libervia_widget.WidgetsPanel, or replaced to the expected # position if the previous and the new parent are the same. REUSE_EXISTING_LIBERVIA_WIDGETS = True @@ -81,7 +82,7 @@ QuickApp.__init__(self, json.BridgeCall) self.uni_box = None # FIXME: to be removed self.status_panel = HTML('
') - self.panel = panels.MainPanel(self) + self.panel = main_panel.MainPanel(self) self.tab_panel = self.panel.tab_panel self.tab_panel.addTabListener(self) self._register_box = None @@ -156,15 +157,15 @@ def getSelected(self): wid = self.tab_panel.getCurrentPanel() - if not isinstance(wid, base_widget.WidgetsPanel): - log.error("Tab widget is not a base_widget.WidgetsPanel, can't get selected widget") + if not isinstance(wid, libervia_widget.WidgetsPanel): + log.error("Tab widget is not a WidgetsPanel, can't get selected widget") return None return wid.selected def setSelected(self, widget): """Define the selected widget""" widgets_panel = self.tab_panel.getCurrentPanel() - if not isinstance(widgets_panel, base_widget.WidgetsPanel): + if not isinstance(widgets_panel, libervia_widget.WidgetsPanel): return selected = widgets_panel.selected @@ -194,7 +195,8 @@ return True def onTabSelected(self, sender, tab_index): - selected = self.getSelected() + pass + # selected = self.getSelected() # FIXME: # for callback in self._selected_listeners: # callback(selected) @@ -306,7 +308,7 @@ # display the real presence status panel self.panel.header.remove(self.status_panel) - self.status_panel = panels.PresenceStatusPanel(self) + self.status_panel = main_panel.PresenceStatusPanel(self) self.panel.header.add(self.status_panel) self.bridge_signals.call('getSignals', self.bridge_signals.signalHandler) @@ -329,7 +331,7 @@ self.plug_profiles([C.PROF_KEY_NONE]) # XXX: None was used intitially, but pyjamas bug when using variable arguments and None is the only arg. microblog_widget = self.displayWidget(blog.MicroblogPanel, ()) self.setSelected(microblog_widget) - # self.discuss_panel.addWidget(panels.MicroblogPanel(self, [])) + # self.discuss_panel.addWidget(panel.MicroblogPanel(self, [])) # # get cached params and refresh the display # def param_cb(cat, name, count): @@ -344,11 +346,11 @@ def profilePlugged(self, dummy): QuickApp.profilePlugged(self, dummy) # we fill the panels already here - for widget in self.widgets.getWidgets(blog.MicroblogPanel): - if widget.accept_all(): - self.bridge.getMassiveLastMblogs('ALL', [], 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert) + for wid in self.widgets.getWidgets(blog.MicroblogPanel): + if wid.accept_all(): + self.bridge.getMassiveLastMblogs('ALL', [], 10, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) else: - self.bridge.getMassiveLastMblogs('GROUP', widget.accepted_groups, 10, profile=C.PROF_KEY_NONE, callback=widget.massiveInsert) + self.bridge.getMassiveLastMblogs('GROUP', wid.accepted_groups, 10, profile=C.PROF_KEY_NONE, callback=wid.massiveInsert) #we ask for our own microblogs: self.bridge.getMassiveLastMblogs('JID', [unicode(self.whoami.bare)], 10, profile=C.PROF_KEY_NONE, callback=self._ownBlogsFills) @@ -363,9 +365,9 @@ self.panel.addContactList(contact_list) return contact_list - def newWidget(self, widget): - log.debug("newWidget: {}".format(widget)) - self.addWidget(widget) + def newWidget(self, wid): + log.debug("newWidget: {}".format(wid)) + self.addWidget(wid) def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile): if type_ == C.MESS_TYPE_HEADLINE: @@ -418,7 +420,7 @@ # TODO: use the bare instead of node when all blogs can be retrieved node = jid.JID(data['public_blog']).node # FIXME: "/blog/{}" won't work with unicode nodes - self.displayWidget(panels.WebPanel, "/blog/{}".format(node), show_url=False, new_tab="{}'s blog".format(unicode(node))) + self.displayWidget(widget.WebWidget, "/blog/{}".format(node), show_url=False, new_tab="{}'s blog".format(unicode(node))) else: dialog.InfoDialog("Error", "Unmanaged action result", Width="400px").center() @@ -464,8 +466,8 @@ if len(self.mblog_cache) > MAX_MBLOG_CACHE: del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - for widget in self.widgets.getWidgets(blog.MicroblogPanel): - self.FillMicroblogPanel(widget) + for wid in self.widgets.getWidgets(blog.MicroblogPanel): + self.FillMicroblogPanel(wid) # FIXME self.initialised = True # initialisation phase is finished here @@ -485,7 +487,7 @@ # self.bridge.call('getWaitingSub', self._getWaitingSubCb) # #we fill the panels already here # for lib_wid in self.libervia_widgets: - # if isinstance(lib_wid, panels.MicroblogPanel): + # if isinstance(lib_wid, panel.MicroblogPanel): # if lib_wid.accept_all(): # self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10) # else: @@ -516,8 +518,8 @@ _groups = None mblog_entry = blog.MicroblogItem(data) - for widget in self.widgets.getWidgets(blog.MicroblogPanel): - widget.addEntryIfAccepted(sender, _groups, mblog_entry) + for wid in self.widgets.getWidgets(blog.MicroblogPanel): + wid.addEntryIfAccepted(sender, _groups, mblog_entry) if sender == self.whoami.bare: found = False @@ -534,8 +536,8 @@ if len(self.mblog_cache) > MAX_MBLOG_CACHE: del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] elif event_type == 'MICROBLOG_DELETE': - for widget in self.widgets.getWidgets(blog.MicroblogPanel): - widget.removeEntry(data['type'], data['id']) + for wid in self.widgets.getWidgets(blog.MicroblogPanel): + wid.removeEntry(data['type'], data['id']) log.debug("%s %s %s" % (self.whoami.bare, sender, data['type'])) if sender == self.whoami.bare and data['type'] == 'main_item': @@ -622,7 +624,7 @@ except quick_widgets.WidgetAlreadyExistsError: kwargs['on_existing_widget'] = C.WIDGET_KEEP wid = self.widgets.getOrCreateWidget(class_, target, *args, **kwargs) - widgets_panel = wid.getParent(base_widget.WidgetsPanel, expect=False) + widgets_panel = wid.getParent(libervia_widget.WidgetsPanel, expect=False) if widgets_panel is None: # The widget exists but is hidden self.addWidget(wid) @@ -675,18 +677,18 @@ # """Get the MUC widget for the given target. # @param target (jid.JID): BARE jid of the MUC - # @return: panels.ChatPanel instance or None + # @return: panel.ChatPanel instance or None # """ # entity = {'item': target, 'type_': 'group'} # if target.full() in self.room_list or target in self.room_list: # as JID is a string-based class, we don't know what will please Pyjamas... - # return self.getLiberviaWidget(panels.ChatPanel, entity, ignoreOtherTabs=False) + # return self.getLiberviaWidget(panel.ChatPanel, entity, ignoreOtherTabs=False) # return None # def getOrCreateRoomWidget(self, target): # """Get the MUC widget for the given target, create it if necessary. # @param target (jid.JID): BARE jid of the MUC - # @return: panels.ChatPanel instance + # @return: panel.ChatPanel instance # """ # lib_wid = self.getRoomWidget(target) # if lib_wid: @@ -707,7 +709,7 @@ # self.room_list.append(target) # entity = {'item': target, 'type_': 'group'} - # return self.getOrCreateLiberviaWidget(panels.ChatPanel, entity, new_tab=tab_name) + # return self.getOrCreateLiberviaWidget(panel.ChatPanel, entity, new_tab=tab_name) # def _newMessageCb(self, from_jid_s, msg, msg_type, to_jid_s, extra): # from_jid = jid.JID(from_jid_s) @@ -720,7 +722,7 @@ # def newMessageCb(self, from_jid, msg, msg_type, to_jid, extra): # other = to_jid if from_jid.bare == self.whoami.bare else from_jid - # lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': other}, ignoreOtherTabs=False) + # lib_wid = self.getLiberviaWidget(panel.ChatPanel, {'item': other}, ignoreOtherTabs=False) # self.displayNotification(from_jid, msg) # if msg_type == 'headline' and from_jid.full() == self._defaultDomain: # try: @@ -748,7 +750,7 @@ # def _presenceUpdateCb(self, entity, show, priority, statuses): # entity_jid = jid.JID(entity) # if self.whoami and self.whoami == entity_jid: # XXX: QnD way to get our presence/status - # assert(isinstance(self.status_panel, panels.PresenceStatusPanel)) + # assert(isinstance(self.status_panel, main_panel.PresenceStatusPanel)) # self.status_panel.setPresence(show) # pylint: disable=E1103 # if statuses: # self.status_panel.setStatus(statuses.values()[0]) # pylint: disable=E1103 @@ -881,11 +883,11 @@ # """ # if from_jid_s == '@ALL@': # for lib_wid in self.libervia_widgets: - # if isinstance(lib_wid, panels.ChatPanel): + # if isinstance(lib_wid, panel.ChatPanel): # lib_wid.setState(state, nick=C.ALL_OCCUPANTS) # return # from_jid = jid.JID(from_jid_s) - # lib_wid = self.getLiberviaWidget(panels.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False) + # lib_wid = self.getLiberviaWidget(panel.ChatPanel, {'item': from_jid}, ignoreOtherTabs=False) # lib_wid.setState(state, nick=from_jid.resource) def _askConfirmation(self, confirmation_id, confirmation_type, data): @@ -945,7 +947,7 @@ # elif type_ == "COMMENT": # self.bridge.call("sendMblogComment", None, entities, text, extra) # elif type_ == "STATUS": - # assert(isinstance(self.status_panel, panels.PresenceStatusPanel)) + # assert(isinstance(self.status_panel, main_panel.PresenceStatusPanel)) # self.bridge.call('setStatus', None, self.status_panel.presence, text) # pylint: disable=E1103 # elif type_ in ("groupchat", "chat"): # addresses.append((addr, entities)) @@ -970,7 +972,7 @@ @msg: message to be displayed """ if not hasattr(self, "warning_popup"): - self.warning_popup = panels.WarningPopup() + self.warning_popup = main_panel.WarningPopup() self.warning_popup.showWarning(type_, msg) def showDialog(self, message, title="", type_="info", answer_cb=None, answer_data=None): diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/base_menu.py --- a/src/browser/sat_browser/base_menu.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/base_menu.py Thu Feb 26 18:10:54 2015 +0100 @@ -24,7 +24,6 @@ by base_widget.py, and the import sequence caused a JS runtime error.""" -import pyjd # this is dummy in pyjs from sat.core.log import getLogger log = getLogger(__name__) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/base_panel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/base_panel.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,227 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat.core.i18n import _ + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.ScrollPanel import ScrollPanel +from pyjamas.ui.Button import Button +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 import DOM + + +### Menus ### + + +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": "itemKeyMenu", "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) + + +### Generic panels ### + + +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 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) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/base_panels.py --- a/src/browser/sat_browser/base_panels.py Thu Feb 26 13:10:46 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,688 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ -from sat_frontends.tools import strings -from sat_frontends.tools import jid - -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -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 - -import html_tools -import base_widget -from constants import Const as C - - - -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, StyleName="occupant") - self.nick = nick - self._state = state - self.special = special - self._refresh() - - def __str__(self): - return self.nick - - def setState(self, state): - self._state = state - self._refresh() - - def addSpecial(self, special): - """@param special: unicode""" - if special not in self.special: - self.special += special - self._refresh() - - def removeSpecials(self, special): - """@param special: unicode or list""" - if not isinstance(special, list): - special = [special] - for symbol in special: - self.special = self.special.replace(symbol, "") - self._refresh() - - def _refresh(self): - state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' - special = "" if len(self.special) == 0 else " %s" % self.special - self.setHTML("%s%s%s" % (html_tools.html_sanitize(self.nick), special, state)) - - -class ContactsPanel(VerticalPanel): - """ContactList graphic representation - - Special features like popup menu panel or changing the contact states must be done in a sub-class. - """ - - def __init__(self, parent, on_click=None, handle_menu=True): - """ - @param on_click (callable): click callback (used if ContactBox is created) - @param handle_menu (bool): if True, bind a popup menu to the avatar (used if ContactBox is created) - """ # FIXME - VerticalPanel.__init__(self) - self._parent = parent - self.host = parent.host - self._contacts = {} # entity jid to ContactBox map - self.click_listener = None - self.handle_menu = handle_menu - - if on_click is not None: - self.onClick = on_click - - def display(self, jids): - """Display a contact in the list. - - @param jids (list[jid.JID]): jids to display (the order is kept) - @param name (unicode): optional name of the contact - """ - # FIXME: we do a full clear and add boxes after, we should only remove recently hidden boxes and add new ones, and re-order - current = [box.jid for box in self.children if isinstance(box, base_widget.ContactBox)] - if current == jids: - # the display doesn't change - return - self.clear() - for jid_ in jids: - assert isinstance(jid_, jid.JID) - box = self.getContactBox(jid_) - VerticalPanel.append(self, box) - - 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 getContactBox(self, contact_jid): - """get the Contactbox of a contact - - if the Contactbox doesn't exists, it will be created - @param contact_jid (jid.JID): the contact - @return: ContactBox instance - """ - try: - return self._contacts[contact_jid.bare] - except KeyError: - box = base_widget.ContactBox(self, contact_jid) - self._contacts[contact_jid.bare] = box - return box - - def updateAvatar(self, jid_, url): - """Update the avatar of the given contact - - @param jid_ (jid.JID): contact jid - @param url (unicode): image url - """ - try: - self.getContactBox(jid_).updateAvatar(url) - except TypeError: - pass - - def updateNick(self, jid_, new_nick): - """Update the avatar of the given contact - - @param jid_ (jid.JID): contact jid - @param new_nick (unicode): new nick of the contact - """ - try: - self.getContactBox(jid_).updateNick(new_nick) - except TypeError: - pass - - - -# FIXME: must be removed and ContactsPanel must be used instead -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): - if nick in self.occupants_list: - return - _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 getOccupantBox(self, nick): - """Get the widget element of the given nick. - - @return: Occupant - """ - try: - return self.occupants_list[nick] - except KeyError: - return None - - 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": "itemKeyMenu", "item": "popupMenuItem"} - if isinstance(style, dict): - self.style.update(style) - self._senders = {} - - def _show(self, sender): - """Popup the menu relative to this sender's position. - @param sender: the widget that has been clicked - """ - menu = VerticalPanel() if self.vertical is True else HorizontalPanel() - menu.setStyleName(self.style["menu"]) - - def button_cb(item): - """You can not put that method in the loop and rely - on _key, because it is overwritten by each step. - You can rely on item.key instead, which is copied - from _key after the item creation. - @param item: the menu item that has been clicked - """ - if self._callback is not None: - self._callback(sender=sender, key=item.key) - self.hide(autoClosed=True) - - for _key in self._entries.keys(): - entry = self._entries[_key] - if self._hide is not None and self._hide(sender=sender, key=_key) is True: - continue - title = entry["title"] if "title" in entry.keys() else _key - item = Button(title, button_cb) - item.key = _key - item.setStyleName(self.style["item"]) - item.setTitle(entry["desc"] if "desc" in entry.keys() else title) - menu.add(item) - if len(menu.getChildren()) == 0: - return - self.add(menu) - if self.vertical is True: - x = sender.getAbsoluteLeft() + sender.getOffsetWidth() - y = sender.getAbsoluteTop() - else: - x = sender.getAbsoluteLeft() - y = sender.getAbsoluteTop() + sender.getOffsetHeight() - self.setPopupPosition(x, y) - self.show() - if self.style["selected"]: - sender.addStyleDependentName(self.style["selected"]) - - def _onHide(popup): - if self.style["selected"]: - sender.removeStyleDependentName(self.style["selected"]) - return PopupPanel.onHideImpl(self, popup) - - self.onHideImpl = _onHide - - def registerClickSender(self, sender, button=BUTTON_LEFT): - """Bind the menu to the specified sender. - @param sender: the widget to which the menu should be bound - @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT - """ - self._senders.setdefault(sender, []) - self._senders[sender].append(button) - - if button == BUTTON_RIGHT: - # WARNING: to disable the context menu is a bit tricky... - # The following seems to work on Firefox 24.0, but: - # TODO: find a cleaner way to disable the context menu - sender.getElement().setAttribute("oncontextmenu", "return false") - - def _onBrowserEvent(event): - button = DOM.eventGetButton(event) - if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: - self._show(sender) - return sender.__class__.onBrowserEvent(sender, event) - - sender.onBrowserEvent = _onBrowserEvent - - def registerMiddleClickSender(self, sender): - self.registerClickSender(sender, BUTTON_MIDDLE) - - def registerRightClickSender(self, sender): - self.registerClickSender(sender, BUTTON_RIGHT) - - -class ToggleStackPanel(StackPanel): - """This is a pyjamas.ui.StackPanel with modified behavior. All sub-panels ca be - visible at the same time, clicking a sub-panel header will not display it and hide - the others but only toggle its own visibility. The argument 'visibleStack' is ignored. - Note that the argument 'visible' has been added to listener's 'onStackChanged' method. - """ - - def __init__(self, **kwargs): - StackPanel.__init__(self, **kwargs) - - def onBrowserEvent(self, event): - if DOM.eventGetType(event) == "click": - index = self.getDividerIndex(DOM.eventGetTarget(event)) - if index != -1: - self.toggleStack(index) - - def add(self, widget, stackText="", asHTML=False, visible=False): - StackPanel.add(self, widget, stackText, asHTML) - self.setStackVisible(self.getWidgetCount() - 1, visible) - - def toggleStack(self, index): - if index >= self.getWidgetCount(): - return - visible = not self.getWidget(index).getVisible() - self.setStackVisible(index, visible) - for listener in self.stackListeners: - listener.onStackChanged(self, index, visible) - - -class TitlePanel(ToggleStackPanel): - """A toggle panel to set the message title""" - def __init__(self): - ToggleStackPanel.__init__(self, Width="100%") - self.text_area = TextArea() - self.add(self.text_area, _("Title")) - self.addStackChangeListener(self) - - def onStackChanged(self, sender, index, visible=None): - if visible is None: - visible = sender.getWidget(index).getVisible() - text = self.text_area.getText() - suffix = "" if (visible or not text) else (": %s" % text) - sender.setStackText(index, _("Title") + suffix) - - def getText(self): - return self.text_area.getText() - - def setText(self, text): - self.text_area.setText(text) - - -class BaseTextEditor(object): - """Basic definition of a text editor. The method edit gets a boolean parameter which - should be set to True when you want to edit the text and False to only display it.""" - - def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): - """ - Remark when inheriting this class: since the setContent method could be - overwritten by the child class, you should consider calling this __init__ - after all the parameters affecting this setContent method have been set. - @param content: dict with at least a 'text' key - @param strproc: method to be applied on strings to clean the content - @param modifiedCb: method to be called when the text has been modified. - If this method returns: - - True: the modification will be saved and afterEditCb called; - - False: the modification won't be saved and afterEditCb called; - - None: the modification won't be saved and afterEditCb not called. - @param afterEditCb: method to be called when the edition is done - """ - if content is None: - content = {'text': ''} - assert('text' in content) - if strproc is None: - def strproc(text): - try: - return text.strip() - except (TypeError, AttributeError): - return text - self.strproc = strproc - self.__modifiedCb = modifiedCb - self._afterEditCb = afterEditCb - self.initialized = False - self.edit_listeners = [] - self.setContent(content) - - def setContent(self, content=None): - """Set the editable content. The displayed content, which is set from the child class, could differ. - @param content: dict with at least a 'text' key - """ - if content is None: - content = {'text': ''} - elif not isinstance(content, dict): - content = {'text': content} - assert('text' in content) - self._original_content = {} - for key in content: - self._original_content[key] = self.strproc(content[key]) - - def getContent(self): - """Get the current edited or editable content. - @return: dict with at least a 'text' key - """ - raise NotImplementedError - - def setOriginalContent(self, content): - """Use this method with care! Content initialization should normally be - done with self.setContent. This method exists to let you trick the editor, - e.g. for self.modified to return True also when nothing has been modified. - @param content: dict - """ - self._original_content = content - - def getOriginalContent(self): - """ - @return the original content before modification (dict) - """ - return self._original_content - - def modified(self, content=None): - """Check if the content has been modified. - Remark: we don't use the direct comparison because we want to ignore empty elements - @content: content to be check against the original content or None to use the current content - @return: True if the content has been modified. - """ - if content is None: - content = self.getContent() - # the following method returns True if one non empty element exists in a but not in b - diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] - # the following method returns True if the values for the common keys are not equals - diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] - # finally the combination of both to return True if a difference is found - diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) - - return diff(content, self._original_content) - - def edit(self, edit, abort=False, sync=False): - """ - Remark: the editor must be visible before you call this method. - @param edit: set to True to edit the content or False to only display it - @param abort: set to True to cancel the edition and loose the changes. - If edit and abort are both True, self.abortEdition can be used to ask for a - confirmation. When edit is False and abort is True, abortion is actually done. - @param sync: set to True to cancel the edition after the content has been saved somewhere else - """ - if edit: - if not self.initialized: - self.syncToEditor() # e.g.: use the selected target and unibox content - self.setFocus(True) - if abort: - content = self.getContent() - if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation - self.edit(False, True, sync) - return - if sync: - self.syncFromEditor(content) # e.g.: save the content to unibox - return - else: - if not self.initialized: - return - content = self.getContent() - if abort: - self._afterEditCb(content) - return - if self.__modifiedCb and self.modified(content): - result = self.__modifiedCb(content) # e.g.: send a message or update something - if result is not None: - if self._afterEditCb: - self._afterEditCb(content) # e.g.: restore the display mode - if result is True: - self.setContent(content) - elif self._afterEditCb: - self._afterEditCb(content) - - self.initialized = True - - def setFocus(self, focus): - """ - @param focus: set to True to focus the editor - """ - raise NotImplementedError - - def syncToEditor(self): - pass - - def syncFromEditor(self, content): - pass - - def abortEdition(self, content): - return True - - def addEditListener(self, listener): - """Add a method to be called whenever the text is edited. - @param listener: method taking two arguments: sender, keycode""" - self.edit_listeners.append(listener) - - -class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): - """Base class for manage a simple text editor.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - """ - @param content - @param modifiedCb - @param afterEditCb - @param options: dict with the following value: - - no_xhtml: set to True to clean any xhtml content. - - enhance_display: if True, the display text will be enhanced with strings.addURLToText - - listen_keyboard: set to True to terminate the edition with or . - - listen_focus: set to True to terminate the edition when the focus is lost. - - listen_click: set to True to start the edition when you click on the widget. - """ - self.options = {'no_xhtml': False, - 'enhance_display': True, - 'listen_keyboard': True, - 'listen_focus': False, - 'listen_click': False - } - if options: - self.options.update(options) - self.__shift_down = False - if self.options['listen_focus']: - FocusHandler.__init__(self) - if self.options['listen_click']: - ClickHandler.__init__(self) - KeyboardHandler.__init__(self) - strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) - BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) - self.textarea = self.display = None - - def setContent(self, content=None): - BaseTextEditor.setContent(self, content) - - def getContent(self): - raise NotImplementedError - - def edit(self, edit, abort=False, sync=False): - BaseTextEditor.edit(self, edit) - if edit: - if self.options['listen_focus'] and self not in self.textarea._focusListeners: - self.textarea.addFocusListener(self) - if self.options['listen_click']: - self.display.clearClickListener() - if self not in self.textarea._keyboardListeners: - self.textarea.addKeyboardListener(self) - else: - self.setDisplayContent() - if self.options['listen_focus']: - try: - self.textarea.removeFocusListener(self) - except ValueError: - pass - if self.options['listen_click'] and self not in self.display._clickListeners: - self.display.addClickListener(self) - try: - self.textarea.removeKeyboardListener(self) - except ValueError: - pass - - def setDisplayContent(self): - text = self._original_content['text'] - if not self.options['no_xhtml']: - text = strings.addURLToImage(text) - if self.options['enhance_display']: - text = strings.addURLToText(text) - self.display.setHTML(html_tools.convertNewLinesToXHTML(text)) - - def setFocus(self, focus): - raise NotImplementedError - - def onKeyDown(self, sender, keycode, modifiers): - for listener in self.edit_listeners: - listener(self.textarea, keycode) - if not self.options['listen_keyboard']: - return - if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with + - self.__shift_down = True - return - if keycode == KEY_ENTER: # finish the edition - self.textarea.setFocus(False) - if not self.options['listen_focus']: - self.edit(False) - - def onKeyUp(self, sender, keycode, modifiers): - if keycode == KEY_SHIFT: - self.__shift_down = False - - def onLostFocus(self, sender): - """Finish the edition when focus is lost""" - if self.options['listen_focus']: - self.edit(False) - - def onClick(self, sender=None): - """Start the edition when the widget is clicked""" - if self.options['listen_click']: - self.edit(True) - - def onBrowserEvent(self, event): - if self.options['listen_focus']: - FocusHandler.onBrowserEvent(self, event) - if self.options['listen_click']: - ClickHandler.onBrowserEvent(self, event) - KeyboardHandler.onBrowserEvent(self, event) - - -class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): - """Manage a simple text editor with the HTML 5 "contenteditable" property.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - HTML.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = self.display = self - - def getContent(self): - text = DOM.getInnerHTML(self.getElement()) - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setHTML(self._original_content['text']) - self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus: - self.getElement().focus() - else: - self.getElement().blur() - - -class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): - """Manage a simple text editor with a TextArea for editing, HTML for display.""" - - def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): - SimplePanel.__init__(self) - SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) - self.textarea = TextArea() - self.display = HTML() - - def getContent(self): - text = self.textarea.getText() - return {'text': self.strproc(text) if text else ''} - - def edit(self, edit, abort=False, sync=False): - if edit: - self.textarea.setText(self._original_content['text']) - self.setWidget(self.textarea if edit else self.display) - SimpleTextEditor.edit(self, edit, abort, sync) - - def setFocus(self, focus): - if focus and self.isAttached(): - self.textarea.setCursorPos(len(self.textarea.getText())) - self.textarea.setFocus(focus) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/base_widget.py --- a/src/browser/sat_browser/base_widget.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/base_widget.py Thu Feb 26 18:10:54 2015 +0100 @@ -20,197 +20,18 @@ import pyjd # this is dummy in pyjs from sat.core.log import getLogger log = getLogger(__name__) -from sat.core import exceptions -from sat.core.i18n import _ -from sat_frontends.quick_frontend import quick_widgets +import base_menu -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.HTML import HTML -from pyjamas.ui.Image import Image -from pyjamas.ui.Button import Button -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 constants import Const as C -from __pyjamas__ import doc - -import dialog -import base_menu -import html_tools - -unicode = str # XXX: pyjama doesn't manage unicode +### Exceptions ### class NoLiberviaWidgetException(Exception): + """A Libervia widget was expected""" pass -class DragLabel(DragWidget): - - def __init__(self, text, type_, host=None): - """Base of Drag n Drop mecanism in Libervia - - @param text: data embedded with in drag n drop operation - @param type_: type of data that we are dragging - @param host: if not None, the host will be use to highlight BorderWidgets - """ - DragWidget.__init__(self) - self.host = host - 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) - if self.host is not None: - current_panel = self.host.tab_panel.getCurrentPanel() - for widget in current_panel.widgets: - if isinstance(widget, BorderWidget): - widget.addStyleName('borderWidgetOnDrag') - - def onDragEnd(self, event): - if self.host is not None: - current_panel = self.host.tab_panel.getCurrentPanel() - for widget in current_panel.widgets: - if isinstance(widget, BorderWidget): - widget.removeStyleName('borderWidgetOnDrag') - - -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_, widget.host) - self.widget = widget - - def onDragStart(self, event): - LiberviaDragWidget.current = self.widget - DragLabel.onDragStart(self, event) - - def onDragEnd(self, event): - DragLabel.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, cb): - """Add a association between a key and a class to create on drop. - - @param key: key to be associated (e.g. "CONTACT", "CHAT") - @param cb: a callable (either a class or method) returning a - LiberviaWidget instance - """ - DropCell.drop_keys[key] = cb - - 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): - """ - @raise NoLiberviaWidgetException: something else than a LiberviaWidget - has been returned by the callback. - """ - 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.getParent(WidgetsPanel, expect=True) - 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) - if not isinstance(_new_panel, LiberviaWidget): - raise NoLiberviaWidgetException - else: - log.warning("unmanaged item type") - return - if isinstance(self, LiberviaWidget): - # self.host.unregisterWidget(self) # FIXME - 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)""" - if isinstance(self, quick_widgets.QuickWidget): - self.host.widgets.deleteWidget(self) +### Menus ### class WidgetMenuBar(base_menu.GenericMenuBar): @@ -259,744 +80,3 @@ @classmethod def getCategoryHTML(cls, menu_name_i18n, type_): return menu_name_i18n - - -class WidgetHeader(AbsolutePanel, LiberviaDragWidget): - - def __init__(self, parent, host, title, info=None): - """ - @param parent (LiberviaWidget): LiberWidget instance - @param host (SatWebFrontend): SatWebFrontend instance - @param title (Label, HTML): text widget instance - @param info (Widget): text widget instance - """ - AbsolutePanel.__init__(self) - self.add(title) - if info: - # FIXME: temporary design to display the info near the menu - button_group_wrapper = HorizontalPanel() - button_group_wrapper.add(info) - else: - button_group_wrapper = SimplePanel() - button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') - button_group = WidgetMenuBar(parent, host) - button_group.addItem('', True, base_menu.MenuCmd(parent, 'onSetting')) - button_group.addItem('', True, base_menu.MenuCmd(parent, 'onClose')) - button_group_wrapper.add(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='', info=None, selectable=False): - """Init the widget - - @param host (SatWebFrontend): SatWebFrontend instance - @param title (str): title shown in the header of the widget - @param info (str, callable): info shown in the header of the widget - @param selectable (bool): 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') - if info is not None: - if isinstance(info, str): - self._info = HTML(info) - else: # the info will be set by a callback - assert callable(info) - self._info = HTML() - info(self._info.setHTML) - self._info.setStyleName('widgetHeader_info') - else: - self._info = None - header = WidgetHeader(self, host, self._title, self._info) - self.add(header) - self.setSize('100%', '100%') - self.addStyleName('widget') - if self._selectable: - self.addClickListener(self) - - # FIXME - # 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) # FIXME - - def getDebugName(self): - return "%s (%s)" % (self, self._title.getText()) - - 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 (raise an error if not found) - @return: the parent/ancestor or None if it has not been found - @raise exceptions.InternalError: expect is True and no parent is 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: - raise exceptions.InternalError("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 """ - widgets_panel = self.getParent(WidgetsPanel, expect=True) - widgets_panel.removeWidget(self) - self.onQuit() - self.host.widgets.deleteWidget(self) - - def onQuit(self): - """ Called when the widget is actually ending """ - pass - - 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.getParent(WidgetsPanel, expect=True) - 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 setHeaderInfo(self, text): - """change the info in the header of the widget - @param text: text of the new title""" - try: - self._info.setHTML(text) - except TypeError: - log.error("LiberviaWidget.setInfo: info widget has not been initialized!") - - 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, item): - """Check if this widget corresponds to the given entity. - - This method should be overwritten by child classes. - @return: True if the widget matches the entity""" - raise NotImplementedError - - def addMenus(self, menu_bar): - """Add menus to the header. - - This method can be overwritten by child classes. - @param menu_bar (GenericMenuBar): menu bar of the widget's header - """ - pass - - -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): - """ - - @param host (SatWebFrontend): host instance - @param locked (bool): If True, the tab containing self will not be - removed when there are no more widget inside self. If False, the - tab will be removed with self's last widget. - """ - ScrollPanelWrapper.__init__(self) - self.setSize('100%', '100%') - self.host = host - self.locked = locked - 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 in a raw - - @property - def widgets(self): - return iter(self.flextable) - - 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: # FIXME: except without exception specified ! - 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 - 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.parent.addStyleName('dragover') - DOM.eventPreventDefault(event) - - def onDragLeave(self, event): - self.parent.removeStyleName('dragover') - - def onDragOver(self, event): - DOM.eventPreventDefault(event) - - def onDrop(self, event): - DOM.eventPreventDefault(event) - self.parent.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.getParent(WidgetsPanel, expect=True).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, ClickHandler): - - def __init__(self, host): - TabPanel.__init__(self) - ClickHandler.__init__(self) - self.host = host - self.setStyleName('liberviaTabPanel') - self.addStyleName('mainTabPanel') - Window.addWindowResizeListener(self) - - self.tabBar.addTab(u'✚', True) - - def onTabSelected(self, sender, tabIndex): - if tabIndex < self.getWidgetCount(): - TabPanel.onTabSelected(self, sender, tabIndex) - return - # user clicked the "+" tab - default_label = _(u'new tab') - try: - label = Window.prompt(_(u'Name of the new tab'), default_label) - if not label: - label = default_label - except: # this happens when the user prevents the page to open the prompt dialog - label = default_label - self.addWidgetsTab(label, select=True) - - def getCurrentPanel(self): - """ Get the panel of the currently selected tab - - @return: WidgetsPanel - """ - 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 addTab(self, widget, label, select=False): - """Create a new tab for the given widget. - - @param widget (Widget): widget to associate to the tab - @param label (unicode): label of the tab - @param select (bool): True to select the added tab - """ - TabPanel.add(self, widget, DropTab(self, label), False) - if select: - self.selectTab(self.getWidgetCount() - 1) - - def addWidgetsTab(self, label, select=False, locked=False): - """Create a new tab for containing LiberviaWidgets. - - @param label (unicode): label of the tab - @param select (bool): True to select the added tab - @param locked (bool): If True, the tab will not be removed when there - are no more widget inside. If False, the tab will be removed with - the last widget. - @return: WidgetsPanel - """ - widgets_panel = WidgetsPanel(self, locked=locked) - self.addTab(widgets_panel, label, select) - return widgets_panel - - def onWidgetPanelRemove(self, panel): - """ Called when a child WidgetsPanel is empty and need to be removed """ - widget_index = self.getWidgetIndex(panel) - self.remove(panel) - widgets_count = self.getWidgetCount() - self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1) - - -class ContactLabel(HTML): - """Display a contact in HTML, selecting best display (jid/nick/etc)""" - - def __init__(self, host, jid_): - # TODO: add a listener for nick changes - HTML.__init__(self) - self.host = host - self.jid = jid_.bare - self.nick = self.host.contact_lists[C.PROF_KEY_NONE].getCache(self.jid, "nick") - self.alert = False - self.refresh() - self.setStyleName('contactLabel') - - def refresh(self): - alert_html = "(*) " if self.alert else "" - contact_html = html_tools.html_sanitize(self.nick or unicode(self.jid)) - html = "%(alert)s%(contact)s" % {'alert': alert_html, - 'contact': contact_html} - self.setHTML(html) - - def updateNick(self, new_nick): - """Change the current nick - - @param new_nick(unicode): new nick to use - """ - self.nick = new_nick - self.refresh() - - def setAlert(self, alert): - """Show a visual indicator - - @param alert: True if alert must be shown - """ - self.alert = alert - self.refresh() - - -class ContactMenuBar(WidgetMenuBar): - - def onBrowserEvent(self, event): - WidgetMenuBar.onBrowserEvent(self, event) - event.stopPropagation() # prevent opening the chat dialog - - @classmethod - def getCategoryHTML(cls, menu_name_i18n, type_): - return '' % C.DEFAULT_AVATAR_URL - - def setUrl(self, url): - """Set the URL of the contact avatar.""" - self.items[0].setHTML('' % url) - - -class ContactBox(VerticalPanel, ClickHandler, DragLabel): - - def __init__(self, parent, jid_): - """ - @param parent (ContactPanel): ContactPanel hosting this box - @param jid_ (jid.JID): contact JID - """ - VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle') - ClickHandler.__init__(self) - DragLabel.__init__(self, jid_, "CONTACT", parent.host) - self.jid = jid_.bare - self.label = ContactLabel(parent.host, self.jid) - self.avatar = ContactMenuBar(self, parent.host) if parent.handle_menu else Image() - self.updateAvatar(parent.host.getAvatarURL(self.jid)) - self.add(self.avatar) - self.add(self.label) - self.addClickListener(self) - - def addMenus(self, menu_bar): - menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': unicode(self.jid)}) - menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': unicode(self.jid)}) - - def setAlert(self, alert): - """Show a visual indicator - - @param alert: True if alert indicator show be shown""" - self.label.setAlert(alert) - - def updateAvatar(self, url): - """Update the avatar. - - @param url (unicode): image url - """ - self.avatar.setUrl(url) - - def updateNick(self, new_nick): - """Update the nickname. - - @param new_nick (unicode): new nickname to use - """ - self.label.updateNick(new_nick) - - def onClick(self, sender): - try: - self.parent.onClick(self.jid) - except AttributeError: - pass - else: - self.setAlert(False) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/blog.py --- a/src/browser/sat_browser/blog.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/blog.py Thu Feb 26 18:10:54 2015 +0100 @@ -41,10 +41,10 @@ from time import time import html_tools -import base_panels import dialog -import base_widget import richtext +import editor_widget +import libervia_widget from constants import Const as C from sat_frontends.quick_frontend import quick_widgets from sat_frontends.tools import jid @@ -233,7 +233,7 @@ options = [] if self.empty else ['update_msg'] self.bubble = richtext.RichTextEditor(self._blog_panel.host, content, self.__modifiedCb, self.__afterEditCb, options) else: # assume raw text message have no title - self.bubble = base_panels.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) + self.bubble = editor_widget.LightTextEditor(content, self.__modifiedCb, self.__afterEditCb, options={'no_xhtml': True}) self.bubble.addStyleName("bubble") try: self.toggle_syntax_button.removeFromParent() @@ -355,7 +355,7 @@ self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) -class MicroblogPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget): +class MicroblogPanel(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget): warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" warning_msg_group = "This message will be published for all the people of the group %s" # FIXME: all the generic parts must be moved to quick_frontends @@ -367,7 +367,7 @@ """ self.setAcceptedGroup(accepted_groups) quick_widgets.QuickWidget.__init__(self, host, self.target, C.PROF_KEY_NONE) - base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) + libervia_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) self.entries = {} self.comments = {} self.selected_entry = None @@ -735,7 +735,7 @@ return False -base_widget.LiberviaWidget.addDropKey("GROUP", MicroblogPanel.onGroupDrop) +libervia_widget.LiberviaWidget.addDropKey("GROUP", MicroblogPanel.onGroupDrop) # Needed for the drop keys to not be mixed between meta panel and panel for "Contacts" group -base_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, None)) +libervia_widget.LiberviaWidget.addDropKey("CONTACT_TITLE", lambda host, item: MicroblogPanel.onGroupDrop(host, None)) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/chat.py --- a/src/browser/sat_browser/chat.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/chat.py Thu Feb 26 18:10:54 2015 +0100 @@ -39,11 +39,12 @@ from time import time import html_tools -import base_panels -import panels +import libervia_widget +import base_panel +import contact_panel +import editor_widget import card_game import radiocol -import base_widget import contact_list from constants import Const as C import plugin_xep_0085 @@ -73,7 +74,7 @@ self.setStyleName('chatText') -class Chat(QuickChat, base_widget.LiberviaWidget, KeyboardHandler): +class Chat(QuickChat, libervia_widget.LiberviaWidget, KeyboardHandler): def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None): """Panel used for conversation (one 2 one or group chat) @@ -91,19 +92,19 @@ host.plugins['otr'].infoTextCallback(target, cb) header_info = header_info_cb if (type_ == C.CHAT_ONE2ONE and 'otr' in host.plugins) else None - base_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True) + libervia_widget.LiberviaWidget.__init__(self, host, title=unicode(target.bare), info=header_info, selectable=True) self._body = AbsolutePanel() self._body.setStyleName('chatPanel_body') chat_area = HorizontalPanel() chat_area.setStyleName('chatArea') if type_ == C.CHAT_GROUP: - self.occupants_list = base_panels.OccupantsList() + self.occupants_list = contact_panel.OccupantsList() self.occupants_initialised = False 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) + self.content_scroll = base_panel.ScrollPanelWrapper(self.content) chat_area.add(self.content_scroll) chat_area.setCellWidth(self.content_scroll, '100%') self.vpanel.add(self._body) @@ -153,7 +154,7 @@ if hasattr(self, 'message_box'): self.message_box.setVisible(enable_box) elif enable_box: - self.message_box = panels.MessageBox(self.host) + self.message_box = editor_widget.MessageBox(self.host) self.message_box.onSelectedChange(self) self.message_box.addKeyboardListener(self) self.vpanel.add(self.message_box) @@ -207,7 +208,7 @@ self.state_machine._onEvent("active") def onQuit(self): - base_widget.LiberviaWidget.onQuit(self) + libervia_widget.LiberviaWidget.onQuit(self) if self.type == C.CHAT_GROUP: self.host.bridge.call('mucLeave', None, unicode(self.target.bare)) @@ -363,4 +364,4 @@ quick_widgets.register(QuickChat, Chat) -base_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True)) +libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True)) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/contact_group.py --- a/src/browser/sat_browser/contact_group.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/contact_group.py Thu Feb 26 18:10:54 2015 +0100 @@ -28,8 +28,8 @@ import dialog import list_manager +import contact_panel import contact_list -import base_panels unicode = str # FIXME: pyjamas workaround @@ -186,13 +186,13 @@ """Add the contact list to the DockPanel.""" self.toggle = Button("", self.toggleContacts) self.toggle.addStyleName("toggleAssignedContacts") - self.contacts = base_panels.ContactsPanel(self.host) + self.contacts = contact_panel.ContactsPanel(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 + panel = VerticalPanel() + panel.add(self.toggle) + panel.add(self.contacts) + return panel def toggleContacts(self, sender=None, showAll=None): """Toggle the button to show contacts and the contact list. diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/contact_list.py --- a/src/browser/sat_browser/contact_list.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/contact_list.py Thu Feb 26 18:10:54 2015 +0100 @@ -31,8 +31,8 @@ from __pyjamas__ import doc from constants import Const as C -import base_widget -import base_panels +import libervia_widget +import contact_panel import blog import chat @@ -70,7 +70,7 @@ widget.presence_style = style -class GroupLabel(base_widget.DragLabel, Label, ClickHandler): +class GroupLabel(libervia_widget.DragLabel, Label, ClickHandler): def __init__(self, host, group): """ @@ -80,7 +80,7 @@ self.group = group Label.__init__(self, group) # , Element=DOM.createElement('div') self.setStyleName('group') - base_widget.DragLabel.__init__(self, group, "GROUP", host) + libervia_widget.DragLabel.__init__(self, group, "GROUP", host) ClickHandler.__init__(self) self.addClickListener(self) @@ -133,13 +133,13 @@ return self._groups -class ContactsPanel(base_panels.ContactsPanel): +class ContactsPanel(contact_panel.ContactsPanel): """The contact list that is displayed on the left side.""" def __init__(self, parent): def on_click(contact_jid): self.host.displayWidget(chat.Chat, contact_jid, type_=C.CHAT_ONE2ONE) - base_panels.ContactsPanel.__init__(self, parent, on_click=on_click, handle_menu=True) + contact_panel.ContactsPanel.__init__(self, parent, on_click=on_click, handle_menu=True) def setState(self, jid_, type_, state): """Change the appearance of the contact, according to the state @@ -161,12 +161,12 @@ contact_box.setAlert(state) -class ContactTitleLabel(base_widget.DragLabel, Label, ClickHandler): +class ContactTitleLabel(libervia_widget.DragLabel, Label, ClickHandler): def __init__(self, host, text): Label.__init__(self, text) # , Element=DOM.createElement('div') self.setStyleName('contactTitle') - base_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host) + libervia_widget.DragLabel.__init__(self, text, "CONTACT_TITLE", host) ClickHandler.__init__(self) self.addClickListener(self) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/contact_panel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact_panel.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,215 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +""" Contacts / jids related panels """ + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools import jid + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HTML import HTML + +import html_tools +import contact_widget +from constants import Const as C + + +# FIXME: must be removed +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, StyleName="occupant") + self.nick = nick + self._state = state + self.special = special + self._refresh() + + def __str__(self): + return self.nick + + def setState(self, state): + self._state = state + self._refresh() + + def addSpecial(self, special): + """@param special: unicode""" + if special not in self.special: + self.special += special + self._refresh() + + def removeSpecials(self, special): + """@param special: unicode or list""" + if not isinstance(special, list): + special = [special] + for symbol in special: + self.special = self.special.replace(symbol, "") + self._refresh() + + def _refresh(self): + state = (' %s' % C.MUC_USER_STATES[self._state]) if self._state else '' + special = "" if len(self.special) == 0 else " %s" % self.special + self.setHTML("%s%s%s" % (html_tools.html_sanitize(self.nick), special, state)) + + +class ContactsPanel(VerticalPanel): + """ContactList graphic representation + + Special features like popup menu panel or changing the contact states must be done in a sub-class. + """ + + def __init__(self, parent, on_click=None, handle_menu=True): + """ + @param on_click (callable): click callback (used if ContactBox is created) + @param handle_menu (bool): if True, bind a popup menu to the avatar (used if ContactBox is created) + """ # FIXME + VerticalPanel.__init__(self) + self._parent = parent + self.host = parent.host + self._contacts = {} # entity jid to ContactBox map + self.click_listener = None + self.handle_menu = handle_menu + + if on_click is not None: + self.onClick = on_click + + def display(self, jids): + """Display a contact in the list. + + @param jids (list[jid.JID]): jids to display (the order is kept) + @param name (unicode): optional name of the contact + """ + # FIXME: we do a full clear and add boxes after, we should only remove recently hidden boxes and add new ones, and re-order + current = [box.jid for box in self.children if isinstance(box, contact_widget.ContactBox)] + if current == jids: + # the display doesn't change + return + self.clear() + for jid_ in jids: + assert isinstance(jid_, jid.JID) + box = self.getContactBox(jid_) + VerticalPanel.append(self, box) + + 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 getContactBox(self, contact_jid): + """get the Contactbox of a contact + + if the Contactbox doesn't exists, it will be created + @param contact_jid (jid.JID): the contact + @return: ContactBox instance + """ + try: + return self._contacts[contact_jid.bare] + except KeyError: + box = contact_widget.ContactBox(self, contact_jid) + self._contacts[contact_jid.bare] = box + return box + + def updateAvatar(self, jid_, url): + """Update the avatar of the given contact + + @param jid_ (jid.JID): contact jid + @param url (unicode): image url + """ + try: + self.getContactBox(jid_).updateAvatar(url) + except TypeError: + pass + + def updateNick(self, jid_, new_nick): + """Update the avatar of the given contact + + @param jid_ (jid.JID): contact jid + @param new_nick (unicode): new nick of the contact + """ + try: + self.getContactBox(jid_).updateNick(new_nick) + except TypeError: + pass + + + +# FIXME: must be removed and ContactsPanel must be used instead +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): + if nick in self.occupants_list: + return + _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 getOccupantBox(self, nick): + """Get the widget element of the given nick. + + @return: Occupant + """ + try: + return self.occupants_list[nick] + except KeyError: + return None + + 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) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/contact_widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/contact_widget.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Image import Image +from pyjamas.ui.ClickListener import ClickHandler +from constants import Const as C +import html_tools +import base_widget +import libervia_widget + +unicode = str # XXX: pyjama doesn't manage unicode + + +class ContactLabel(HTML): + """Display a contact in HTML, selecting best display (jid/nick/etc)""" + + def __init__(self, host, jid_): + # TODO: add a listener for nick changes + HTML.__init__(self) + self.host = host + self.jid = jid_.bare + self.nick = self.host.contact_lists[C.PROF_KEY_NONE].getCache(self.jid, "nick") + self.alert = False + self.refresh() + self.setStyleName('contactLabel') + + def refresh(self): + alert_html = "(*) " if self.alert else "" + contact_html = html_tools.html_sanitize(self.nick or unicode(self.jid)) + html = "%(alert)s%(contact)s" % {'alert': alert_html, + 'contact': contact_html} + self.setHTML(html) + + def updateNick(self, new_nick): + """Change the current nick + + @param new_nick(unicode): new nick to use + """ + self.nick = new_nick + self.refresh() + + def setAlert(self, alert): + """Show a visual indicator + + @param alert: True if alert must be shown + """ + self.alert = alert + self.refresh() + + +class ContactMenuBar(base_widget.WidgetMenuBar): + + def onBrowserEvent(self, event): + base_widget.WidgetMenuBar.onBrowserEvent(self, event) + event.stopPropagation() # prevent opening the chat dialog + + @classmethod + def getCategoryHTML(cls, menu_name_i18n, type_): + return '' % C.DEFAULT_AVATAR_URL + + def setUrl(self, url): + """Set the URL of the contact avatar.""" + self.items[0].setHTML('' % url) + + +class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel): + + def __init__(self, parent, jid_): + """ + @param parent (ContactPanel): ContactPanel hosting this box + @param jid_ (jid.JID): contact JID + """ + VerticalPanel.__init__(self, StyleName='contactBox', VerticalAlignment='middle') + ClickHandler.__init__(self) + libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", parent.host) + self.jid = jid_.bare + self.label = ContactLabel(parent.host, self.jid) + self.avatar = ContactMenuBar(self, parent.host) if parent.handle_menu else Image() + self.updateAvatar(parent.host.getAvatarURL(self.jid)) + self.add(self.avatar) + self.add(self.label) + self.addClickListener(self) + + def addMenus(self, menu_bar): + menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': unicode(self.jid)}) + menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': unicode(self.jid)}) + + def setAlert(self, alert): + """Show a visual indicator + + @param alert: True if alert indicator show be shown""" + self.label.setAlert(alert) + + def updateAvatar(self, url): + """Update the avatar. + + @param url (unicode): image url + """ + self.avatar.setUrl(url) + + def updateNick(self, new_nick): + """Update the nickname. + + @param new_nick (unicode): new nickname to use + """ + self.label.updateNick(new_nick) + + def onClick(self, sender): + try: + self.parent.onClick(self.jid) + except AttributeError: + pass + else: + self.setAlert(False) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/dialog.py --- a/src/browser/sat_browser/dialog.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/dialog.py Thu Feb 26 18:10:54 2015 +0100 @@ -40,7 +40,7 @@ from pyjamas.ui.MouseListener import MouseWheelHandler from pyjamas import Window -import base_panels +import base_panel # List here the patterns that are not allowed in contact group names @@ -207,7 +207,7 @@ self.room_panel = RoomChooser(host, "" if visible == (False, True) else DEFAULT_MUC) self.contact_panel = ContactsChooser(host, nb_contact, ok_button) - self.stack_panel = base_panels.ToggleStackPanel(Width="100%") + self.stack_panel = base_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) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/editor_widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/editor_widget.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,390 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from sat.core.log import getLogger +log = getLogger(__name__) +from sat_frontends.tools import strings + +from pyjamas.ui.HTML import HTML +from pyjamas.ui.SimplePanel import SimplePanel +from pyjamas.ui.TextArea import TextArea +from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_SHIFT, KEY_UP, KEY_DOWN, KeyboardHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.Timer import Timer +from pyjamas import DOM + +import html_tools + + +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) + + 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.host.selected_widget.onTextEntered(_txt) + self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? + 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.""" + self.host.selected_widget.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 BaseTextEditor(object): + """Basic definition of a text editor. The method edit gets a boolean parameter which + should be set to True when you want to edit the text and False to only display it.""" + + def __init__(self, content=None, strproc=None, modifiedCb=None, afterEditCb=None): + """ + Remark when inheriting this class: since the setContent method could be + overwritten by the child class, you should consider calling this __init__ + after all the parameters affecting this setContent method have been set. + @param content: dict with at least a 'text' key + @param strproc: method to be applied on strings to clean the content + @param modifiedCb: method to be called when the text has been modified. + If this method returns: + - True: the modification will be saved and afterEditCb called; + - False: the modification won't be saved and afterEditCb called; + - None: the modification won't be saved and afterEditCb not called. + @param afterEditCb: method to be called when the edition is done + """ + if content is None: + content = {'text': ''} + assert('text' in content) + if strproc is None: + def strproc(text): + try: + return text.strip() + except (TypeError, AttributeError): + return text + self.strproc = strproc + self.__modifiedCb = modifiedCb + self._afterEditCb = afterEditCb + self.initialized = False + self.edit_listeners = [] + self.setContent(content) + + def setContent(self, content=None): + """Set the editable content. The displayed content, which is set from the child class, could differ. + @param content: dict with at least a 'text' key + """ + if content is None: + content = {'text': ''} + elif not isinstance(content, dict): + content = {'text': content} + assert('text' in content) + self._original_content = {} + for key in content: + self._original_content[key] = self.strproc(content[key]) + + def getContent(self): + """Get the current edited or editable content. + @return: dict with at least a 'text' key + """ + raise NotImplementedError + + def setOriginalContent(self, content): + """Use this method with care! Content initialization should normally be + done with self.setContent. This method exists to let you trick the editor, + e.g. for self.modified to return True also when nothing has been modified. + @param content: dict + """ + self._original_content = content + + def getOriginalContent(self): + """ + @return the original content before modification (dict) + """ + return self._original_content + + def modified(self, content=None): + """Check if the content has been modified. + Remark: we don't use the direct comparison because we want to ignore empty elements + @content: content to be check against the original content or None to use the current content + @return: True if the content has been modified. + """ + if content is None: + content = self.getContent() + # the following method returns True if one non empty element exists in a but not in b + diff1 = lambda a, b: [a[key] for key in set(a.keys()).difference(b.keys()) if a[key]] != [] + # the following method returns True if the values for the common keys are not equals + diff2 = lambda a, b: [1 for key in set(a.keys()).intersection(b.keys()) if a[key] != b[key]] != [] + # finally the combination of both to return True if a difference is found + diff = lambda a, b: diff1(a, b) or diff1(b, a) or diff2(a, b) + + return diff(content, self._original_content) + + def edit(self, edit, abort=False, sync=False): + """ + Remark: the editor must be visible before you call this method. + @param edit: set to True to edit the content or False to only display it + @param abort: set to True to cancel the edition and loose the changes. + If edit and abort are both True, self.abortEdition can be used to ask for a + confirmation. When edit is False and abort is True, abortion is actually done. + @param sync: set to True to cancel the edition after the content has been saved somewhere else + """ + if edit: + if not self.initialized: + self.syncToEditor() # e.g.: use the selected target and unibox content + self.setFocus(True) + if abort: + content = self.getContent() + if not self.modified(content) or self.abortEdition(content): # e.g: ask for confirmation + self.edit(False, True, sync) + return + if sync: + self.syncFromEditor(content) # e.g.: save the content to unibox + return + else: + if not self.initialized: + return + content = self.getContent() + if abort: + self._afterEditCb(content) + return + if self.__modifiedCb and self.modified(content): + result = self.__modifiedCb(content) # e.g.: send a message or update something + if result is not None: + if self._afterEditCb: + self._afterEditCb(content) # e.g.: restore the display mode + if result is True: + self.setContent(content) + elif self._afterEditCb: + self._afterEditCb(content) + + self.initialized = True + + def setFocus(self, focus): + """ + @param focus: set to True to focus the editor + """ + raise NotImplementedError + + def syncToEditor(self): + pass + + def syncFromEditor(self, content): + pass + + def abortEdition(self, content): + return True + + def addEditListener(self, listener): + """Add a method to be called whenever the text is edited. + @param listener: method taking two arguments: sender, keycode""" + self.edit_listeners.append(listener) + + +class SimpleTextEditor(BaseTextEditor, FocusHandler, KeyboardHandler, ClickHandler): + """Base class for manage a simple text editor.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + """ + @param content + @param modifiedCb + @param afterEditCb + @param options: dict with the following value: + - no_xhtml: set to True to clean any xhtml content. + - enhance_display: if True, the display text will be enhanced with strings.addURLToText + - listen_keyboard: set to True to terminate the edition with or . + - listen_focus: set to True to terminate the edition when the focus is lost. + - listen_click: set to True to start the edition when you click on the widget. + """ + self.options = {'no_xhtml': False, + 'enhance_display': True, + 'listen_keyboard': True, + 'listen_focus': False, + 'listen_click': False + } + if options: + self.options.update(options) + self.__shift_down = False + if self.options['listen_focus']: + FocusHandler.__init__(self) + if self.options['listen_click']: + ClickHandler.__init__(self) + KeyboardHandler.__init__(self) + strproc = lambda text: html_tools.html_sanitize(html_tools.html_strip(text)) if self.options['no_xhtml'] else html_tools.html_strip(text) + BaseTextEditor.__init__(self, content, strproc, modifiedCb, afterEditCb) + self.textarea = self.display = None + + def setContent(self, content=None): + BaseTextEditor.setContent(self, content) + + def getContent(self): + raise NotImplementedError + + def edit(self, edit, abort=False, sync=False): + BaseTextEditor.edit(self, edit) + if edit: + if self.options['listen_focus'] and self not in self.textarea._focusListeners: + self.textarea.addFocusListener(self) + if self.options['listen_click']: + self.display.clearClickListener() + if self not in self.textarea._keyboardListeners: + self.textarea.addKeyboardListener(self) + else: + self.setDisplayContent() + if self.options['listen_focus']: + try: + self.textarea.removeFocusListener(self) + except ValueError: + pass + if self.options['listen_click'] and self not in self.display._clickListeners: + self.display.addClickListener(self) + try: + self.textarea.removeKeyboardListener(self) + except ValueError: + pass + + def setDisplayContent(self): + text = self._original_content['text'] + if not self.options['no_xhtml']: + text = strings.addURLToImage(text) + if self.options['enhance_display']: + text = strings.addURLToText(text) + self.display.setHTML(html_tools.convertNewLinesToXHTML(text)) + + def setFocus(self, focus): + raise NotImplementedError + + def onKeyDown(self, sender, keycode, modifiers): + for listener in self.edit_listeners: + listener(self.textarea, keycode) + if not self.options['listen_keyboard']: + return + if keycode == KEY_SHIFT or self.__shift_down: # allow input a new line with + + self.__shift_down = True + return + if keycode == KEY_ENTER: # finish the edition + self.textarea.setFocus(False) + if not self.options['listen_focus']: + self.edit(False) + + def onKeyUp(self, sender, keycode, modifiers): + if keycode == KEY_SHIFT: + self.__shift_down = False + + def onLostFocus(self, sender): + """Finish the edition when focus is lost""" + if self.options['listen_focus']: + self.edit(False) + + def onClick(self, sender=None): + """Start the edition when the widget is clicked""" + if self.options['listen_click']: + self.edit(True) + + def onBrowserEvent(self, event): + if self.options['listen_focus']: + FocusHandler.onBrowserEvent(self, event) + if self.options['listen_click']: + ClickHandler.onBrowserEvent(self, event) + KeyboardHandler.onBrowserEvent(self, event) + + +class HTMLTextEditor(SimpleTextEditor, HTML, FocusHandler, KeyboardHandler): + """Manage a simple text editor with the HTML 5 "contenteditable" property.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + HTML.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = self.display = self + + def getContent(self): + text = DOM.getInnerHTML(self.getElement()) + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setHTML(self._original_content['text']) + self.getElement().setAttribute('contenteditable', 'true' if edit else 'false') + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus: + self.getElement().focus() + else: + self.getElement().blur() + + +class LightTextEditor(SimpleTextEditor, SimplePanel, FocusHandler, KeyboardHandler): + """Manage a simple text editor with a TextArea for editing, HTML for display.""" + + def __init__(self, content=None, modifiedCb=None, afterEditCb=None, options=None): + SimplePanel.__init__(self) + SimpleTextEditor.__init__(self, content, modifiedCb, afterEditCb, options) + self.textarea = TextArea() + self.display = HTML() + + def getContent(self): + text = self.textarea.getText() + return {'text': self.strproc(text) if text else ''} + + def edit(self, edit, abort=False, sync=False): + if edit: + self.textarea.setText(self._original_content['text']) + self.setWidget(self.textarea if edit else self.display) + SimpleTextEditor.edit(self, edit, abort, sync) + + def setFocus(self, focus): + if focus and self.isAttached(): + self.textarea.setCursorPos(len(self.textarea.getText())) + self.textarea.setFocus(focus) diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/file_tools.py --- a/src/browser/sat_browser/file_tools.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/file_tools.py Thu Feb 26 18:10:54 2015 +0100 @@ -20,7 +20,7 @@ from sat.core.log import getLogger log = getLogger(__name__) from constants import Const as C -from sat.core.i18n import D_ +from sat.core.i18n import _, D_ from pyjamas.ui.FileUpload import FileUpload from pyjamas.ui.FormPanel import FormPanel from pyjamas import Window diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/libervia_widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/libervia_widget.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,831 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +"""Libervia base widget""" + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from sat.core.i18n import _ +from sat.core import exceptions +from sat_frontends.quick_frontend import quick_widgets + +from pyjamas.ui.FlexTable import FlexTable +from pyjamas.ui.TabPanel import TabPanel +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.Label import Label +from pyjamas.ui.HTML import HTML +from pyjamas.ui.Button import Button +from pyjamas.ui.Widget import Widget +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.ui import HasAlignment +from pyjamas.ui.DragWidget import DragWidget +from pyjamas.ui.DropWidget import DropWidget +from pyjamas import DOM +from pyjamas import Window +from __pyjamas__ import doc + +import dialog +import base_menu +import base_widget +import base_panel + + +# FIXME: we need to group several unrelated panels/widgets in this module because of isinstance tests and other references to classes (e.g. if we separate Drag n Drop classes in a separate module, we'll have cyclic import because of the references to LiberviaWidget in DropCell). +# TODO: use a more generic method (either use duck typing, or register classes in a generic way, without hard references), then split classes in separate modules + + +### Drag n Drop ### + + +class DragLabel(DragWidget): + + def __init__(self, text, type_, host=None): + """Base of Drag n Drop mecanism in Libervia + + @param text: data embedded with in drag n drop operation + @param type_: type of data that we are dragging + @param host: if not None, the host will be use to highlight BorderWidgets + """ + DragWidget.__init__(self) + self.host = host + 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) + if self.host is not None: + current_panel = self.host.tab_panel.getCurrentPanel() + for widget in current_panel.widgets: + if isinstance(widget, BorderWidget): + widget.addStyleName('borderWidgetOnDrag') + + def onDragEnd(self, event): + if self.host is not None: + current_panel = self.host.tab_panel.getCurrentPanel() + for widget in current_panel.widgets: + if isinstance(widget, BorderWidget): + widget.removeStyleName('borderWidgetOnDrag') + + +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_, widget.host) + self.widget = widget + + def onDragStart(self, event): + LiberviaDragWidget.current = self.widget + DragLabel.onDragStart(self, event) + + def onDragEnd(self, event): + DragLabel.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, cb): + """Add a association between a key and a class to create on drop. + + @param key: key to be associated (e.g. "CONTACT", "CHAT") + @param cb: a callable (either a class or method) returning a + LiberviaWidget instance + """ + DropCell.drop_keys[key] = cb + + 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): + """ + @raise NoLiberviaWidgetException: something else than a LiberviaWidget + has been returned by the callback. + """ + 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.getParent(WidgetsPanel, expect=True) + 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) + if not isinstance(_new_panel, LiberviaWidget): + raise base_widget.NoLiberviaWidgetException + else: + log.warning("unmanaged item type") + return + if isinstance(self, LiberviaWidget): + # self.host.unregisterWidget(self) # FIXME + 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)""" + if isinstance(self, quick_widgets.QuickWidget): + self.host.widgets.deleteWidget(self) + + +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 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.parent.addStyleName('dragover') + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + self.parent.removeStyleName('dragover') + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def onDrop(self, event): + DOM.eventPreventDefault(event) + self.parent.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.getParent(WidgetsPanel, expect=True).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) + + +### Libervia Widget ### + + +class WidgetHeader(AbsolutePanel, LiberviaDragWidget): + + def __init__(self, parent, host, title, info=None): + """ + @param parent (LiberviaWidget): LiberWidget instance + @param host (SatWebFrontend): SatWebFrontend instance + @param title (Label, HTML): text widget instance + @param info (Widget): text widget instance + """ + AbsolutePanel.__init__(self) + self.add(title) + if info: + # FIXME: temporary design to display the info near the menu + button_group_wrapper = HorizontalPanel() + button_group_wrapper.add(info) + else: + button_group_wrapper = SimplePanel() + button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper') + button_group = base_widget.WidgetMenuBar(parent, host) + button_group.addItem('', True, base_menu.MenuCmd(parent, 'onSetting')) + button_group.addItem('', True, base_menu.MenuCmd(parent, 'onClose')) + button_group_wrapper.add(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='', info=None, selectable=False): + """Init the widget + + @param host (SatWebFrontend): SatWebFrontend instance + @param title (str): title shown in the header of the widget + @param info (str, callable): info shown in the header of the widget + @param selectable (bool): 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') + if info is not None: + if isinstance(info, str): + self._info = HTML(info) + else: # the info will be set by a callback + assert callable(info) + self._info = HTML() + info(self._info.setHTML) + self._info.setStyleName('widgetHeader_info') + else: + self._info = None + header = WidgetHeader(self, host, self._title, self._info) + self.add(header) + self.setSize('100%', '100%') + self.addStyleName('widget') + if self._selectable: + self.addClickListener(self) + + # FIXME + # 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) # FIXME + + def getDebugName(self): + return "%s (%s)" % (self, self._title.getText()) + + 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 (raise an error if not found) + @return: the parent/ancestor or None if it has not been found + @raise exceptions.InternalError: expect is True and no parent is 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: + raise exceptions.InternalError("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 """ + widgets_panel = self.getParent(WidgetsPanel, expect=True) + widgets_panel.removeWidget(self) + self.onQuit() + self.host.widgets.deleteWidget(self) + + def onQuit(self): + """ Called when the widget is actually ending """ + pass + + 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.getParent(WidgetsPanel, expect=True) + 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 setHeaderInfo(self, text): + """change the info in the header of the widget + @param text: text of the new title""" + try: + self._info.setHTML(text) + except TypeError: + log.error("LiberviaWidget.setInfo: info widget has not been initialized!") + + 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 = base_panel.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, item): + """Check if this widget corresponds to the given entity. + + This method should be overwritten by child classes. + @return: True if the widget matches the entity""" + raise NotImplementedError + + def addMenus(self, menu_bar): + """Add menus to the header. + + This method can be overwritten by child classes. + @param menu_bar (GenericMenuBar): menu bar of the widget's header + """ + pass + + +# XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import + + +class WidgetsPanel(base_panel.ScrollPanelWrapper): + """The panel wanaging the widgets indide a tab""" + + def __init__(self, host, locked=False): + """ + + @param host (SatWebFrontend): host instance + @param locked (bool): If True, the tab containing self will not be + removed when there are no more widget inside self. If False, the + tab will be removed with self's last widget. + """ + base_panel.ScrollPanelWrapper.__init__(self) + self.setSize('100%', '100%') + self.host = host + self.locked = locked + 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 in a raw + + @property + def widgets(self): + return iter(self.flextable) + + 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: # FIXME: except without exception specified ! + 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 + 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 MainTabPanel(TabPanel, ClickHandler): + """The panel managing the tabs""" + + def __init__(self, host): + TabPanel.__init__(self) + ClickHandler.__init__(self) + self.host = host + self.setStyleName('liberviaTabPanel') + self.addStyleName('mainTabPanel') + Window.addWindowResizeListener(self) + + self.tabBar.addTab(u'✚', True) + + def onTabSelected(self, sender, tabIndex): + if tabIndex < self.getWidgetCount(): + TabPanel.onTabSelected(self, sender, tabIndex) + return + # user clicked the "+" tab + default_label = _(u'new tab') + try: + label = Window.prompt(_(u'Name of the new tab'), default_label) + if not label: + label = default_label + except: # this happens when the user prevents the page to open the prompt dialog + label = default_label + self.addWidgetsTab(label, select=True) + + def getCurrentPanel(self): + """ Get the panel of the currently selected tab + + @return: WidgetsPanel + """ + 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 addTab(self, widget, label, select=False): + """Create a new tab for the given widget. + + @param widget (Widget): widget to associate to the tab + @param label (unicode): label of the tab + @param select (bool): True to select the added tab + """ + TabPanel.add(self, widget, DropTab(self, label), False) + if select: + self.selectTab(self.getWidgetCount() - 1) + + def addWidgetsTab(self, label, select=False, locked=False): + """Create a new tab for containing LiberviaWidgets. + + @param label (unicode): label of the tab + @param select (bool): True to select the added tab + @param locked (bool): If True, the tab will not be removed when there + are no more widget inside. If False, the tab will be removed with + the last widget. + @return: WidgetsPanel + """ + widgets_panel = WidgetsPanel(self, locked=locked) + self.addTab(widgets_panel, label, select) + return widgets_panel + + def onWidgetPanelRemove(self, panel): + """ Called when a child WidgetsPanel is empty and need to be removed """ + widget_index = self.getWidgetIndex(panel) + self.remove(panel) + widgets_count = self.getWidgetCount() + self.selectTab(widget_index if widget_index < widgets_count else widgets_count - 1) + + diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/list_manager.py --- a/src/browser/sat_browser/list_manager.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/list_manager.py Thu Feb 26 18:10:54 2015 +0100 @@ -26,8 +26,9 @@ from pyjamas.ui.DragWidget import DragWidget from pyjamas.Timer import Timer -import base_panels +import base_panel import base_widget +import libervia_widget from sat_frontends.tools import jid @@ -250,7 +251,7 @@ @param callback (callable): common callback for all menu items, takes in the button widget and the item key. """ - self.popup_menu = base_panels.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]}) + self.popup_menu = base_panel.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]}) class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget): @@ -377,7 +378,7 @@ DELETE = 3 -class ListPanel(FlowPanel, base_widget.DropCell): +class ListPanel(FlowPanel, libervia_widget.DropCell): """Panel used for listing items sharing the same key. The key is showed as a Button to which you can bind a popup menu and the items are represented with a sequence of DragAutoCompleteTextBox.""" @@ -401,7 +402,7 @@ "CONTACT_TITLE": lambda host, item: self.addItem('@@'), "CONTACT_TEXTBOX": setTargetDropCell } - base_widget.DropCell.__init__(self, None) + libervia_widget.DropCell.__init__(self, None) self.drop_keys = drop_cbs self.style = style self.addStyleName(self.style["keyPanel"]) @@ -411,7 +412,7 @@ def onDrop(self, event): try: - base_widget.DropCell.onDrop(self, event) + libervia_widget.DropCell.onDrop(self, event) except base_widget.NoLiberviaWidgetException: pass diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/main_panel.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/main_panel.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,311 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Panels used as main basis""" + +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 + +from pyjamas.ui.AbsolutePanel import AbsolutePanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.HTML import HTML +from pyjamas.ui.ClickListener import ClickHandler +from pyjamas.Timer import Timer +from pyjamas import Window +from __pyjamas__ import doc + + +import base_menu +import menu +import dialog +import base_widget +import libervia_widget +import editor_widget +import contact_list +from constants import Const as C + + +### Warning notification (visibility of message, and other warning data) ### + + +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) + + +### Status ### + + +class StatusPanel(editor_widget.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 + editor_widget.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(editor_widget.HTMLTextEditor.getContent(self)) + + def setContent(self, content): + content = self.__cleanContent(content) + editor_widget.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 PresenceStatusMenuBar(base_widget.WidgetMenuBar): + def __init__(self, parent): + styles = {'menu_bar': 'presence-button'} + base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles) + self.button = self.addCategory(u"◉", u"◉", '') + for presence, presence_i18n in C.PRESENCE.items(): + html = u' %s' % (contact_list.buildPresenceStyle(presence), presence_i18n) + self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True) + self.parent_panel = parent + + def changePresenceCb(self, presence): + """Callback to notice the backend of a new presence set by the user. + @param presence (str): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa') + """ + self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status) + + @classmethod + def getCategoryHTML(cls, menu_name_i18n, type_): + return menu_name_i18n + + +class PresenceStatusPanel(HorizontalPanel, ClickHandler): + + def __init__(self, host, presence="", status=""): + self.host = host + HorizontalPanel.__init__(self, Width='100%') + self.menu = PresenceStatusMenuBar(self) + self.status_panel = StatusPanel(host, status=status) + self.setPresence(presence) + + panel = HorizontalPanel() + panel.add(self.menu) + panel.add(self.status_panel) + panel.setCellVerticalAlignment(self.menu, '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_list.setPresenceStyle(self.menu.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) + + +### Panels managing the main area ### + + +class MainPanel(AbsolutePanel): + """The panel which take the whole screen""" + + def __init__(self, host): + self.host = host + AbsolutePanel.__init__(self) + + # menu + self.menu = menu.MainMenuPanel(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) + + # tabs + self.tab_panel = libervia_widget.MainTabPanel(host) + self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True) + + 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 addContactList(self, contact_list): + self._contacts.add(contact_list) + + def _contactsSwitch(self, btn=None): + """ (Un)hide contacts panel """ + if btn is None: + btn = self.contacts_switch + clist = self.host.contact_list + clist.setVisible(not clist.getVisible()) + btn.setText(u"«" if clist.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() + self.host.contact_panel.refresh() + + diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/menu.py --- a/src/browser/sat_browser/menu.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/menu.py Thu Feb 26 18:10:54 2015 +0100 @@ -32,7 +32,7 @@ import file_tools import xmlui import chat -import panels +import widget import dialog import contact_group import base_menu @@ -100,7 +100,7 @@ # General menu def onWebWidget(self): - web_widget = self.host.displayWidget(panels.WebPanel, C.WEB_PANEL_DEFAULT_URL) + web_widget = self.host.displayWidget(widget.WebWidget, C.WEB_PANEL_DEFAULT_URL) self.host.setSelected(web_widget) def onDisconnect(self): diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/panels.py --- a/src/browser/sat_browser/panels.py Thu Feb 26 13:10:46 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,565 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Libervia: a Salut à Toi frontend -# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import pyjd # this is dummy in pyjs -from sat.core.log import getLogger -log = getLogger(__name__) - -from sat.core.i18n import _ -from sat_frontends.tools.strings import addURLToText - -from pyjamas.ui.AbsolutePanel import AbsolutePanel -from pyjamas.ui.VerticalPanel import VerticalPanel -from pyjamas.ui.HorizontalPanel import HorizontalPanel -from pyjamas.ui.TextArea import TextArea -from pyjamas.ui.Button import Button -from pyjamas.ui.HTML import HTML -from pyjamas.ui.ClickListener import ClickHandler -from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN -from pyjamas.ui.MouseListener import MouseHandler -from pyjamas.ui.Frame import Frame -from pyjamas.Timer import Timer -from pyjamas import Window -from pyjamas import DOM -from __pyjamas__ import doc - - -import base_panels -import base_menu -import menu -import dialog -import base_widget -import contact_list -from constants import Const as C -from sat_frontends.quick_frontend import quick_widgets - - -# 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.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true' -# self.setVisible(enable) -# if enable and not self.unibox: -# self.button = Button('') -# self.button.setTitle('Open the rich text editor') -# self.button.addStyleName('uniBoxButton') -# self.add(self.button) -# self.unibox = UniBox(self.host) -# self.add(self.unibox) -# self.setCellWidth(self.unibox, '100%') -# self.button.addClickListener(self.openRichMessageEditor) -# self.unibox.addKey("@@: ") -# self.unibox.onSelectedChange(self.host.getSelected()) -# -# def openRichMessageEditor(self): -# """Open the rich text editor.""" -# self.button.setVisible(False) -# self.unibox.setVisible(False) -# self.setCellWidth(self.unibox, '0px') -# self.host.panel._contactsMove(self) -# -# def afterEditCb(): -# Window.removeWindowResizeListener(self) -# self.host.panel._contactsMove(self.host.panel._hpanel) -# self.setCellWidth(self.unibox, '100%') -# self.button.setVisible(True) -# self.unibox.setVisible(True) -# self.host.resize() -# -# richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) -# Window.addWindowResizeListener(self) -# self.host.resize() -# -# def onWindowResized(self, width, height): -# right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() -# left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() -# ideal_width = right - left - 40 -# self.host.richtext.setWidth("%spx" % ideal_width) - - -class MessageBox(TextArea): - """A basic text area for entering messages""" - - def __init__(self, host): - TextArea.__init__(self) - self.host = host - self.size = (0, 0) - self.setStyleName('messageBox') - self.addKeyboardListener(self) - MouseHandler.__init__(self) - self.addMouseListener(self) - - 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.host.selected_widget.onTextEntered(_txt) - self.host._updateInputHistory(_txt) # FIXME: why using a global variable ? - 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.""" - self.host.selected_widget.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') -# # FIXME -# # 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): # FIXME -# # 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 StatusPanel(base_panels.HTMLTextEditor): - - EMPTY_STATUS = '<click to set a status>' - - def __init__(self, host, status=''): - self.host = host - modifiedCb = lambda content: self.host.bridge.call('setStatus', None, self.host.status_panel.presence, content['text']) or True - base_panels.HTMLTextEditor.__init__(self, {'text': status}, modifiedCb, options={'no_xhtml': True, 'listen_focus': True, 'listen_click': True}) - self.edit(False) - self.setStyleName('statusPanel') - - @property - def status(self): - return self._original_content['text'] - - def __cleanContent(self, content): - status = content['text'] - if status == self.EMPTY_STATUS or status in C.PRESENCE.values(): - content['text'] = '' - return content - - def getContent(self): - return self.__cleanContent(base_panels.HTMLTextEditor.getContent(self)) - - def setContent(self, content): - content = self.__cleanContent(content) - base_panels.BaseTextEditor.setContent(self, content) - - def setDisplayContent(self): - status = self._original_content['text'] - try: - presence = self.host.status_panel.presence - except AttributeError: # during initialization - presence = None - if not status: - if presence and presence in C.PRESENCE: - status = C.PRESENCE[presence] - else: - status = self.EMPTY_STATUS - self.display.setHTML(addURLToText(status)) - - -class PresenceStatusMenuBar(base_widget.WidgetMenuBar): - def __init__(self, parent): - styles = {'menu_bar': 'presence-button'} - base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles) - self.button = self.addCategory(u"◉", u"◉", '') - for presence, presence_i18n in C.PRESENCE.items(): - html = u' %s' % (contact_list.buildPresenceStyle(presence), presence_i18n) - self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True) - self.parent_panel = parent - - def changePresenceCb(self, presence): - """Callback to notice the backend of a new presence set by the user. - @param presence (str): the new presence is a value in ('', 'chat', 'away', 'dnd', 'xa') - """ - self.host.bridge.call('setStatus', None, presence, self.parent_panel.status_panel.status) - - @classmethod - def getCategoryHTML(cls, menu_name_i18n, type_): - return menu_name_i18n - - -class PresenceStatusPanel(HorizontalPanel, ClickHandler): - - def __init__(self, host, presence="", status=""): - self.host = host - HorizontalPanel.__init__(self, Width='100%') - self.menu = PresenceStatusMenuBar(self) - self.status_panel = StatusPanel(host, status=status) - self.setPresence(presence) - - panel = HorizontalPanel() - panel.add(self.menu) - panel.add(self.status_panel) - panel.setCellVerticalAlignment(self.menu, '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_list.setPresenceStyle(self.menu.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 WebPanel(quick_widgets.QuickWidget, base_widget.LiberviaWidget): - """ (mini)browser like widget """ - - def __init__(self, host, target, show_url=True, profiles=None): - """ - @param host: SatWebFrontend instance - @param target: url to open - """ - quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE) - 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(target or "") - self._url.setWidth('100%') - if show_url: - hpanel = HorizontalPanel() - hpanel.add(self._url) - btn = Button("Go", self.onUrlClick) - hpanel.setCellWidth(self._url, "100%") - hpanel.add(btn) - self._vpanel.add(hpanel) - self._vpanel.setCellHeight(hpanel, '20px') - self._frame = Frame(target 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): - url = self._url.getText() - scheme_end = url.find(':') - scheme = "" if scheme_end == -1 else url[:scheme_end] - if scheme not in C.WEB_PANEL_SCHEMES: - url = "http://" + url - self._frame.setUrl(url) - - -class MainPanel(AbsolutePanel): - - def __init__(self, host): - self.host = host - AbsolutePanel.__init__(self) - - # menu - self.menu = menu.MainMenuPanel(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) - - # tabs - self.tab_panel = base_widget.MainTabPanel(host) - self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True) - - 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 addContactList(self, contact_list): - self._contacts.add(contact_list) - - def _contactsSwitch(self, btn=None): - """ (Un)hide contacts panel """ - if btn is None: - btn = self.contacts_switch - clist = self.host.contact_list - clist.setVisible(not clist.getVisible()) - btn.setText(u"«" if clist.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() - self.host.contact_panel.refresh() diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/richtext.py --- a/src/browser/sat_browser/richtext.py Thu Feb 26 13:10:46 2015 +0100 +++ b/src/browser/sat_browser/richtext.py Thu Feb 26 18:10:54 2015 +0100 @@ -34,14 +34,15 @@ from constants import Const as C import dialog -import base_panels +import base_panel +import editor_widget import list_manager import html_tools import blog import chat -class RichTextEditor(base_panels.BaseTextEditor, FlexTable): +class RichTextEditor(editor_widget.BaseTextEditor, FlexTable): """Panel for the rich text editor.""" def __init__(self, host, content=None, modifiedCb=None, afterEditCb=None, options=None, style=None): @@ -64,7 +65,7 @@ if isinstance(style, dict): self.style.update(style) self._prepareUI() - base_panels.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) + editor_widget.BaseTextEditor.__init__(self, content, None, modifiedCb, afterEditCb) def __readOptions(self, options): """Set the internal flags according to the given options.""" @@ -92,7 +93,7 @@ def addEditListener(self, listener): """Add a method to be called whenever the text is edited. @param listener: method taking two arguments: sender, keycode""" - base_panels.BaseTextEditor.addEditListener(self, listener) + editor_widget.BaseTextEditor.addEditListener(self, listener) if hasattr(self, 'display'): self.display.addEditListener(listener) @@ -109,7 +110,7 @@ if hasattr(self, 'toolbar'): self.toolbar.setVisible(False) if not hasattr(self, 'display'): - self.display = base_panels.HTMLTextEditor(options={'enhance_display': False, 'listen_keyboard': False}) # for display mode + self.display = editor_widget.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'): @@ -126,7 +127,7 @@ return if not self.no_title and not hasattr(self, 'title_panel'): - self.title_panel = base_panels.TitlePanel() + self.title_panel = base_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) @@ -286,10 +287,10 @@ """ if not (edit and abort): self.refresh(edit) # not when we are asking for a confirmation - base_panels.BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed + editor_widget.BaseTextEditor.edit(self, edit, abort, sync) # after the UI has been refreshed if (edit and abort): - return # self.abortEdition is called by base_panels.BaseTextEditor.edit - self.setWysiwyg(False, init=True) # after base_panels.BaseTextEditor (it affects self.getContent) + return # self.abortEdition is called by editor_widget.BaseTextEditor.edit + self.setWysiwyg(False, init=True) # after editor_widget.BaseTextEditor (it affects self.getContent) if sync: return # the following must NOT be done at each UI refresh! @@ -319,7 +320,7 @@ self.display.edit(False) def setDisplayContent(self): - """Set the content of the base_panels.HTMLTextEditor which is used for display/wysiwyg""" + """Set the content of the editor_widget.HTMLTextEditor which is used for display/wysiwyg""" content = self._original_content text = content['text'] if 'title' in content and content['title']: diff -r e0021d571eef -r 6d3142b782c3 src/browser/sat_browser/widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/sat_browser/widget.py Thu Feb 26 18:10:54 2015 +0100 @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pyjd # this is dummy in pyjs +from sat.core.log import getLogger +log = getLogger(__name__) + +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.Button import Button +from pyjamas.ui.Frame import Frame +from pyjamas import DOM + + +import dialog +import libervia_widget +from constants import Const as C +from sat_frontends.quick_frontend import quick_widgets + + +# 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.getCachedParam(C.COMPOSITION_KEY, C.ENABLE_UNIBOX_PARAM) == 'true' +# self.setVisible(enable) +# if enable and not self.unibox: +# self.button = Button('') +# self.button.setTitle('Open the rich text editor') +# self.button.addStyleName('uniBoxButton') +# self.add(self.button) +# self.unibox = UniBox(self.host) +# self.add(self.unibox) +# self.setCellWidth(self.unibox, '100%') +# self.button.addClickListener(self.openRichMessageEditor) +# self.unibox.addKey("@@: ") +# self.unibox.onSelectedChange(self.host.getSelected()) +# +# def openRichMessageEditor(self): +# """Open the rich text editor.""" +# self.button.setVisible(False) +# self.unibox.setVisible(False) +# self.setCellWidth(self.unibox, '0px') +# self.host.panel._contactsMove(self) +# +# def afterEditCb(): +# Window.removeWindowResizeListener(self) +# self.host.panel._contactsMove(self.host.panel._hpanel) +# self.setCellWidth(self.unibox, '100%') +# self.button.setVisible(True) +# self.unibox.setVisible(True) +# self.host.resize() +# +# richtext.RichMessageEditor.getOrCreate(self.host, self, afterEditCb) +# Window.addWindowResizeListener(self) +# self.host.resize() +# +# def onWindowResized(self, width, height): +# right = self.host.panel.menu.getAbsoluteLeft() + self.host.panel.menu.getOffsetWidth() +# left = self.host.panel._contacts.getAbsoluteLeft() + self.host.panel._contacts.getOffsetWidth() +# ideal_width = right - left - 40 +# self.host.richtext.setWidth("%spx" % ideal_width) + + + +# class 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') +# # FIXME +# # 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): # FIXME +# # 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 WebWidget(quick_widgets.QuickWidget, libervia_widget.LiberviaWidget): + """ (mini)browser like widget """ + + def __init__(self, host, target, show_url=True, profiles=None): + """ + @param host: SatWebFrontend instance + @param target: url to open + """ + quick_widgets.QuickWidget.__init__(self, host, target, C.PROF_KEY_NONE) + libervia_widget.LiberviaWidget.__init__(self, host) + self._vpanel = VerticalPanel() + self._vpanel.setSize('100%', '100%') + self._url = dialog.ExtTextBox(enter_cb=self.onUrlClick) + self._url.setText(target or "") + self._url.setWidth('100%') + if show_url: + hpanel = HorizontalPanel() + hpanel.add(self._url) + btn = Button("Go", self.onUrlClick) + hpanel.setCellWidth(self._url, "100%") + hpanel.add(btn) + self._vpanel.add(hpanel) + self._vpanel.setCellHeight(hpanel, '20px') + self._frame = Frame(target 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): + url = self._url.getText() + scheme_end = url.find(':') + scheme = "" if scheme_end == -1 else url[:scheme_end] + if scheme not in C.WEB_PANEL_SCHEMES: + url = "http://" + url + self._frame.setUrl(url)