quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p):

This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments:
    - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions
    - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far
    - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour
    - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events
    - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles
    - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid
    - better management of CHAT_GROUP mode for Chat widgets
    - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses
    - no more (un)escapePrivate/PRIVATE_PREFIX
    - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed
    - resources can be displayed in Primitivus, and their status messages
    - profiles are managed in QuickFrontend with dedicated managers


This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
This commit is contained in:
Goffi 2014-12-10 19:00:09 +01:00
commit e4708c9cbb
19 changed files with 1583 additions and 969 deletions

View file

@ -144,8 +144,9 @@ class DBusBridgeFrontend(BridgeFrontend):
def confirmationAnswer(self, id, accepted, data, profile):
return self.db_core_iface.confirmationAnswer(id, accepted, data, profile)
def delContact(self, entity_jid, profile_key="@DEFAULT@"):
return self.db_core_iface.delContact(entity_jid, profile_key)
def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None):
error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
return self.db_core_iface.delContact(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def discoInfos(self, entity_jid, profile_key, callback=None, errback=None):
error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
@ -161,8 +162,9 @@ class DBusBridgeFrontend(BridgeFrontend):
def getConfig(self, section, name):
return unicode(self.db_core_iface.getConfig(section, name))
def getContacts(self, profile_key="@DEFAULT@"):
return self.db_core_iface.getContacts(profile_key)
def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None):
error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
return self.db_core_iface.getContacts(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
def getContactsFromGroup(self, group, profile_key="@DEFAULT@"):
return self.db_core_iface.getContactsFromGroup(group, profile_key)
@ -255,5 +257,6 @@ class DBusBridgeFrontend(BridgeFrontend):
def subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
return self.db_core_iface.subscription(sub_type, entity, profile_key)
def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key)
def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None):
error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))
return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)

View file

@ -23,11 +23,12 @@ log = logging.getLogger(__name__)
import urwid
from urwid_satext import sat_widgets
from urwid_satext.files_management import FileDialog
from sat_frontends.quick_frontend import quick_widgets
from sat_frontends.quick_frontend.quick_chat import QuickChat
from sat_frontends.primitivus.card_game import CardGame
from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
from sat_frontends.primitivus.constants import Const as C
from sat_frontends.primitivus.keys import action_key_map as a_key
from sat_frontends.primitivus.widget import PrimitivusWidget
import time
from sat_frontends.tools.jid import JID
@ -80,19 +81,27 @@ class ChatText(urwid.FlowWidget):
return txt_widget
class Chat(urwid.WidgetWrap, QuickChat):
class Chat(PrimitivusWidget, QuickChat):
def __init__(self, target, host, type_='one2one'):
self.target = target
QuickChat.__init__(self, target, host, type_)
def __init__(self, host, target, type_='one2one', profiles=None):
QuickChat.__init__(self, host, target, type_, profiles=profiles)
self.content = urwid.SimpleListWalker([])
self.text_list = urwid.ListBox(self.content)
self.chat_widget = urwid.Frame(self.text_list)
self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
self.pile = urwid.Pile([self.chat_colums])
urwid.WidgetWrap.__init__(self, self.__getDecoration(self.pile))
self.setType(type_)
PrimitivusWidget.__init__(self, self.pile, self.target)
# we must adapt the behavious with the type
if type_ == 'one2one':
self.historyPrint(profile=self.profile)
elif type_ == 'group':
if len(self.chat_colums.contents) == 1:
present_widget = self._buildPresentList()
self.present_panel = sat_widgets.VerticalSeparator(present_widget)
self._appendPresentPanel()
self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) #struct_time of day changing time
self.show_timestamp = True
self.show_short_nick = False
@ -104,9 +113,9 @@ class Chat(urwid.WidgetWrap, QuickChat):
if self.type == 'group':
widgets = [widget for (widget, options) in self.chat_colums.contents]
if self.present_panel in widgets:
self.__removePresentPanel()
self._removePresentPanel()
else:
self.__appendPresentPanel()
self._appendPresentPanel()
elif key == a_key['TIMESTAMP_HIDE']: #user wants to (un)hide timestamp
self.show_timestamp = not self.show_timestamp
for wid in self.content:
@ -115,10 +124,6 @@ class Chat(urwid.WidgetWrap, QuickChat):
self.show_short_nick = not self.show_short_nick
for wid in self.content:
wid._invalidate()
elif key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
show = not isinstance(self._w, sat_widgets.LabelLine)
self.showDecoration(show)
self._invalidate()
elif key == a_key['SUBJECT_SWITCH']: #user wants to (un)hide group's subject or change its apperance
if self.subject:
self.show_title = (self.show_title + 1) % 3
@ -130,7 +135,6 @@ class Chat(urwid.WidgetWrap, QuickChat):
self.chat_widget.header = None
self._invalidate()
return super(Chat, self).keypress(size, key)
def getMenu(self):
@ -141,56 +145,20 @@ class Chat(urwid.WidgetWrap, QuickChat):
game = _("Game")
menu.addMenu(game, "Tarot", self.onTarotRequest)
elif self.type == 'one2one':
self.host.addMenus(menu, C.MENU_SINGLE, {'jid': unescapePrivate(self.target)})
self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target})
menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest)
return menu
def setType(self, type_):
QuickChat.setType(self, type_)
if type_ == 'one2one':
self.historyPrint(profile=self.host.profile)
elif type_ == 'group':
if len(self.chat_colums.contents) == 1:
present_widget = self.__buildPresentList()
self.present_panel = sat_widgets.VerticalSeparator(present_widget)
self.__appendPresentPanel()
def __getDecoration(self, widget):
return sat_widgets.LabelLine(widget, self.__getSurrendedText())
def __getSurrendedText(self):
"""Get the text to be displayed as the dialog title."""
if not hasattr(self, "surrended_text"):
self.__setSurrendedText()
return self.surrended_text
def __setSurrendedText(self, state=None):
"""Set the text to be displayed as the dialog title
@param stat: chat state of the contact
"""
text = unicode(unescapePrivate(self.target))
if state:
text += " (" + state + ")"
self.surrended_text = sat_widgets.SurroundedText(text)
def showDecoration(self, show=True):
"""Show/Hide the decoration around the chat window"""
if show:
main_widget = self.__getDecoration(self.pile)
else:
main_widget = self.pile
self._w = main_widget
def updateChatState(self, state, nick=None):
"""Set the chat state (XEP-0085) of the contact. Leave nick to None
to set the state for a one2one conversation, or give a nickname or
C.ALL_OCCUPANTS to set the state of a participant within a MUC.
@param state: the new chat state
@param nick: None for one2one, the MUC user nick or C.ALL_OCCUPANTS
"""
if nick:
assert(self.type == 'group')
occupants = self.occupants if nick == C.ALL_OCCUPANTS else [nick]
def updateChatState(self, from_jid, state):
if self.type == C.CHAT_GROUP:
if from_jid == C.ENTITY_ALL:
occupants = self.occupants
else:
nick = from_jid.resource
if not nick:
log.debug("no nick found for chatstate")
return
occupants = [nick]
options = self.present_wid.getAllValues()
for index in xrange(0, len(options)):
nick = options[index].value
@ -199,46 +167,43 @@ class Chat(urwid.WidgetWrap, QuickChat):
self.present_wid.changeValues(options)
self.host.redraw()
else:
assert(self.type == 'one2one')
self.__setSurrendedText(state)
self.showDecoration()
self.host.redraw()
self.title_dynamic = '({})'.format(state)
def _presentClicked(self, list_wid, clicked_wid):
assert(self.type == 'group')
assert self.type == 'group'
nick = clicked_wid.getValue().value
if nick == self.getUserNick():
#We ignore click on our own nick
#We ignore clicks on our own nick
return
#we have a click on a nick, we add the private conversation to the contact_list
contact_list = self.host.contact_lists[self.profile]
full_jid = JID("%s/%s" % (self.target.bare, nick))
new_jid = escapePrivate(full_jid)
if new_jid not in self.host.contact_list:
self.host.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
#we have a click on a nick, we need to create the widget if it doesn't exists
self.getOrCreatePrivateWidget(full_jid)
#now we select the new window
self.host.contact_list.setFocus(full_jid, True)
contact_list.setFocus(full_jid, True)
def __buildPresentList(self):
def _buildPresentList(self):
self.present_wid = sat_widgets.GenericList([],option_type = sat_widgets.ClickableText, on_click=self._presentClicked)
return self.present_wid
def __appendPresentPanel(self):
def _appendPresentPanel(self):
self.chat_colums.contents.append((self.present_panel,('weight', 2, False)))
def __removePresentPanel(self):
def _removePresentPanel(self):
for widget, options in self.chat_colums.contents:
if widget is self.present_panel:
self.chat_colums.contents.remove((widget, options))
break
def __appendGamePanel(self, widget):
def _appendGamePanel(self, widget):
assert (len(self.pile.contents) == 1)
self.pile.contents.insert(0,(widget,('weight', 1)))
self.pile.contents.insert(1,(urwid.Filler(urwid.Divider('-'),('fixed', 1))))
self.host.redraw()
def __removeGamePanel(self):
def _removeGamePanel(self):
assert (len(self.pile.contents) == 3)
del self.pile.contents[0]
self.host.redraw()
@ -290,13 +255,16 @@ class Chat(urwid.WidgetWrap, QuickChat):
self.text_list.focus_position = len(self.content) - 1 # scroll down
self.host.redraw()
def onPrivateCreated(self, widget):
self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
def printMessage(self, from_jid, msg, profile, timestamp=None):
assert isinstance(from_jid, JID)
try:
jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp)
except TypeError:
# None is returned, the message is managed
return
new_text = ChatText(self, timestamp, nick, mymess, msg)
if timestamp and self.content:
@ -357,7 +325,7 @@ class Chat(urwid.WidgetWrap, QuickChat):
"""Configure the chat window to start a game"""
if game_type=="Tarot":
self.tarot_wid = CardGame(self, referee, players, self.nick)
self.__appendGamePanel(self.tarot_wid)
self._appendGamePanel(self.tarot_wid)
def getGame(self, game_type):
"""Return class managing the game type"""
@ -371,7 +339,7 @@ class Chat(urwid.WidgetWrap, QuickChat):
if len(self.occupants) != 4:
self.host.showPopUp(sat_widgets.Alert(_("Can't start game"), _("You need to be exactly 4 peoples in the room to start a Tarot game"), ok_cb=self.host.removePopUp))
else:
self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.host.profile)
self.host.bridge.tarotGameCreate(self.id, list(self.occupants), self.profile)
def onSendFileRequest(self, menu):
# TODO: move this to core with dynamic menus
@ -388,11 +356,14 @@ class Chat(urwid.WidgetWrap, QuickChat):
self.host.showDialog(_(u"File has a unicode error in its name, it's not yet managed by SàT"), title=_("Can't send file"), type_="error")
return
#FIXME: check last_resource: what if self.target.resource exists ?
last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.host.profile)
last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.profile)
if last_resource:
full_jid = JID("%s/%s" % (self.target.bare, last_resource))
else:
full_jid = self.target
progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.host.profile)
progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile)
self.host.addProgress(progress_id,filepath)
self.host.showDialog(_(u"You file request has been sent, we are waiting for your contact answer"), title=_("File request sent"))
quick_widgets.register(QuickChat, Chat)

View file

@ -68,6 +68,8 @@ class Const(constants.Const):
('show_dnd_focus', 'dark red, bold', 'default'),
('show_xa', 'dark red', 'default'),
('show_xa_focus', 'dark red, bold', 'default'),
('resource', 'light blue', 'default'),
('resource_main', 'dark blue', 'default'),
('status', 'yellow', 'default'),
('status_focus', 'yellow, bold', 'default'),
('param_selected','default, bold', 'dark red'),
@ -85,3 +87,15 @@ class Const(constants.Const):
CONFIG_SECTION = APP_NAME.lower()
CONFIG_OPT_KEY_PREFIX = "KEY_"
MENU_ID_MAIN = "MAIN_MENU"
MENU_ID_WIDGET = "WIDGET_MENU"
MODE_NORMAL = 'NORMAL'
MODE_INSERTION = 'INSERTION'
MODE_COMMAND = 'COMMAND'
GROUP_DATA_FOLDED = 'folded'
# contacts and contact list
ALERT_HEADER='(*) '

View file

@ -21,36 +21,25 @@ from sat.core.i18n import _
import urwid
from urwid_satext import sat_widgets
from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
from sat_frontends.quick_frontend.quick_utils import unescapePrivate
from sat_frontends.tools.jid import JID
from sat_frontends.primitivus.status import StatusBar
from sat_frontends.primitivus.constants import Const as C
from sat_frontends.primitivus.keys import action_key_map as a_key
from sat_frontends.primitivus.widget import PrimitivusWidget
from sat_frontends.tools import jid
from sat.core import log as logging
log = logging.getLogger(__name__)
class ContactList(urwid.WidgetWrap, QuickContactList):
class ContactList(PrimitivusWidget, QuickContactList):
signals = ['click','change']
def __init__(self, host, on_click=None, on_change=None, user_data=None):
QuickContactList.__init__(self)
self.host = host
self.selected = None
self.groups={}
self.alert_jid=set()
self.show_status = False
self.show_disconnected = False
self.show_empty_groups = True
# TODO: this may lead to two successive UI refresh and needs an optimization
self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=host.profile, callback=self.showEmptyGroups)
self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=host.profile, callback=self.showOfflineContacts)
def __init__(self, host, on_click=None, on_change=None, user_data=None, profile=None):
QuickContactList.__init__(self, host, profile)
#we now build the widget
self.host.status_bar = StatusBar(host)
self.frame = sat_widgets.FocusFrame(self.__buildList(), None, self.host.status_bar)
self.main_widget = sat_widgets.LabelLine(self.frame, sat_widgets.SurroundedText(_("Contacts")))
urwid.WidgetWrap.__init__(self, self.main_widget)
self.status_bar = StatusBar(host)
self.frame = sat_widgets.FocusFrame(self._buildList(), None, self.status_bar)
PrimitivusWidget.__init__(self, self.frame, _(u'Contacts'))
if on_click:
urwid.connect_signal(self, 'click', on_click, user_data)
if on_change:
@ -59,16 +48,14 @@ class ContactList(urwid.WidgetWrap, QuickContactList):
def update(self):
"""Update display, keep focus"""
widget, position = self.frame.body.get_focus()
self.frame.body = self.__buildList()
self.frame.body = self._buildList()
if position:
try:
self.frame.body.focus_position = position
except IndexError:
pass
self.host.redraw()
def update_jid(self, jid):
self.update()
self._invalidate()
self.host.redraw() # FIXME: check if can be avoided
def keypress(self, size, key):
# FIXME: we have a temporary behaviour here: FOCUS_SWITCH change focus globally in the parent,
@ -81,18 +68,18 @@ class ContactList(urwid.WidgetWrap, QuickContactList):
self.show_status = not self.show_status
self.update()
elif key == a_key['DISCONNECTED_HIDE']: #user wants to (un)hide disconnected contacts
self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.host.profile)
self.host.bridge.setParam(C.SHOW_OFFLINE_CONTACTS, C.str(not self.show_disconnected), "General", profile_key=self.profile)
elif key == a_key['RESOURCES_HIDE']: #user wants to (un)hide contacts resources
self.showResources(not self.show_resources)
self.update()
return super(ContactList, self).keypress(size, key)
def __contains__(self, jid):
for group in self.groups:
if jid.bare in self.groups[group][1]:
return True
return False
# modify the contact list
def setFocus(self, text, select=False):
"""give focus to the first element that matches the given text. You can also
pass in text a sat_frontends.tools.jid.JID (it's a subclass of unicode).
@param text: contact group name, contact or muc userhost, muc private dialog jid
@param select: if True, the element is also clicked
"""
@ -103,12 +90,8 @@ class ContactList(urwid.WidgetWrap, QuickContactList):
# contact group
value = widget.getValue()
elif isinstance(widget, sat_widgets.SelectableText):
if widget.data.startswith(C.PRIVATE_PREFIX):
# muc private dialog
value = widget.getValue()
else:
# contact or muc
value = widget.data
# contact or muc
value = widget.data
else:
# Divider instance
continue
@ -116,243 +99,207 @@ class ContactList(urwid.WidgetWrap, QuickContactList):
if text.strip() == value.strip():
self.frame.body.focus_position = idx
if select:
self.__contactClicked(widget, True)
self._contactClicked(False, widget, True)
return
except AttributeError:
pass
idx += 1
def putAlert(self, jid):
"""Put an alert on the jid to get attention from user (e.g. for new message)"""
self.alert_jid.add(jid.bare)
log.debug(u"Not element found for {} in setFocus".format(text))
def specialResourceVisible(self, entity):
"""Assure a resource of a special entity is visible and clickable
Mainly used to display private conversation in MUC rooms
@param entity: full jid of the resource to show
"""
assert isinstance(entity, jid.JID)
if entity not in self._special_extras:
self._special_extras.add(entity)
self.update()
# events
def _groupClicked(self, group_wid):
group = group_wid.getValue()
data = self.getGroupData(group)
data[C.GROUP_DATA_FOLDED] = not data.setdefault(C.GROUP_DATA_FOLDED, False)
self.setFocus(group)
self.update()
def __groupClicked(self, group_wid):
group = self.groups[group_wid.getValue()]
group[0] = not group[0]
self.update()
self.setFocus(group_wid.getValue())
def _contactClicked(self, use_bare_jid, contact_wid, selected):
"""Method called when a contact is clicked
def __contactClicked(self, contact_wid, selected):
self.selected = contact_wid.data
for widget in self.frame.body.body:
if widget.__class__ == sat_widgets.SelectableText:
widget.setState(widget.data == self.selected, invisible=True)
if self.selected in self.alert_jid:
self.alert_jid.remove(self.selected)
self.host.modeHint('INSERTION')
@param use_bare_jid: True if use_bare_jid is set in self._buildEntityWidget.
If True, all jids in self._alerts with the same bare jid has contact_wid.data will be removed
@param contact_wid: widget of the contact, must have the entity set in data attribute
@param selected: boolean returned by the widget, telling if it is selected
"""
entity = contact_wid.data
if use_bare_jid:
to_remove = set()
for alert_entity in self._alerts:
if alert_entity.bare == entity.bare:
to_remove.add(alert_entity)
self._alerts.difference_update(to_remove)
else:
self._alerts.discard(entity)
self.host.modeHint(C.MODE_INSERTION)
self.update()
self._emit('click')
self._emit('click', entity)
# Methods to build the widget
def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None):
"""Build one contact markup data
@param entity (jid.JID): entity to build
@param keys (iterable): value to markup, in preferred order.
The first available key will be used.
If key starts with "cache_", it will be checked in cache,
else, getattr will be done on entity with the key (e.g. getattr(entity, 'node')).
If nothing full or keys is None, full entity is used.
@param use_bare_jid (bool): if True, use bare jid for alerts and selected comparisons
@param with_alert (bool): if True, show alert if entity is in self._alerts
@param with_show_attr (bool): if True, show color corresponding to presence status
@param markup_prepend (list): markup to prepend to the generated one before building the widget
@param markup_append (list): markup to append to the generated one before building the widget
@return (list): markup data are expected by Urwid text widgets
"""
markup = []
if use_bare_jid:
alerts = {entity.bare for entity in self._alerts}
selected = {entity.bare for entity in self._selected}
else:
alerts = self._alerts
selected = self._selected
if keys is None:
entity_txt = entity
else:
cache = self.getCache(entity)
for key in keys:
if key.startswith('cache_'):
entity_txt = cache.get(key[6:])
else:
entity_txt = getattr(entity, key)
if entity_txt:
break
if not entity_txt:
entity_txt = entity
if with_show_attr:
show = self.getCache(entity, C.PRESENCE_SHOW)
if show is None:
show = C.PRESENCE_UNAVAILABLE
show_icon, entity_attr = C.PRESENCE.get(show, ('', 'default'))
markup.insert(0, u"{} ".format(show_icon))
else:
entity_attr = 'default'
if with_alert and entity in alerts:
entity_attr = 'alert'
header = C.ALERT_HEADER
else:
header = ''
markup.append((entity_attr, entity_txt))
if markup_prepend:
markup.insert(0, markup_prepend)
if markup_append:
markup.extend(markup_append)
widget = sat_widgets.SelectableText(markup,
selected = entity in selected,
header = header)
widget.data = entity
widget.comp = entity_txt.lower() # value to use for sorting
urwid.connect_signal(widget, 'change', self._contactClicked, user_args=[use_bare_jid])
return widget
def _buildEntities(self, content, entities):
"""Add entity representation in widget list
def __buildContact(self, content, contacts):
"""Add contact representation in widget list
@param content: widget list, e.g. SimpleListWalker
@param contacts (list): list of JID userhosts"""
if not contacts:
@param entities (iterable): iterable of JID to display
"""
if not entities:
return
widgets = [] # list of built widgets
for contact in contacts:
if contact.startswith(C.PRIVATE_PREFIX):
contact_disp = ('alert' if contact in self.alert_jid else "show_normal", unescapePrivate(contact))
show_icon = ''
status = ''
for entity in entities:
if entity in self._specials or not self.entityToShow(entity):
continue
markup_extra = []
if self.show_resources:
for resource in self.getCache(entity, C.CONTACT_RESOURCES):
resource_disp = ('resource_main' if resource == self.getCache(entity, C.CONTACT_MAIN_RESOURCE) else 'resource', "\n " + resource)
markup_extra.append(resource_disp)
if self.show_status:
status = self.getCache(jid.JID('%s/%s' % (entity, resource)), 'status')
status_disp = ('status', "\n " + status) if status else ""
markup_extra.append(status_disp)
else:
jid = JID(contact)
name = self.getCache(jid, 'name')
nick = self.getCache(jid, 'nick')
status = self.getCache(jid, 'status')
show = self.getCache(jid, 'show')
if show is None:
show = "unavailable"
if not self.contactToShow(contact):
continue
show_icon, show_attr = C.PRESENCE.get(show, ('', 'default'))
contact_disp = ('alert' if contact in self.alert_jid else show_attr, nick or name or jid.node or jid.bare)
display = [show_icon + " ", contact_disp]
if self.show_status:
status_disp = ('status', "\n " + status) if status else ""
display.append(status_disp)
header = '(*) ' if contact in self.alert_jid else ''
widget = sat_widgets.SelectableText(display,
selected=contact == self.selected,
header=header)
widget.data = contact
widget.comp = contact_disp[1].lower() # value to use for sorting
if self.show_status:
status = self.getCache(entity, 'status')
status_disp = ('status', "\n " + status) if status else ""
markup_extra.append(status_disp)
widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), use_bare_jid=True, markup_append=markup_extra)
widgets.append(widget)
widgets.sort(key=lambda widget: widget.comp)
for widget in widgets:
content.append(widget)
urwid.connect_signal(widget, 'change', self.__contactClicked)
def __buildSpecials(self, content):
def _buildSpecials(self, content):
"""Build the special entities"""
specials = self.specials.keys()
specials = list(self._specials)
specials.sort()
for special in specials:
jid=JID(special)
name = self.getCache(jid, 'name')
nick = self.getCache(jid, 'nick')
special_disp = ('alert' if special in self.alert_jid else 'default', nick or name or jid.node or jid.bare)
display = [ " " , special_disp]
header = '(*) ' if special in self.alert_jid else ''
widget = sat_widgets.SelectableText(display,
selected = special==self.selected,
header=header)
widget.data = special
extra_shown = set()
for entity in specials:
# the special widgets
widget = self._buildEntityWidget(entity, ('cache_nick', 'cache_name', 'node'), with_show_attr=False)
content.append(widget)
urwid.connect_signal(widget, 'change', self.__contactClicked)
def __buildList(self):
# resources which must be displayed (e.g. MUC private conversations)
extras = [extra for extra in self._special_extras if extra.bare == entity.bare]
extras.sort()
for extra in extras:
widget = self._buildEntityWidget(extra, ('resource',), markup_prepend = ' ')
content.append(widget)
extra_shown.add(extra)
# entities which must be visible but not resource of current special entities
for extra in self._special_extras.difference(extra_shown):
widget = self._buildEntityWidget(extra, ('resource',))
content.append(widget)
def _buildList(self):
"""Build the main contact list widget"""
content = urwid.SimpleListWalker([])
self.__buildSpecials(content)
if self.specials:
self._buildSpecials(content)
if self._specials:
content.append(urwid.Divider('='))
group_keys = self.groups.keys()
group_keys.sort(key=lambda x: x.lower() if x else x)
for key in group_keys:
unfolded = self.groups[key][0]
contacts = list(self.groups[key][1])
if key is not None and (self.nonEmptyGroup(contacts) or self.show_empty_groups):
header = '[-]' if unfolded else '[+]'
widget = sat_widgets.ClickableText(key, header=header + ' ')
content.append(widget)
urwid.connect_signal(widget, 'click', self.__groupClicked)
if unfolded:
self.__buildContact(content, contacts)
return urwid.ListBox(content)
def contactToShow(self, contact):
"""Tell if the contact should be showed or hidden.
@param contact (str): JID userhost of the contact
@return: True if that contact should be showed in the list"""
show = self.getCache(JID(contact), 'show')
return (show is not None and show != "unavailable") or \
self.show_disconnected or contact in self.alert_jid or contact == self.selected
def nonEmptyGroup(self, contacts):
"""Tell if a contact group contains some contacts to show.
@param contacts (list[str]): list of JID userhosts
@return: bool
"""
for contact in contacts:
if self.contactToShow(contact):
return True
return False
def unselectAll(self):
"""Unselect all contacts"""
self.selected = None
for widget in self.frame.body.body:
if widget.__class__ == sat_widgets.SelectableText:
widget.setState(False, invisible=True)
def getContact(self):
"""Return contact currently selected"""
return self.selected
def clearContacts(self):
"""clear all the contact list"""
QuickContactList.clearContacts(self)
self.groups={}
self.selected = None
self.unselectAll()
self.update()
def replace(self, jid, groups=None, attributes=None):
"""Add a contact to the list if doesn't exist, else update it.
This method can be called with groups=None for the purpose of updating
the contact's attributes (e.g. nickname). In that case, the groups
attribute must not be set to the default group but ignored. If not,
you may move your contact from its actual group(s) to the default one.
None value for 'groups' has a different meaning than [None] which is for the default group.
@param jid (JID)
@param groups (list): list of groups or None to ignore the groups membership.
@param attributes (dict)
"""
QuickContactList.replace(self, jid, groups, attributes) # eventually change the nickname
if jid.bare in self.specials:
return
if groups is None:
self.update()
return
assert isinstance(jid, JID)
assert isinstance(groups, list)
if groups == []:
groups = [None] # [None] is the default group
for group in [group for group in self.groups if group not in groups]:
try: # remove the contact from a previous group
self.groups[group][1].remove(jid.bare)
except KeyError:
pass
groups = list(self._groups)
groups.sort(key=lambda x: x.lower() if x else x)
for group in groups:
if group not in self.groups:
self.groups[group] = [True, set()] # [unfold, list_of_contacts]
self.groups[group][1].add(jid.bare)
self.update()
data = self.getGroupData(group)
folded = data.get(C.GROUP_DATA_FOLDED, False)
jids = list(data['jids'])
if group is not None and (self.anyEntityToShow(jids) or self.show_empty_groups):
header = '[-]' if not folded else '[+]'
widget = sat_widgets.ClickableText(group, header=header + ' ')
content.append(widget)
urwid.connect_signal(widget, 'click', self._groupClicked)
if not folded:
self._buildEntities(content, jids)
not_in_roster = set(self._cache).difference(self._roster).difference(self._specials)
if not_in_roster:
content.append(urwid.Divider('-'))
self._buildEntities(content, not_in_roster)
def remove(self, jid):
"""remove a contact from the list"""
QuickContactList.remove(self, jid)
groups_to_remove = []
for group in self.groups:
contacts = self.groups[group][1]
if jid.bare in contacts:
contacts.remove(jid.bare)
if not len(contacts):
groups_to_remove.append(group)
for group in groups_to_remove:
del self.groups[group]
self.update()
def add(self, jid, param_groups=None):
"""add a contact to the list"""
self.replace(jid, param_groups if param_groups else [None])
def setSpecial(self, special_jid, special_type, show=False):
"""Set entity as a special
@param special_jid: jid of the entity
@param special_type: special type (e.g.: "MUC")
@param show: True to display the dialog to chat with this entity
"""
QuickContactList.setSpecial(self, special_jid, special_type, show)
if None in self.groups:
folded, group_jids = self.groups[None]
for group_jid in group_jids:
if JID(group_jid).bare == special_jid.bare:
group_jids.remove(group_jid)
break
self.update()
if show:
# also display the dialog for this room
self.setFocus(special_jid, True)
self.host.redraw()
def updatePresence(self, jid, show, priority, statuses):
#XXX: for the moment, we ignore presence updates for special entities
if jid.bare not in self.specials:
QuickContactList.updatePresence(self, jid, show, priority, statuses)
def showOfflineContacts(self, show):
show = C.bool(show)
if self.show_disconnected == show:
return
self.show_disconnected = show
self.update()
def showEmptyGroups(self, show):
show = C.bool(show)
if self.show_empty_groups == show:
return
self.show_empty_groups = show
self.update()
return urwid.ListBox(content)

View file

@ -42,15 +42,18 @@ action_key_map.update(
("menu_global", "APP_QUIT"): 'ctrl x',
("menu_global", "ROOM_JOIN"): 'meta j',
# primitivus widgets
("primitivus_widget", "DECORATION_HIDE"): "meta l",
# contact list
("contact_list", "STATUS_HIDE"): "meta s",
("contact_list", "DISCONNECTED_HIDE"): "meta d",
("contact_list", "RESOURCES_HIDE"): "meta r",
# chat panel
("chat_panel", "OCCUPANTS_HIDE"): "meta p",
("chat_panel", "TIMESTAMP_HIDE"): "meta t",
("chat_panel", "SHORT_NICKNAME"): "meta n",
("chat_panel", "DECORATION_HIDE"): "meta l",
("chat_panel", "SUBJECT_SWITCH"): "meta s",
#card game

View file

@ -28,8 +28,8 @@ import urwid
from urwid_satext import sat_widgets
from urwid_satext.files_management import FileDialog
from sat_frontends.quick_frontend.quick_app import QuickApp
from sat_frontends.quick_frontend.quick_chat_list import QuickChatList
from sat_frontends.quick_frontend.quick_utils import getNewPath, unescapePrivate
from sat_frontends.quick_frontend.quick_utils import getNewPath
from sat_frontends.quick_frontend import quick_chat
from sat_frontends.primitivus.profile_manager import ProfileManager
from sat_frontends.primitivus.contact_list import ContactList
from sat_frontends.primitivus.chat import Chat
@ -39,35 +39,28 @@ from sat_frontends.primitivus.notify import Notify
from sat_frontends.primitivus.keys import action_key_map as a_key
from sat_frontends.primitivus import config
from sat_frontends.tools.misc import InputHistory
from sat_frontends.constants import Const as commonConst # FIXME
from sat_frontends.tools.jid import JID
from sat_frontends.tools import jid
from os.path import join
import signal
class ChatList(QuickChatList):
"""This class manage the list of chat windows"""
def createChat(self, target):
return Chat(target, self.host)
class EditBar(sat_widgets.ModalEdit):
"""
The modal edit bar where you would enter messages and commands.
"""
def __init__(self, app):
modes = {None: ('NORMAL', u''),
a_key['MODE_INSERTION']: ('INSERTION', u'> '),
a_key['MODE_COMMAND']: ('COMMAND', u':')} #XXX: captions *MUST* be unicode
def __init__(self, host):
modes = {None: (C.MODE_NORMAL, u''),
a_key['MODE_INSERTION']: (C.MODE_INSERTION, u'> '),
a_key['MODE_COMMAND']: (C.MODE_COMMAND, u':')} #XXX: captions *MUST* be unicode
super(EditBar, self).__init__(modes)
self.app = app
self.host = host
self.setCompletionMethod(self._text_completion)
urwid.connect_signal(self, 'click', self.onTextEntered)
def _text_completion(self, text, completion_data, mode):
if mode == 'INSERTION':
if mode == C.MODE_INSERTION:
return self._nick_completion(text, completion_data)
else:
return text
@ -75,9 +68,9 @@ class EditBar(sat_widgets.ModalEdit):
def _nick_completion(self, text, completion_data):
"""Completion method which complete pseudo in group chat
for params, see AdvancedEdit"""
contact = self.app.contact_list.getContact() ###Based on the fact that there is currently only one contact selectable at once
contact = self.host.contact_list.getContact() ###Based on the fact that there is currently only one contact selectable at once
if contact:
chat = self.app.chat_wins[contact]
chat = self.host.chat_wins[contact]
if chat.type != "group":
return text
space = text.rfind(" ")
@ -98,18 +91,17 @@ class EditBar(sat_widgets.ModalEdit):
def onTextEntered(self, editBar):
"""Called when text is entered in the main edit bar"""
if self.mode == 'INSERTION':
contact = self.app.contact_list.getContact() ###Based on the fact that there is currently only one contact selectableat once
if contact:
chat = self.app.chat_wins[contact]
self.app.sendMessage(contact,
if self.mode == C.MODE_INSERTION:
if isinstance(self.host.selected_widget, quick_chat.QuickChat):
chat_widget = self.host.selected_widget
self.host.sendMessage(chat_widget.target,
editBar.get_edit_text(),
mess_type = "groupchat" if chat.type == 'group' else "chat",
errback=lambda failure: self.app.notify(_("Error while sending message (%s)") % failure),
profile_key=self.app.profile
mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
profile_key=chat_widget.profile
)
editBar.set_edit_text('')
elif self.mode == 'COMMAND':
elif self.mode == C.MODE_COMMAND:
self.commandHandler()
def commandHandler(self):
@ -118,42 +110,44 @@ class EditBar(sat_widgets.ModalEdit):
tokens = self.get_edit_text().split(' ')
command, args = tokens[0], tokens[1:]
if command == 'quit':
self.app.onExit()
self.host.onExit()
raise urwid.ExitMainLoop()
elif command == 'messages':
wid = sat_widgets.GenericList(logging.memoryGet())
self.app.addWindow(wid)
elif command == 'presence':
values = [value for value in commonConst.PRESENCE.keys()]
values = [value if value else 'online' for value in values] # the empty value actually means 'online'
if args and args[0] in values:
presence = '' if args[0] == 'online' else args[0]
self.app.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
else:
self.app.status_bar.onPresenceClick()
elif command == 'status':
if args:
self.app.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
else:
self.app.status_bar.onStatusClick()
self.host.selectWidget(wid)
# elif command == 'presence':
# values = [value for value in commonConst.PRESENCE.keys()]
# values = [value if value else 'online' for value in values] # the empty value actually means 'online'
# if args and args[0] in values:
# presence = '' if args[0] == 'online' else args[0]
# self.host.status_bar.onChange(user_data=sat_widgets.ClickableText(commonConst.PRESENCE[presence]))
# else:
# self.host.status_bar.onPresenceClick()
# elif command == 'status':
# if args:
# self.host.status_bar.onChange(user_data=sat_widgets.AdvancedEdit(args[0]))
# else:
# self.host.status_bar.onStatusClick()
elif command == 'history':
try:
limit = int(args[0])
except (IndexError, ValueError):
limit = 50
win = self.app.chat_wins[JID(self.app.contact_list.selected).bare]
win.clearHistory()
if limit > 0:
win.historyPrint(size=limit, profile=self.app.profile)
widget = self.host.selected_widget
if isinstance(widget, quick_chat.QuickChat):
try:
limit = int(args[0])
except (IndexError, ValueError):
limit = 50
widget.clearHistory()
if limit > 0:
widget.historyPrint(size=limit, profile=widget.profile)
elif command == 'search':
pattern = " ".join(args)
if not pattern:
self.app.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
win = self.app.chat_wins[JID(self.app.contact_list.selected).bare]
win.clearHistory()
win.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, timestamp=0)
win.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=self.app.profile)
win.printInfo(D_("Type ':history <lines>' to reset the chat history"))
widget = self.host.selected_widget
if isinstance(widget, quick_chat.QuickChat):
pattern = " ".join(args)
if not pattern:
self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
widget.clearHistory()
widget.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, timestamp=0)
widget.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
widget.printInfo(D_("Type ':history <lines>' to reset the chat history"))
else:
return
self.set_edit_text('')
@ -167,22 +161,24 @@ class EditBar(sat_widgets.ModalEdit):
and move the index of the temporary history stack."""
if key == a_key['MODAL_ESCAPE']:
# first save the text to the current mode, then change to NORMAL
self.app._updateInputHistory(self.get_edit_text(), mode=self.mode)
self.app._updateInputHistory(mode='NORMAL')
if self._mode == 'NORMAL' and key in self._modes:
self.app._updateInputHistory(mode=self._modes[key][0])
self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
self.host._updateInputHistory(mode=C.MODE_NORMAL)
if self._mode == C.MODE_NORMAL and key in self._modes:
self.host._updateInputHistory(mode=self._modes[key][0])
if key == a_key['HISTORY_PREV']:
self.app._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
self.host._updateInputHistory(self.get_edit_text(), -1, self._historyCb, self.mode)
return
elif key == a_key['HISTORY_NEXT']:
self.app._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
self.host._updateInputHistory(self.get_edit_text(), +1, self._historyCb, self.mode)
return
elif key == a_key['EDIT_ENTER']:
self.app._updateInputHistory(self.get_edit_text(), mode=self.mode)
self.host._updateInputHistory(self.get_edit_text(), mode=self.mode)
else:
contact = self.app.contact_list.getContact()
if contact:
self.app.bridge.chatStateComposing(unescapePrivate(contact), self.app.profile)
if (self._mode == C.MODE_INSERTION
and isinstance(self.host.selected_widget, quick_chat.QuickChat)
and key not in sat_widgets.FOCUS_KEYS):
self.host.bridge.chatStateComposing(self.host.selected_widget.target, self.host.selected_widget.profile)
return super(EditBar, self).keypress(size, key)
@ -287,12 +283,13 @@ class PrimitivusApp(QuickApp, InputHistory):
self.main_widget = ProfileManager(self)
self.loop = urwid.MainLoop(self.main_widget, C.PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
##misc setup##
self.chat_wins = ChatList(self)
self.notif_bar = sat_widgets.NotificationBar()
urwid.connect_signal(self.notif_bar, 'change', self.onNotification)
self.progress_wid = Progress(self)
urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.addWindow(self.progress_wid))
self.progress_wid = self.widgets.getOrCreateWidget(Progress, None, on_new_widget=None)
urwid.connect_signal(self.notif_bar.progress, 'click', lambda x: self.selectWidget(self.progress_wid))
self.__saved_overlay = None
self.x_notify = Notify()
@ -351,7 +348,7 @@ class PrimitivusApp(QuickApp, InputHistory):
self._early_popup = popup
else:
self.showPopUp(popup)
super(PrimitivusApp, self).postInit()
super(PrimitivusApp, self).postInit(self.main_widget)
def inputFilter(self, input_, raw):
if self.__saved_overlay and input_ != a_key['OVERLAY_HIDE']:
@ -387,14 +384,14 @@ class PrimitivusApp(QuickApp, InputHistory):
elif input_ == a_key['DEBUG'] and 'D' in self.bridge.getVersion(): #Debug only for dev versions
self.debug()
elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact_list
elif input_ == a_key['CONTACTS_HIDE']: #user wants to (un)hide the contact lists
try:
for wid, options in self.center_part.contents:
if self.contact_list is wid:
if self.contact_lists_pile is wid:
self.center_part.contents.remove((wid, options))
break
else:
self.center_part.contents.insert(0, (self.contact_list, ('weight', 2, False)))
self.center_part.contents.insert(0, (self.contact_lists_pile, ('weight', 2, False)))
except AttributeError:
#The main widget is not built (probably in Profile Manager)
pass
@ -413,17 +410,18 @@ class PrimitivusApp(QuickApp, InputHistory):
except AttributeError:
return input_
def addMenus(self, menu, type_, menu_data=None):
def addMenus(self, menu, type_filter, menu_data=None):
"""Add cached menus to instance
@param menu: sat_widgets.Menu instance
@param type_: menu type like is sat.core.sat_main.importMenu
@param type_filter: menu type like is sat.core.sat_main.importMenu
@param menu_data: data to send with these menus
"""
menus = self.profiles[self.profile]['menus'].get(type_,[])
def add_menu_cb(callback_id):
self.launchAction(callback_id, menu_data, profile_key = self.profile)
for id_, path, path_i18n in menus:
self.launchAction(callback_id, menu_data, profile=self.current_profile)
for id_, type_, path, path_i18n in self.bridge.getMenus("", C.NO_SECURITY_LIMIT ):
if type_ != type_filter:
continue
if len(path) != 2:
raise NotImplementedError("Menu with a path != 2 are not implemented yet")
menu.addMenu(path_i18n[0], path_i18n[1], lambda dummy,id_=id_: add_menu_cb(id_))
@ -445,23 +443,28 @@ class PrimitivusApp(QuickApp, InputHistory):
#FIXME: do this in a more generic way (in quickapp)
self.addMenus(menu, C.MENU_GLOBAL)
menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)])
menu_roller = sat_widgets.MenuRoller([(_('Main menu'), menu, C.MENU_ID_MAIN)])
return menu_roller
def _buildMainWidget(self):
self.contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw())
#self.center_part = urwid.Columns([('weight',2,self.contact_list),('weight',8,Chat('',self))])
self.center_part = urwid.Columns([('weight', 2, self.contact_list), ('weight', 8, urwid.Filler(urwid.Text('')))])
self.contact_lists_pile = urwid.Pile([])
#self.center_part = urwid.Columns([('weight',2,self.contact_lists[profile]),('weight',8,Chat('',self))])
self.center_part = urwid.Columns([('weight', 2, self.contact_lists_pile), ('weight', 8, urwid.Filler(urwid.Text('')))])
self.editBar = EditBar(self)
self.menu_roller = self._buildMenuRoller()
self.main_widget = PrimitivusTopWidget(self.center_part, self.menu_roller, self.notif_bar, self.editBar)
return self.main_widget
def plug_profile_1(self, profile_key='@DEFAULT@'):
def addContactList(self, profile):
contact_list = ContactList(self, on_click=self.contactSelected, on_change=lambda w: self.redraw(), profile=profile)
self.contact_lists_pile.contents.append((contact_list, ('weight', 1)))
self.contact_lists[profile] = contact_list
return contact_list
def plugging_profiles(self):
self.loop.widget = self._buildMainWidget()
self.redraw()
QuickApp.plug_profile_1(self, profile_key)
try:
# if a popup arrived before main widget is build, we need to show it now
self.showPopUp(self._early_popup)
@ -492,15 +495,32 @@ class PrimitivusApp(QuickApp, InputHistory):
self.notif_bar.addMessage(message)
self.redraw()
def addWindow(self, widget):
"""Display a window if possible,
def newWidget(self, widget):
"""Display a widget if possible,
else add it in the notification bar queue
@param widget: BoxWidget"""
assert(len(self.center_part.widget_list)<=2)
@param widget: BoxWidget
"""
self.selectWidget(widget)
def selectWidget(self, widget):
assert len(self.center_part.widget_list)<=2
wid_idx = len(self.center_part.widget_list)-1
self.center_part.widget_list[wid_idx] = widget
self.menu_roller.removeMenu(_('Chat menu'))
self.contact_list.unselectAll()
try:
self.menu_roller.removeMenu(C.MENU_ID_WIDGET)
except KeyError:
log.debug("No menu to delete")
self.selected_widget = widget
self.visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
for contact_list in self.contact_lists.itervalues():
contact_list.unselectAll()
for wid in self.visible_widgets:
if isinstance(wid, Chat):
contact_list = self.contact_lists[wid.profile]
contact_list.select(wid.target)
self.redraw()
def removeWindow(self):
@ -522,23 +542,29 @@ class PrimitivusApp(QuickApp, InputHistory):
"""Set the progression shown in notification bar"""
self.notif_bar.setProgress(percentage)
def contactSelected(self, contact_list):
contact = contact_list.getContact()
if contact:
assert(len(self.center_part.widget_list)==2)
self.center_part.widget_list[1] = self.chat_wins[contact]
self.menu_roller.addMenu(_('Chat menu'), self.chat_wins[contact].getMenu())
def contactSelected(self, contact_list, entity):
if entity.resource:
# we have clicked on a private MUC conversation
chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, force_hash = Chat.getPrivateHash(contact_list.profile, entity), profile=contact_list.profile)
else:
chat_widget = self.widgets.getOrCreateWidget(Chat, entity, on_new_widget=None, profile=contact_list.profile)
self.selectWidget(chat_widget)
self.menu_roller.addMenu(_('Chat menu'), chat_widget.getMenu(), C.MENU_ID_WIDGET)
def newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile):
QuickApp.newMessageHandler(self, from_jid, to_jid, msg, _type, extra, profile)
def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
QuickApp.newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile)
if not from_jid in self.contact_list and from_jid.bare != self.profiles[profile]['whoami'].bare:
if not from_jid in self.contact_lists[profile] and from_jid.bare != self.profiles[profile].whoami.bare:
#XXX: needed to show entities which haven't sent any
# presence information and which are not in roster
self.contact_list.replace(from_jid, [C.GROUP_NOT_IN_ROSTER])
if self.contact_list.selected is None or JID(self.contact_list.selected).bare != from_jid.bare:
self.contact_list.putAlert(from_jid)
self.contact_lists[profile].setContact(from_jid)
visible = False
for widget in self.visible_widgets:
if isinstance(widget, Chat) and widget.manageMessage(from_jid, type_):
visible = True
break
if not visible:
self.contact_lists[profile].setAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
def _dialogOkCb(self, widget, data):
self.removePopUp()
@ -552,7 +578,6 @@ class PrimitivusApp(QuickApp, InputHistory):
answer_data = [data[1]] if data[1] else []
answer_cb(False, *answer_data)
def showDialog(self, message, title="", type_="info", answer_cb = None, answer_data = None):
if type_ == 'info':
popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
@ -578,11 +603,15 @@ class PrimitivusApp(QuickApp, InputHistory):
else:
self.main_widget.show('notif_bar')
def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
""" Launch a dynamic action
@param callback_id: id of the action to launch
@param data: data needed only for certain actions
@param profile_key: %(doc_profile_key)s
@param callback: if not None and 'validated' key is present, it will be called with the following parameters:
- callback_id
- data
- profile
@param profile: %(doc_profile)s
"""
if data is None:
@ -593,24 +622,20 @@ class PrimitivusApp(QuickApp, InputHistory):
# action was a one shot, nothing to do
pass
elif "xmlui" in data:
ui = xmlui.create(self, xml_data=data['xmlui'])
ui = xmlui.create(self, xml_data=data['xmlui'], callback=callback, profile=profile)
ui.show()
elif "authenticated_profile" in data:
assert("caller" in data)
if data["caller"] == "profile_manager":
assert(isinstance(self.main_widget, ProfileManager))
self.main_widget.getXMPPParams(data['authenticated_profile'])
elif data["caller"] == "plug_profile":
self.plug_profile_1(data['authenticated_profile'])
else:
raise NotImplementedError
elif 'validated' in data:
pass # this key is managed below
else:
self.showPopUp(sat_widgets.Alert(_("Error"), _(u"Unmanaged action result"), ok_cb=self.removePopUp))
if callback and 'validated' in data:
callback(callback_id, data, profile)
def action_eb(failure):
self.showPopUp(sat_widgets.Alert(failure.fullname, failure.message, ok_cb=self.removePopUp))
self.bridge.launchAction(callback_id, data, profile_key, callback=action_cb, errback=action_eb)
self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=action_eb)
def askConfirmationHandler(self, confirmation_id, confirmation_type, data, profile):
answer_data={}
@ -684,23 +709,30 @@ class PrimitivusApp(QuickApp, InputHistory):
log.error (_("FIXME FIXME FIXME: type [%s] not implemented") % type_)
raise NotImplementedError
def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile)
self.contact_lists[profile].setFocus(jid.JID(room_jid_s), True)
##DIALOGS CALLBACKS##
def onJoinRoom(self, button, edit):
self.removePopUp()
room_jid = JID(edit.get_edit_text())
room_jid = jid.JID(edit.get_edit_text())
if room_jid.is_valid():
self.bridge.joinMUC(room_jid, self.profiles[self.profile]['whoami'].node, {}, self.profile)
self.bridge.joinMUC(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile)
else:
message = _("'%s' is an invalid JID !") % room_jid
message = _("'%s' is an invalid jid.JID !") % room_jid
log.error (message)
self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp))
#MENU EVENTS#
def onConnectRequest(self, menu):
QuickApp.asyncConnect(self, self.profile)
QuickApp.asyncConnect(self, self.current_profile)
def onDisconnectRequest(self, menu):
self.bridge.disconnect(self.profile)
self.bridge.disconnect(self.current_profile)
def onParam(self, menu):
def success(params):
@ -709,7 +741,7 @@ class PrimitivusApp(QuickApp, InputHistory):
def failure(error):
self.showPopUp(sat_widgets.Alert(_("Error"), _("Can't get parameters (%s)") % error, ok_cb=self.removePopUp))
self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.profile, callback=success, errback=failure)
self.bridge.getParamsUI(app=C.APP_NAME, profile_key=self.current_profile, callback=success, errback=failure)
def onExitRequest(self, menu):
QuickApp.onExit(self)
@ -725,12 +757,12 @@ class PrimitivusApp(QuickApp, InputHistory):
#MISC CALLBACKS#
def setStatusOnline(self, online=True, show="", statuses={}):
def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
if not online or not statuses:
self.status_bar.setPresenceStatus(show if online else 'unavailable', '')
self.contact_lists[profile].status_bar.setPresenceStatus(show if online else 'unavailable', '')
return
try:
self.status_bar.setPresenceStatus(show, statuses['default'])
self.contact_lists[profile].status_bar.setPresenceStatus(show, statuses['default'])
except (KeyError, TypeError):
pass

View file

@ -18,29 +18,69 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.i18n import _
from sat.core import log as logging
log = logging.getLogger(__name__)
from sat_frontends.primitivus.constants import Const as C
import urwid
from urwid_satext.sat_widgets import AdvancedEdit, Password, List, InputDialog, ConfirmDialog, Alert
from sat_frontends.primitivus.keys import action_key_map as a_key
from urwid_satext import sat_widgets
import urwid
class ProfileRecord(object):
def __init__(self, profile=None, login=None, password=None):
self._profile = profile
self._login = login
self._password = password
@property
def profile(self):
return self._profile
@profile.setter
def profile(self, value):
self._profile = value
# if we change the profile,
# we must have no login/password until backend give them
self._login = self._password = None
@property
def login(self):
return self._login
@login.setter
def login(self, value):
self._login = value
@property
def password(self):
return self._password
@password.setter
def password(self, value):
self._password = value
class ProfileManager(urwid.WidgetWrap):
"""Class with manage profiles creation/deletion/connection"""
def __init__(self, host):
def __init__(self, host, autoconnect=None):
"""Create the manager
@param host: %(doc_host)s
@param autoconnect(iterable): list of profiles to connect automatically
"""
self.host = host
#profiles list
self._autoconnect = bool(autoconnect)
self.current = ProfileRecord()
profiles = self.host.bridge.getProfilesList()
profiles.sort()
#login & password box must be created before list because of onProfileChange
self.login_wid = AdvancedEdit(_('Login:'), align='center')
self.pass_wid = Password(_('Password:'), align='center')
self.login_wid = sat_widgets.AdvancedEdit(_('Login:'), align='center')
self.pass_wid = sat_widgets.Password(_('Password:'), align='center')
self.selected_profile = None # allow to reselect the previous selection until the profile is authenticated
style = ['single']
if self.host.options.profile:
style.append('no_first_select')
self.list_profile = List(profiles, style=style, align='center', on_change=self.onProfileChange)
style = ['no_first_select']
self.list_profile = sat_widgets.List(profiles, style=style, align='center', on_change=self.onProfileChange)
#new & delete buttons
buttons = [urwid.Button(_("New"), self.onNewProfile),
@ -51,14 +91,29 @@ class ProfileManager(urwid.WidgetWrap):
divider = urwid.Divider('-')
#connect button
connect_button = urwid.Button(_("Connect"), self.onConnectProfile)
connect_button = sat_widgets.CustomButton(_("Connect"), self.onConnectProfiles, align='center')
#we now build the widget
list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile,divider,self.login_wid, self.pass_wid, connect_button])
list_walker = urwid.SimpleFocusListWalker([buttons_flow,self.list_profile, divider, self.login_wid, self.pass_wid, connect_button])
frame_body = urwid.ListBox(list_walker)
frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Profile Manager"),align='center'),'title'))
self.main_widget = urwid.LineBox(frame)
urwid.WidgetWrap.__init__(self, self.main_widget)
if self._autoconnect:
self.autoconnect(autoconnect)
def autoconnect(self, profile_keys):
"""Automatically connect profiles
@param profile_keys(iterable): list of profile keys to connect
"""
if not profile_keys:
log.warning("No profile given to autoconnect")
return
self._autoconnect = True
self._autoconnect_profiles=[]
self._do_autoconnect(profile_keys)
def keypress(self, size, key):
if key == a_key['APP_QUIT']:
@ -80,11 +135,41 @@ class ProfileManager(urwid.WidgetWrap):
return
return super(ProfileManager, self).keypress(size, key)
def __refillProfiles(self):
def _do_autoconnect(self, profile_keys):
"""Connect automatically given profiles
@param profile_kes(iterable): profiles to connect
"""
assert self._autoconnect
def authenticate_cb(callback_id, data, profile):
if C.bool(data['validated']):
self._autoconnect_profiles.append(profile)
if len(self._autoconnect_profiles) == len(profile_keys):
# all the profiles have been validated
self.host.plug_profiles(self._autoconnect_profiles)
else:
# a profile is not validated, we go to manual mode
self._autoconnect=False
for profile_key in profile_keys:
profile = self.host.bridge.getProfileName(profile_key)
if not profile:
self._autoconnect = False # manual mode
msg = _("Trying to plug an unknown profile key ({})".format(profile_key))
log.warning(msg)
popup = sat_widgets.Alert(_("Profile plugging in error"), msg, ok_cb=self.host.removePopUp)
self.host.showPopUp(popup)
break
self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=profile)
def refillProfiles(self):
"""Update the list of profiles"""
profiles = self.host.bridge.getProfilesList()
profiles.sort()
self.list_profile.changeValues(profiles)
self.host.redraw()
def cancelDialog(self, button):
self.host.removePopUp()
@ -92,105 +177,120 @@ class ProfileManager(urwid.WidgetWrap):
def newProfile(self, button, edit):
"""Create the profile"""
name = edit.get_edit_text()
self.host.bridge.asyncCreateProfile(name, callback=lambda: self._newProfileCreated(name), errback=self._profileCreationFailure)
self.host.bridge.asyncCreateProfile(name, callback=lambda: self.newProfileCreated(name), errback=self.profileCreationFailure)
def _newProfileCreated(self, name):
self.__refillProfiles()
#We select the profile created in the list
self.list_profile.selectValue(name)
def newProfileCreated(self, profile):
self.host.removePopUp()
self.refillProfiles()
self.list_profile.selectValue(profile)
self.current.profile=profile
self.getConnectionParams(profile)
self.host.redraw()
def _profileCreationFailure(self, reason):
def profileCreationFailure(self, reason):
self.host.removePopUp()
if reason == "ConflictError":
message = _("A profile with this name already exists")
elif reason == "CancelError":
message = _("Profile creation cancelled by backend")
elif reason == "ValueError":
message = _("You profile name is not valid") # TODO: print a more informative message (empty name, name starting with '@')
else:
message = _("Unknown reason (%s)") % reason
popup = Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
message = _("Can't create profile ({})").format(reason)
popup = sat_widgets.Alert(_("Can't create profile"), message, ok_cb=self.host.removePopUp)
self.host.showPopUp(popup)
def deleteProfile(self, button):
profile_name = self.list_profile.getSelectedValue()
if profile_name:
self.host.bridge.asyncDeleteProfile(profile_name, callback=self.__refillProfiles)
if self.current.profile:
self.host.bridge.asyncDeleteProfile(self.current.profile, callback=self.refillProfiles)
self.resetFields()
self.host.removePopUp()
def onNewProfile(self, e):
pop_up_widget = InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
pop_up_widget = sat_widgets.InputDialog(_("New profile"), _("Please enter a new profile name"), cancel_cb=self.cancelDialog, ok_cb=self.newProfile)
self.host.showPopUp(pop_up_widget)
def onDeleteProfile(self, e):
pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
self.host.showPopUp(pop_up_widget)
if self.current.profile:
pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the profile {} ?").format(self.current.profile), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
self.host.showPopUp(pop_up_widget)
def getXMPPParams(self, profile):
"""This is called from PrimitivusApp.launchAction when the profile has been authenticated.
def resetFields(self):
"""Set profile to None, and reset fields"""
self.current.profile=None
self.login_wid.set_edit_text("")
self.pass_wid.set_edit_text("")
self.list_profile.unselectAll(invisible=True)
def getConnectionParams(self, profile):
"""Get login and password and display them
@param profile: %(doc_profile)s
"""
def setJID(jabberID):
self.login_wid.set_edit_text(jabberID)
self.host.redraw()
self.current.login = jabberID
self.host.redraw() # FIXME: redraw should be avoided
def setPassword(password):
self.pass_wid.set_edit_text(password)
self.current.password = password
self.host.redraw()
self.list_profile.selectValue(profile, move_focus=False)
self.selected_profile = profile
self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile, callback=setJID, errback=self.getParamError)
self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile, callback=setPassword, errback=self.getParamError)
def updateConnectionParams(self):
"""Check if connection parameters have changed, and update them if so"""
if self.current.profile:
login = self.login_wid.get_edit_text()
password = self.pass_wid.get_edit_text()
if login != self.current.login and self.current.login is not None:
self.current.login = login
self.host.bridge.setParam("JabberID", login, "Connection", profile_key=self.current.profile)
log.info("login updated for profile [{}]".format(self.current.profile))
if password != self.current.password and self.current.password is not None:
self.current.password = password
self.host.bridge.setParam("Password", password, "Connection", profile_key=self.current.profile)
log.info("password updated for profile [{}]".format(self.current.profile))
def onProfileChange(self, list_wid):
"""This is called when a profile is selected in the profile list.
@param list_wid: the List widget who sent the event
"""
profile_name = list_wid.getSelectedValue()
if not profile_name or profile_name == self.selected_profile:
return # avoid infinite loop
if self.selected_profile:
list_wid.selectValue(self.selected_profile, move_focus=False)
else:
list_wid.unselectAll(invisible=True)
self.host.redraw()
self.host.profile = profile_name # FIXME: EXTREMELY DIRTY, needed for sat_frontends.tools.xmlui.XMLUI._xmluiLaunchAction
self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'profile_manager'}, profile_key=profile_name)
self.updateConnectionParams()
focused = list_wid.focus
selected = focused.getState()
if not selected: # profile was just unselected
return
focused.setState(False, invisible=True) # we don't want the widget to be selected until we are sure we can access it
def authenticate_cb(callback_id, data, profile):
if C.bool(data['validated']):
self.current.profile = profile
focused.setState(True, invisible=True)
self.getConnectionParams(profile)
self.host.redraw()
self.host.launchAction(C.AUTHENTICATE_PROFILE_ID, callback=authenticate_cb, profile=focused.text)
def onConnectProfile(self, button):
profile_name = self.list_profile.getSelectedValue()
assert(profile_name == self.selected_profile) # if not, there's a bug somewhere...
if not profile_name:
pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog)
def onConnectProfiles(self, button):
"""Connect the profiles and start the main widget
@param button: the connect button
"""
if self._autoconnect:
pop_up_widget = sat_widgets.Alert(_('Internal error'), _('You can connect manually and automatically at the same time'), ok_cb=self.cancelDialog)
self.host.showPopUp(pop_up_widget)
elif profile_name[0] == '@':
pop_up_widget = Alert(_('Bad profile name'), _("A profile name can't start with a @"), ok_cb=self.cancelDialog)
return
self.updateConnectionParams()
profiles = self.list_profile.getSelectedValues()
if not profiles:
pop_up_widget = sat_widgets.Alert(_('No profile selected'), _('You need to create and select at least one profile before connecting'), ok_cb=self.cancelDialog)
self.host.showPopUp(pop_up_widget)
else:
profile = self.host.bridge.getProfileName(profile_name)
assert(profile)
#TODO: move this to quick_app
self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
callback=lambda old_jid: self.__old_jidReceived(old_jid, profile), errback=self.getParamError)
# All profiles in the list are already validated, so we can plug them directly
self.host.plug_profiles(profiles)
def __old_jidReceived(self, old_jid, profile):
self.host.bridge.asyncGetParamA("Password", "Connection", profile_key=profile,
callback=lambda old_pass: self.__old_passReceived(old_jid, old_pass, profile), errback=self.getParamError)
def __old_passReceived(self, old_jid, old_pass, profile):
"""Check if we have new jid/pass, save them if it is the case, and plug profile"""
new_jid = self.login_wid.get_edit_text()
new_pass = self.pass_wid.get_edit_text()
if old_jid != new_jid:
self.host.bridge.setParam("JabberID", new_jid, "Connection", profile_key=profile)
if old_pass != new_pass:
self.host.bridge.setParam("Password", new_pass, "Connection", profile_key=profile)
self.host.plug_profile(profile)
def getParamError(self, ignore):
popup = Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
def getParamError(self, dummy):
popup = sat_widgets.Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
self.host.showPopUp(popup)

View file

@ -20,11 +20,15 @@
from sat.core.i18n import _
import urwid
from urwid_satext import sat_widgets
from sat_frontends.quick_frontend import quick_widgets
class Progress(urwid.WidgetWrap):
class Progress(urwid.WidgetWrap, quick_widgets.QuickWidget):
PROFILES_ALLOW_NONE = True
def __init__(self, host):
def __init__(self, host, target, profiles):
assert target is None and profiles is None
quick_widgets.QuickWidget.__init__(self, host, target)
self.host = host
self.progress_list = urwid.SimpleListWalker([])
self.progress_dict = {}

View file

@ -0,0 +1,102 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Primitivus: a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core import log as logging
log = logging.getLogger(__name__)
import urwid
from urwid_satext import sat_widgets
from sat_frontends.primitivus.keys import action_key_map as a_key
class PrimitivusWidget(urwid.WidgetWrap):
"""Base widget for Primitivus"""
def __init__(self, w, title=''):
self._title = title
self._title_dynamic = None
self._original_widget = w
urwid.WidgetWrap.__init__(self, self._getDecoration(w))
@property
def title(self):
"""Text shown in title bar of the widget"""
# profiles currently managed by frontend
try:
all_profiles = self.host.profiles
except AttributeError:
all_profiles = []
# profiles managed by the widget
try:
profiles = self.profiles
except AttributeError:
try:
profiles = [self.profile]
except AttributeError:
profiles = []
title_elts = []
if self._title:
title_elts.append(self._title)
if self._title_dynamic:
title_elts.append(self._title_dynamic)
if len(all_profiles)>1 and profiles:
title_elts.append(u'[{}]'.format(u', '.join(profiles)))
return sat_widgets.SurroundedText(u' '.join(title_elts))
@title.setter
def title(self, value):
self._title = value
if self.decorationVisible:
self.showDecoration()
@property
def title_dynamic(self):
"""Dynamic part of title"""
return self._title_dynamic
@title_dynamic.setter
def title_dynamic(self, value):
self._title_dynamic = value
if self.decorationVisible:
self.showDecoration()
@property
def decorationVisible(self):
"""True if the decoration is visible"""
return isinstance(self._w, sat_widgets.LabelLine)
def keypress(self, size, key):
if key == a_key['DECORATION_HIDE']: #user wants to (un)hide widget decoration
show = not self.decorationVisible
self.showDecoration(show)
else:
return super(PrimitivusWidget, self).keypress(size, key)
def _getDecoration(self, widget):
return sat_widgets.LabelLine(widget, self.title)
def showDecoration(self, show=True):
"""Show/Hide the decoration around the window"""
self._w = self._getDecoration(self._original_widget) if show else self._original_widget
def getMenu(self):
raise NotImplementedError

View file

@ -26,6 +26,7 @@ from urwid_satext import files_management
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.primitivus.constants import Const as C
from sat_frontends.primitivus.widget import PrimitivusWidget
from sat_frontends.tools import xmlui
@ -355,14 +356,14 @@ class WidgetFactory(GenericFactory):
return cls
class XMLUIPanel(xmlui.XMLUIPanel, urwid.WidgetWrap):
class XMLUIPanel(xmlui.XMLUIPanel, PrimitivusWidget):
widget_factory = WidgetFactory()
def __init__(self, host, parsed_xml, title = None, flags = None):
def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
self.widget_factory._xmlui_main = self
self._dest = None
xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags)
urwid.WidgetWrap.__init__(self, self.main_cont)
xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags, callback, profile)
PrimitivusWidget.__init__(self, self.main_cont, self.xmlui_title)
def constructUI(self, parsed_dom):
def postTreat():
@ -412,11 +413,10 @@ class XMLUIPanel(xmlui.XMLUIPanel, urwid.WidgetWrap):
raise ValueError('Invalid show_type [%s]' % show_type)
self._dest = show_type
decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or ''))
if show_type == 'popup':
self.host.showPopUp(decorated, valign=valign)
self.host.showPopUp(self, valign=valign)
elif show_type == 'window':
self.host.addWindow(decorated)
self.host.newWidget(self)
else:
assert(False)
self.host.redraw()

View file

@ -22,4 +22,15 @@ from sat_frontends import constants
class Const(constants.Const):
PRIVATE_PREFIX = "@private@"
#Chats
CHAT_ONE2ONE = 'one2one'
CHAT_GROUP = 'group'
#Contact list
CONTACT_GROUPS = 'groups'
CONTACT_RESOURCES = 'resources'
CONTACT_MAIN_RESOURCE = 'main_resource'
CONTACT_SPECIAL = 'special'
CONTACT_SPECIAL_GROUP = 'group' # group chat special entity
CONTACT_SPECIAL_ALLOWED = (CONTACT_SPECIAL_GROUP,) # set of allowed values for special flag
CONTACT_DATA_FORBIDDEN = {CONTACT_GROUPS, CONTACT_RESOURCES, CONTACT_MAIN_RESOURCE} # set of forbidden names for contact data

View file

@ -21,23 +21,128 @@ from sat.core.i18n import _
import sys
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.tools.jid import JID
from sat_frontends.bridge.DBus import DBusBridgeFrontend
from sat.core import exceptions
from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
from sat_frontends.bridge.DBus import DBusBridgeFrontend
from sat_frontends.tools import jid
from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager
from sat_frontends.quick_frontend import quick_chat
from optparse import OptionParser
from sat_frontends.quick_frontend.constants import Const as C
class ProfileManager(object):
"""Class managing all data relative to one profile, and plugging in mechanism"""
host = None
bridge = None
def __init__(self, profile):
self.profile = profile
self.whoami = None
self.data = {}
def __getitem__(self, key):
return self.data[key]
def __setitem__(self, key, value):
self.data[key] = value
def plug(self):
"""Plug the profile to the host"""
# we get the essential params
self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile,
callback=self._plug_profile_jid, errback=self._getParamError)
def _plug_profile_jid(self, _jid):
self.whoami = jid.JID(_jid)
self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile,
callback=self._plug_profile_autoconnect, errback=self._getParamError)
def _plug_profile_autoconnect(self, value_str):
autoconnect = C.bool(value_str)
if autoconnect and not self.bridge.isConnected(self.profile):
self.host.asyncConnect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect())
else:
self._plug_profile_afterconnect()
def _plug_profile_afterconnect(self):
# Profile can be connected or not
# TODO: watched plugin
contact_list = self.host.addContactList(self.profile)
if not self.bridge.isConnected(self.profile):
self.host.setStatusOnline(False, profile=self.profile)
else:
self.host.setStatusOnline(True, profile=self.profile)
contact_list.fill()
#The waiting subscription requests
waitingSub = self.bridge.getWaitingSub(self.profile)
for sub in waitingSub:
self.host.subscribeHandler(waitingSub[sub], sub, self.profile)
#Now we open the MUC window where we already are:
for room_args in self.bridge.getRoomsJoined(self.profile):
self.host.roomJoinedHandler(*room_args, profile=self.profile)
for subject_args in self.bridge.getRoomsSubjects(self.profile):
self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
#Finaly, we get the waiting confirmation requests
for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile):
self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
def _getParamError(self, ignore):
log.error(_("Can't get profile parameter"))
class ProfilesManager(object):
"""Class managing collection of profiles"""
def __init__(self):
self._profiles = {}
def __contains__(self, profile):
return profile in self._profiles
def __iter__(self):
return self._profiles.iterkeys()
def __getitem__(self, profile):
return self._profiles[profile]
def __len__(self):
return len(self._profiles)
def plug(self, profile):
if profile in self._profiles:
raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile))
self._profiles[profile] = ProfileManager(profile)
self._profiles[profile].plug()
def unplug(self, profile):
if profile not in self._profiles:
raise ValueError('The profile [{}] is not plugged'.format(profile))
del self._profiles[profile]
def chooseOneProfile(self):
return self._profiles.keys()[0]
class QuickApp(object):
"""This class contain the main methods needed for the frontend"""
def __init__(self, single_profile=True):
self.profiles = {}
self.single_profile = single_profile
def __init__(self):
ProfileManager.host = self
self.profiles = ProfilesManager()
self.contact_lists = {}
self.widgets = QuickWidgetsManager(self)
self.check_options()
# widgets
self.visible_widgets = set() # widgets currently visible (must be filled by frontend)
self.selected_widget = None # widget currently selected (must be filled by frontend)
## bridge ##
try:
self.bridge = DBusBridgeFrontend()
@ -47,6 +152,7 @@ class QuickApp(object):
except exceptions.BridgeInitError:
print(_(u"Can't init bridge"))
sys.exit(1)
ProfileManager.bridge = self.bridge
self.registerSignal("connected")
self.registerSignal("disconnected")
self.registerSignal("newContact")
@ -84,10 +190,18 @@ class QuickApp(object):
self.registerSignal("quizGameTimerRestarted", iface="plugin")
self.registerSignal("chatStateReceived", iface="plugin")
self.current_action_ids = set()
self.current_action_ids_cb = {}
self.current_action_ids = set() # FIXME: to be removed
self.current_action_ids_cb = {} # FIXME: to be removed
self.media_dir = self.bridge.getConfig('', 'media_dir')
@property
def current_profile(self):
"""Profile that a user would expect to use"""
try:
return self.selected_widget.profile
except (TypeError, AttributeError):
return self.profiles.chooseOneProfile()
def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
"""Register a handler for a signal
@ -115,15 +229,15 @@ class QuickApp(object):
def check_profile(self, profile):
"""Tell if the profile is currently followed by the application"""
return profile in self.profiles.keys()
return profile in self.profiles
def postInit(self):
"""Must be called after initialization is done, do all automatic task (auto plug profile)"""
def postInit(self, profile_manager):
"""Must be called after initialization is done, do all automatic task (auto plug profile)
@param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
"""
if self.options.profile:
if not self.bridge.getProfileName(self.options.profile):
log.error(_("Trying to plug an unknown profile (%s)" % self.options.profile))
else:
self.plug_profile(self.options.profile)
profile_manager.autoconnect([self.options.profile])
def check_options(self):
"""Check command line options"""
@ -132,7 +246,7 @@ class QuickApp(object):
%prog --help for options list
""")
parser = OptionParser(usage=usage)
parser = OptionParser(usage=usage) # TODO: use argparse
parser.add_option("-p", "--profile", help=_("Select the profile to use"))
@ -141,46 +255,6 @@ class QuickApp(object):
self.options.profile = self.options.profile.decode('utf-8')
return args
def _getParamError(self, ignore):
log.error(_("Can't get profile parameter"))
def plug_profile(self, profile_key='@DEFAULT@'):
"""Tell application which profile must be used"""
if self.single_profile and self.profiles:
log.error(_('There is already one profile plugged (we are in single profile mode) !'))
return
profile = self.bridge.getProfileName(profile_key)
if not profile:
log.error(_("The profile asked doesn't exist"))
return
if profile in self.profiles:
log.warning(_("The profile is already plugged"))
return
self.profiles[profile] = {}
if self.single_profile:
self.profile = profile # FIXME: must be refactored (multi profiles are not managed correclty)
raw_menus = self.bridge.getMenus("", C.NO_SECURITY_LIMIT )
menus = self.profiles[profile]['menus'] = {}
for raw_menu in raw_menus:
id_, type_, path, path_i18n = raw_menu
menus_data = menus.setdefault(type_, [])
menus_data.append((id_, path, path_i18n))
self.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'plug_profile'}, profile_key=profile)
def plug_profile_1(self, profile):
###now we get the essential params###
self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
callback=lambda _jid: self.plug_profile_2(_jid, profile), errback=self._getParamError)
def plug_profile_2(self, _jid, profile):
self.profiles[profile]['whoami'] = JID(_jid)
self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=profile,
callback=lambda value: self.plug_profile_3(value == "true", profile), errback=self._getParamError)
def plug_profile_3(self, autoconnect, profile):
self.bridge.asyncGetParamA("Watched", "Misc", profile_key=profile,
callback=lambda watched: self.plug_profile_4(watched, autoconnect, profile), errback=self._getParamError)
def asyncConnect(self, profile, callback=None, errback=None):
if not callback:
callback = lambda dummy: None
@ -188,134 +262,74 @@ class QuickApp(object):
def errback(failure):
log.error(_(u"Can't connect profile [%s]") % failure)
if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile_key=profile)
self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
else:
self.showDialog(failure.message, failure.fullname, 'error')
self.bridge.asyncConnect(profile, callback=callback, errback=errback)
def plug_profile_4(self, watched, autoconnect, profile):
if autoconnect and not self.bridge.isConnected(profile):
#Does the user want autoconnection ?
self.asyncConnect(profile, callback=lambda dummy: self.plug_profile_5(watched, autoconnect, profile))
else:
self.plug_profile_5(watched, autoconnect, profile)
def plug_profiles(self, profiles):
"""Tell application which profiles must be used
def plug_profile_5(self, watched, autoconnect, profile):
self.profiles[profile]['watched'] = watched.split() # TODO: put this in a plugin
@param profiles: list of valid profile names
"""
self.plugging_profiles()
for profile in profiles:
self.profiles.plug(profile)
## misc ##
self.profiles[profile]['onlineContact'] = set() # FIXME: temporary
def plugging_profiles(self):
"""Method to subclass to manage frontend specific things to do
#TODO: manage multi-profiles here
if not self.bridge.isConnected(profile):
self.setStatusOnline(False)
else:
self.setStatusOnline(True)
### now we fill the contact list ###
for contact in self.bridge.getContacts(profile):
self.newContactHandler(*contact, profile=profile)
presences = self.bridge.getPresenceStatuses(profile)
for contact in presences:
for res in presences[contact]:
jabber_id = ('%s/%s' % (JID(contact).bare, res)) if res else contact
show = presences[contact][res][0]
priority = presences[contact][res][1]
statuses = presences[contact][res][2]
self.presenceUpdateHandler(jabber_id, show, priority, statuses, profile)
data = self.bridge.getEntityData(contact, ['avatar', 'nick'], profile)
for key in ('avatar', 'nick'):
if key in data:
self.entityDataUpdatedHandler(contact, key, data[key], profile)
#The waiting subscription requests
waitingSub = self.bridge.getWaitingSub(profile)
for sub in waitingSub:
self.subscribeHandler(waitingSub[sub], sub, profile)
#Now we open the MUC window where we already are:
for room_args in self.bridge.getRoomsJoined(profile):
self.roomJoinedHandler(*room_args, profile=profile)
for subject_args in self.bridge.getRoomsSubjects(profile):
self.roomNewSubjectHandler(*subject_args, profile=profile)
#Finaly, we get the waiting confirmation requests
for confirm_id, confirm_type, data in self.bridge.getWaitingConf(profile):
self.askConfirmationHandler(confirm_id, confirm_type, data, profile)
will be called when profiles are choosen and are to be plugged soon
"""
raise NotImplementedError
def unplug_profile(self, profile):
"""Tell the application to not follow anymore the profile"""
if not profile in self.profiles:
log.warning(_("This profile is not plugged"))
return
raise ValueError("The profile [{}] is not plugged".format(profile))
self.profiles.remove(profile)
def clear_profile(self):
self.profiles.clear()
def newWidget(self, widget):
raise NotImplementedError
def connectedHandler(self, profile):
"""called when the connection is made"""
log.debug(_("Connected"))
self.setStatusOnline(True)
self.setStatusOnline(True, profile=profile)
def disconnectedHandler(self, profile):
"""called when the connection is closed"""
log.debug(_("Disconnected"))
self.contact_list.clearContacts()
self.setStatusOnline(False)
self.contact_lists[profile].clearContacts()
self.setStatusOnline(False, profile=profile)
def newContactHandler(self, JabberId, attributes, groups, profile):
entity = JID(JabberId)
entity = jid.JID(JabberId)
_groups = list(groups)
self.contact_list.replace(entity, _groups, attributes)
self.contact_lists[profile].setContact(entity, _groups, attributes, in_roster=True)
def _newMessage(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
"""newMessage premanagement: a dirty hack to manage private messages
if a private MUC message is detected, from_jid or to_jid is prefixed and resource is escaped
"""
# FIXME: must be refactored for 0.6
from_jid = JID(from_jid_s)
to_jid = JID(to_jid_s)
from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
win = to_jid if from_me else from_jid
if ((type_ != "groupchat" and self.contact_list.getSpecial(win) == "MUC") and
(type_ != C.MESS_TYPE_INFO or (type_ == C.MESS_TYPE_INFO and win.resource))):
#we have a private message in a MUC room
#XXX: normaly we use bare jid as key, here we need the full jid
# so we cheat by replacing the "/" before the resource by
# a "@", so the jid is invalid,
new_jid = escapePrivate(win)
if from_me:
to_jid = new_jid
else:
from_jid = new_jid
if new_jid not in self.contact_list:
self.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
from_jid = jid.JID(from_jid_s)
to_jid = jid.JID(to_jid_s)
self.newMessageHandler(from_jid, to_jid, msg, type_, extra, profile)
def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
win = to_jid if from_me else from_jid
from_me = from_jid.bare == self.profiles[profile].whoami.bare
target = to_jid if from_me else from_jid
self.current_action_ids = set()
self.current_action_ids_cb = {}
chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
timestamp = extra.get('archive')
if type_ == C.MESS_TYPE_INFO:
self.chat_wins[win.bare].printInfo(msg, timestamp=float(timestamp) if timestamp else None)
else:
self.chat_wins[win.bare].printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, profile=profile)
def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key="@NONE@"):
if to_jid.startswith(C.PRIVATE_PREFIX):
to_jid = unescapePrivate(to_jid)
mess_type = "chat"
self.current_action_ids = set() # FIXME: to be removed
self.current_action_ids_cb = {} # FIXME: to be removed
chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
if callback is None:
callback = lambda: None
if errback is None:
@ -326,100 +340,78 @@ class QuickApp(object):
assert alert_type in ['INFO', 'ERROR']
self.showDialog(unicode(msg), unicode(title), alert_type.lower())
def setStatusOnline(self, online=True, show="", statuses={}):
def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
raise NotImplementedError
def presenceUpdateHandler(self, jabber_id, show, priority, statuses, profile):
def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
log.debug(_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
% {'jid': jabber_id, 'show': show, 'priority': priority, 'statuses': statuses, 'profile': profile})
from_jid = JID(jabber_id)
log.debug(_("presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
% {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile})
entity = jid.JID(entity_s)
if from_jid == self.profiles[profile]['whoami']:
if entity == self.profiles[profile].whoami:
if show == "unavailable":
self.setStatusOnline(False)
self.setStatusOnline(False, profile=profile)
else:
self.setStatusOnline(True, show, statuses)
self.setStatusOnline(True, show, statuses, profile=profile)
return
presences = self.profiles[profile].setdefault('presences', {})
# #FIXME: must be moved in a plugin
# if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']:
# self.showAlert(_("Watched jid [%s] is connected !") % entity.bare)
if show != 'unavailable':
self.contact_lists[profile].updatePresence(entity, show, priority, statuses)
#FIXME: must be moved in a plugin
if from_jid.bare in self.profiles[profile].get('watched',[]) and not from_jid.bare in self.profiles[profile]['onlineContact']:
self.showAlert(_("Watched jid [%s] is connected !") % from_jid.bare)
presences[jabber_id] = {'show': show, 'priority': priority, 'statuses': statuses}
self.profiles[profile].setdefault('onlineContact',set()).add(from_jid) # FIXME onlineContact is useless with CM, must be removed
#TODO: vcard data (avatar)
if show == "unavailable" and from_jid in self.profiles[profile].get('onlineContact',set()):
try:
del presences[jabber_id]
except KeyError:
pass
self.profiles[profile]['onlineContact'].remove(from_jid)
# check if the contact is connected with another resource, use the one with highest priority
jids = [jid for jid in presences if JID(jid).bare == from_jid.bare]
if jids:
max_jid = max(jids, key=lambda jid: presences[jid]['priority'])
data = presences[max_jid]
max_priority = data['priority']
if show == "unavailable": # do not check the priority here, because 'unavailable' has a dummy one
from_jid = JID(max_jid)
show, priority, statuses = data['show'], data['priority'], data['statuses']
if not jids or priority >= max_priority:
# case 1: not jids means all resources are disconnected, send the 'unavailable' presence
# case 2: update (or confirm) with the values of the resource which takes precedence
self.contact_list.updatePresence(from_jid, show, priority, statuses)
def roomJoinedHandler(self, room_jid, room_nicks, user_nick, profile):
def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
"""Called when a MUC room is joined"""
log.debug(_("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s") % {'room_jid': room_jid, 'profile': profile, 'users': room_nicks})
self.chat_wins[room_jid].setUserNick(user_nick)
self.chat_wins[room_jid].setType("group")
self.chat_wins[room_jid].id = room_jid
self.chat_wins[room_jid].setPresents(list(set([user_nick] + room_nicks)))
self.contact_list.setSpecial(JID(room_jid), "MUC", show=True)
log.debug("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
room_jid = jid.JID(room_jid_s)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
chat_widget.setUserNick(user_nick)
chat_widget.id = room_jid # FIXME: to be removed
chat_widget.setPresents(list(set([user_nick] + room_nicks)))
self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
def roomLeftHandler(self, room_jid_s, profile):
"""Called when a MUC room is left"""
log.debug(_("Room [%(room_jid)s] left by %(profile)s") % {'room_jid': room_jid_s, 'profile': profile})
log.debug("Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile})
del self.chat_wins[room_jid_s]
self.contact_list.remove(JID(room_jid_s))
self.contact_lists[profile].remove(jid.JID(room_jid_s))
def roomUserJoinedHandler(self, room_jid, user_nick, user_data, profile):
def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
"""Called when an user joined a MUC room"""
if room_jid in self.chat_wins:
self.chat_wins[room_jid].replaceUser(user_nick)
log.debug(_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
room_jid = jid.JID(room_jid_s)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
chat_widget.replaceUser(user_nick)
log.debug("user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
def roomUserLeftHandler(self, room_jid, user_nick, user_data, profile):
def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
"""Called when an user joined a MUC room"""
if room_jid in self.chat_wins:
self.chat_wins[room_jid].removeUser(user_nick)
log.debug(_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
room_jid = jid.JID(room_jid_s)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
chat_widget.removeUser(user_nick)
log.debug("user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
def roomUserChangedNickHandler(self, room_jid, old_nick, new_nick, profile):
def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
"""Called when an user joined a MUC room"""
if room_jid in self.chat_wins:
self.chat_wins[room_jid].changeUserNick(old_nick, new_nick)
log.debug(_("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]") % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
room_jid = jid.JID(room_jid_s)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
chat_widget.changeUserNick(old_nick, new_nick)
log.debug("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
def roomNewSubjectHandler(self, room_jid, subject, profile):
def roomNewSubjectHandler(self, room_jid_s, subject, profile):
"""Called when subject of MUC room change"""
if room_jid in self.chat_wins:
self.chat_wins[room_jid].setSubject(subject)
log.debug(_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid': room_jid, "subject": subject})
room_jid = jid.JID(room_jid_s)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
chat_widget.setSubject(subject)
log.debug("new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject})
def tarotGameStartedHandler(self, room_jid, referee, players, profile):
def tarotGameStartedHandler(self, room_jid_s, referee, players, profile):
log.debug(_("Tarot Game Started \o/"))
if room_jid in self.chat_wins:
self.chat_wins[room_jid].startGame("Tarot", referee, players)
log.debug(_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
room_jid = jid.JID(room_jid_s)
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_='group', profile=profile)
chat_widget.startGame("Tarot", referee, players)
log.debug("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s" % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
def tarotGameNewHandler(self, room_jid, hand, profile):
log.debug(_("New Tarot Game"))
@ -501,27 +493,17 @@ class QuickApp(object):
self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestartedHandler(time_left)
def chatStateReceivedHandler(self, from_jid_s, state, profile):
"""Callback when a new chat state is received.
"""Called when a new chat state is received.
@param from_jid_s: JID of the contact who sent his state, or '@ALL@'
@param state: new state (string)
@profile: current profile
"""
if from_jid_s == '@ALL@':
target = '@ALL@'
nick = C.ALL_OCCUPANTS
else:
from_jid = JID(from_jid_s)
target = from_jid.bare
nick = from_jid.resource
for bare in self.chat_wins.keys():
if target == '@ALL' or target == bare:
chat_win = self.chat_wins[bare]
if chat_win.type == 'one2one':
chat_win.updateChatState(state)
elif chat_win.type == 'group':
chat_win.updateChatState(state, nick=nick)
from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
for widget in self.visible_widgets:
if isinstance(widget, quick_chat.QuickChat):
if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
widget.updateChatState(from_jid, state)
def _subscribe_cb(self, answer, data):
entity, profile = data
@ -532,7 +514,7 @@ class QuickApp(object):
def subscribeHandler(self, type, raw_jid, profile):
"""Called when a subsciption management signal is received"""
entity = JID(raw_jid)
entity = jid.JID(raw_jid)
if type == "subscribed":
# this is a subscription confirmation, we just have to inform user
self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation'))
@ -553,33 +535,27 @@ class QuickApp(object):
log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
if (namespace, name) == ("Connection", "JabberID"):
log.debug(_("Changing JID to %s") % value)
self.profiles[profile]['whoami'] = JID(value)
self.profiles[profile].whoami = jid.JID(value)
elif (namespace, name) == ("Misc", "Watched"):
self.profiles[profile]['watched'] = value.split()
elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS):
self.contact_list.showOfflineContacts(C.bool(value))
self.contact_lists[profile].showOfflineContacts(C.bool(value))
elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS):
self.contact_list.showEmptyGroups(C.bool(value))
self.contact_lists[profile].showEmptyGroups(C.bool(value))
def contactDeletedHandler(self, jid, profile):
target = JID(jid)
self.contact_list.remove(target)
try:
self.profiles[profile]['onlineContact'].remove(target.bare)
except KeyError:
pass
target = jid.JID(jid)
self.contact_lists[profile].remove(target)
def entityDataUpdatedHandler(self, jid_str, key, value, profile):
jid = JID(jid_str)
def entityDataUpdatedHandler(self, entity_s, key, value, profile):
entity = jid.JID(entity_s)
if key == "nick":
if jid in self.contact_list:
self.contact_list.setCache(jid, 'nick', value)
self.contact_list.replace(jid)
if entity in self.contact_lists[profile]:
self.contact_lists[profile].setCache(entity, 'nick', value)
elif key == "avatar":
if jid in self.contact_list:
if entity in self.contact_lists[profile]:
filename = self.bridge.getAvatarFile(value)
self.contact_list.setCache(jid, 'avatar', filename)
self.contact_list.replace(jid)
self.contact_lists[profile].setCache(entity, 'avatar', filename)
def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
raise NotImplementedError
@ -587,22 +563,23 @@ class QuickApp(object):
def actionResultHandler(self, type, id, data, profile):
raise NotImplementedError
def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
""" Launch a dynamic action
def launchAction(self, callback_id, data=None, callback=None, profile="@NONE@"):
"""Launch a dynamic action
@param callback_id: id of the action to launch
@param data: data needed only for certain actions
@param profile_key: %(doc_profile_key)s
@param callback: if not None and 'validated' key is present, it will be called with the following parameters:
- callback_id
- data
- profile_key
@param profile_key: %(doc_profile)s
"""
raise NotImplementedError
def onExit(self):
"""Must be called when the frontend is terminating"""
#TODO: mange multi-profile here
try:
if self.bridge.isConnected(self.profile):
if self.bridge.getParamA("autodisconnect", "Connection", profile_key=self.profile) == "true":
for profile in self.profiles:
if self.bridge.isConnected(profile):
if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)):
#The user wants autodisconnection
self.bridge.disconnect(self.profile)
except:
pass
self.bridge.disconnect(profile)

View file

@ -21,32 +21,75 @@ from sat.core.i18n import _
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.tools.jid import JID
from sat_frontends.quick_frontend.quick_utils import unescapePrivate
from sat_frontends.quick_frontend import quick_widgets
from sat_frontends.quick_frontend.constants import Const as C
class QuickChat(object):
class QuickChat(quick_widgets.QuickWidget):
def __init__(self, target, host, type_='one2one'):
self.target = target
self.host = host
def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
"""
@param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
"""
quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
if type_ == C.CHAT_GROUP and target.resource:
raise ValueError("A group chat entity can't have a resource")
self.current_target = target
self.type = type_
self.id = ""
self.id = "" # FIXME: to be removed
self.nick = None
self.occupants = set()
def setType(self, type_):
"""Set the type of the chat
@param type: can be 'one2one' for single conversation or 'group' for chat à la IRC
def __str__(self):
return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
@staticmethod
def getWidgetHash(target, profile):
return (unicode(profile), target.bare)
@staticmethod
def getPrivateHash(target, profile):
"""Get unique hash for private conversations
This method should be used with force_hash to get unique widget for private MUC conversations
"""
self.type = type_
return (unicode(profile), target)
def addTarget(self, target):
super(QuickChat, self).addTarget(target)
if target.resource:
self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead
@property
def target(self):
if self.type == C.CHAT_GROUP:
return self.current_target.bare
return self.current_target
def manageMessage(self, entity, mess_type):
"""Tell if this chat widget manage an entity and message type couple
@param entity (jid.JID): (full) jid of the sending entity
@param mess_type (str): message type as given by newMessage
@return (bool): True if this Chat Widget manage this couple
"""
if self.type == C.CHAT_GROUP:
if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
return True
else:
if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
return True
return False
def setPresents(self, nicks):
"""Set the users presents in the contact list for a group chat
@param nicks: list of nicknames
"""
log.debug (_("Adding users %s to room") % nicks)
if self.type != "group":
if self.type != C.CHAT_GROUP:
log.error (_("[INTERNAL] trying to set presents nicks for a non group chat window"))
raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
self.occupants.update(nicks)
@ -54,7 +97,7 @@ class QuickChat(object):
def replaceUser(self, nick, show_info=True):
"""Add user if it is not in the group list"""
log.debug (_("Replacing user %s") % nick)
if self.type != "group":
if self.type != C.CHAT_GROUP:
log.error (_("[INTERNAL] trying to replace user for a non group chat window"))
raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
len_before = len(self.occupants)
@ -65,7 +108,7 @@ class QuickChat(object):
def removeUser(self, nick, show_info=True):
"""Remove a user from the group list"""
log.debug(_("Removing user %s") % nick)
if self.type != "group":
if self.type != C.CHAT_GROUP:
log.error (_("[INTERNAL] trying to remove user for a non group chat window"))
raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
self.occupants.remove(nick)
@ -82,7 +125,7 @@ class QuickChat(object):
def changeUserNick(self, old_nick, new_nick):
"""Change nick of a user in group list"""
log.debug(_("Changing nick of user %(old_nick)s to %(new_nick)s") % {"old_nick": old_nick, "new_nick": new_nick})
if self.type != "group":
if self.type != C.CHAT_GROUP:
log.error (_("[INTERNAL] trying to change user nick for a non group chat window"))
raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
self.removeUser(old_nick, show_info=False)
@ -92,7 +135,7 @@ class QuickChat(object):
def setSubject(self, subject):
"""Set title for a group chat"""
log.debug(_("Setting subject to %s") % subject)
if self.type != "group":
if self.type != C.CHAT_GROUP:
log.error (_("[INTERNAL] trying to set subject for a non group chat window"))
raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
@ -111,8 +154,8 @@ class QuickChat(object):
def onHistory(history):
for line in history:
timestamp, from_jid, to_jid, message, _type, extra = line
if ((self.type == 'group' and _type != 'groupchat') or
(self.type == 'one2one' and _type == 'groupchat')):
if ((self.type == C.CHAT_GROUP and _type != C.MESS_TYPE_GROUPCHAT) or
(self.type == C.CHAT_ONE2ONE and _type == C.MESS_TYPE_GROUPCHAT)):
continue
self.printMessage(JID(from_jid), message, profile, timestamp)
self.afterHistoryPrint()
@ -120,26 +163,47 @@ class QuickChat(object):
def onHistoryError(err):
log.error(_("Can't get history"))
if self.target.startswith(C.PRIVATE_PREFIX):
target = unescapePrivate(self.target)
target = self.target.bare
return self.host.bridge.getHistory(self.host.profiles[profile].whoami.bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)
def _get_nick(self, entity):
"""Return nick of this entity when possible"""
if self.type == C.CHAT_GROUP:
return entity.resource
contact_list = self.host.contact_lists[self.profile]
if entity.bare in contact_list:
return contact_list.getCache(entity,'nick') or contact_list.getCache(entity,'name') or entity.node or entity
return entity.node or entity
def onPrivateCreated(self, widget):
"""Method called when a new widget for private conversation (MUC) is created"""
raise NotImplementedError
def getOrCreatePrivateWidget(self, entity):
"""Create a widget for private conversation, or get it if it already exists
@param entity: full jid of the target
"""
return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again
def newMessage(self, from_jid, target, msg, type_, extra, profile):
if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
# we have a private message, we forward it to a private conversation widget
chat_widget = self.getOrCreatePrivateWidget(target)
chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
else:
target = self.target.bare
return self.host.bridge.getHistory(self.host.profiles[profile]['whoami'].bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError)
def _get_nick(self, jid):
"""Return nick of this jid when possible"""
if self.target.startswith(C.PRIVATE_PREFIX):
unescaped = unescapePrivate(self.target)
if jid.startswith(C.PRIVATE_PREFIX) or unescaped.bare == jid.bare:
return unescaped.resource
return jid.resource if self.type == "group" else (self.host.contact_list.getCache(jid,'nick') or self.host.contact_list.getCache(jid,'name') or jid.node)
timestamp = extra.get('archive')
if type_ == C.MESS_TYPE_INFO:
self.printInfo(msg, timestamp=float(timestamp) if timestamp else None)
else:
self.printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
def printMessage(self, from_jid, msg, profile, timestamp=None):
"""Print message in chat window. Must be implemented by child class"""
jid = JID(from_jid)
nick = self._get_nick(jid)
mymess = (jid.resource == self.nick) if self.type == "group" else (jid.bare == self.host.profiles[profile]['whoami'].bare) #mymess = True if message comes from local user
mymess = (jid.resource == self.nick) if self.type == C.CHAT_GROUP else (jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user
if msg.startswith('/me '):
self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', timestamp=timestamp)
return
@ -165,12 +229,11 @@ class QuickChat(object):
#No need to raise an error as game are not mandatory
log.warning(_('getGame is not implemented in this frontend'))
def updateChatState(self, state, nick=None):
"""Set the chat state (XEP-0085) of the contact. Leave nick to None
to set the state for a one2one conversation, or give a nickname or
C.ALL_OCCUPANTS to set the state of a participant within a MUC.
def updateChatState(self, from_jid, state):
"""Set the chat state (XEP-0085) of the contact.
@param state: the new chat state
@param nick: None for one2one, the MUC user nick or ALL_OCCUPANTS
"""
raise NotImplementedError
quick_widgets.register(QuickChat)

View file

@ -1,39 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# helper class for making a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat_frontends.tools.jid import JID
class QuickChatList(dict):
"""This class is used to manage the list of chat windows.
It act as a dict, but create a chat window when the name is found for the first time."""
def __init__(self, host):
dict.__init__(self)
self.host = host
def __getitem__(self, to_jid):
target = JID(to_jid)
if not target.bare in self:
#we have to create the chat win
self[target.bare] = self.createChat(target)
return dict.__getitem__(self, target.bare)
def createChat(self, target):
raise NotImplementedError

View file

@ -20,41 +20,158 @@
from sat.core.i18n import _
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.quick_frontend.quick_widgets import QuickWidget
from sat_frontends.quick_frontend.constants import Const as C
from sat_frontends.tools import jid
class QuickContactList(object):
class QuickContactList(QuickWidget):
"""This class manage the visual representation of contacts"""
def __init__(self):
def __init__(self, host, profile):
log.debug(_("Contact List init"))
super(QuickContactList, self).__init__(host, profile, profile)
# bare jids as keys, resources are used in data
self._cache = {}
self.specials={}
def update_jid(self, jid):
"""Update the jid in the list when something changed"""
# special entities (groupchat, gateways, etc), bare jids
self._specials = set()
# extras are specials with full jids (e.g.: private MUC conversation)
self._special_extras = set()
# group data contain jids in groups and misc frontend data
self._groups = {} # groups to group data map
# contacts in roster (bare jids)
self._roster = set()
# entities with an alert (usually a waiting message), full jid
self._alerts = set()
# selected entities, full jid
self._selected = set()
# options
self.show_disconnected = False
self.show_empty_groups = True
self.show_resources = False
self.show_status = False
# TODO: this may lead to two successive UI refresh and needs an optimization
self.host.bridge.asyncGetParamA(C.SHOW_EMPTY_GROUPS, "General", profile_key=profile, callback=self.showEmptyGroups)
self.host.bridge.asyncGetParamA(C.SHOW_OFFLINE_CONTACTS, "General", profile_key=profile, callback=self.showOfflineContacts)
def __contains__(self, entity):
"""Check if entity is in contact list
@param entity (jid.JID): jid of the entity (resource is not ignored, use bare jid if needed)
"""
if entity.resource:
try:
return entity.resource in self.getCache(entity.bare, C.CONTACT_RESOURCES)
except KeyError:
return False
return entity in self._cache
def fill(self):
"""Get all contacts from backend, and fill the widget"""
def gotContacts(contacts):
for contact in contacts:
self.host.newContactHandler(*contact, profile=self.profile)
presences = self.host.bridge.getPresenceStatuses(self.profile)
for contact in presences:
for res in presences[contact]:
jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
show = presences[contact][res][0]
priority = presences[contact][res][1]
statuses = presences[contact][res][2]
self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
data = self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile)
for key in ('avatar', 'nick'):
if key in data:
self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
self.host.bridge.getContacts(self.profile, callback=gotContacts)
def update(self):
"""Update the display when something changed"""
raise NotImplementedError
def getCache(self, jid, name):
def getCache(self, entity, name=None):
"""Return a cache value for a contact
@param entity(entity.entity): entity of the contact from who we want data (resource is used if given)
if a resource specific information is requested:
- if no resource is given (bare jid), the main resource is used, according to priority
- if resource is given, it is used
@param name(unicode): name the data to get, or None to get everything
"""
cache = self._cache[entity.bare]
if name is None:
return cache
try:
jid_cache = self._cache[jid.bare]
if name == 'status': #XXX: we get the first status for 'status' key
return jid_cache['statuses'].get('default','')
return jid_cache[name]
except (KeyError, IndexError):
if name in ('status', C.PRESENCE_STATUSES, C.PRESENCE_PRIORITY, C.PRESENCE_SHOW):
# these data are related to the resource
if not entity.resource:
main_resource = cache[C.CONTACT_MAIN_RESOURCE]
cache = cache[C.CONTACT_RESOURCES][main_resource]
else:
cache = cache[C.CONTACT_RESOURCES][entity.resource]
if name == 'status': #XXX: we get the first status for 'status' key
# TODO: manage main language for statuses
return cache[C.PRESENCE_STATUSES].get('default','')
return cache[name]
except KeyError:
return None
def setCache(self, jid, name, value):
jid_cache = self._cache.setdefault(jid.bare, {})
jid_cache[name] = value
def setCache(self, entity, name, value):
"""Set or update value for one data in cache
def __contains__(self, jid):
raise NotImplementedError
@param entity(JID): entity to update
@param name(unicode): value to set or update
"""
self.setContact(entity, None, {name: value})
def setGroupData(self, group, name, value):
"""Register a data for a group
@param group: a valid (existing) group name
@param name: name of the data (can't be "jids")
@param value: value to set
"""
assert name is not 'jids'
self._groups[group][name] = value
def getGroupData(self, group, name=None):
"""Return value associated to group data
@param group: a valid (existing) group name
@param name: name of the data or None to get the whole dict
@return: registered value
"""
if name is None:
return self._groups[group]
return self._groups[group][name]
def setSpecial(self, entity, special_type):
"""Set special flag on an entity
@param entity(jid.JID): jid of the special entity
@param special_type: one of special type (e.g. C.CONTACT_SPECIAL_GROUP) or None to remove special flag
"""
assert special_type in C.CONTACT_SPECIAL_ALLOWED + (None,)
self.setCache(entity, C.CONTACT_SPECIAL, special_type)
def clearContacts(self):
"""Clear all the contact list"""
self.specials.clear()
self.unselectAll()
self._cache.clear()
self._groups.clear()
self._specials.clear()
self.update()
def replace(self, jid, groups=None, attributes=None):
def setContact(self, entity, groups=None, attributes=None, in_roster=False):
"""Add a contact to the list if doesn't exist, else update it.
This method can be called with groups=None for the purpose of updating
@ -64,49 +181,177 @@ class QuickContactList(object):
None value for 'groups' has a different meaning than [None] which is for the default group.
@param jid (JID)
@param entity (jid.JID): entity to add or replace
@param groups (list): list of groups or None to ignore the groups membership.
@param attributes (dict)
@param attributes (dict): attibutes of the added jid or to update
@param in_roster (bool): True if contact is from roster
"""
if attributes and 'name' in attributes:
self.setCache(jid, 'name', attributes['name'])
if attributes is None:
attributes = {}
def remove(self, jid):
"""remove a contact from the list"""
entity_bare = entity.bare
if in_roster:
self._roster.add(entity_bare)
cache = self._cache.setdefault(entity_bare, {C.CONTACT_RESOURCES: {}})
assert not C.CONTACT_DATA_FORBIDDEN.intersection(attributes) # we don't want forbidden data in attributes
# we set groups and fill self._groups accordingly
if groups is not None:
if not groups:
groups = [None] # [None] is the default group
cache[C.CONTACT_GROUPS] = groups
for group in groups:
self._groups.setdefault(group, {}).setdefault('jids', set()).add(entity_bare)
# special entities management
if C.CONTACT_SPECIAL in attributes:
if attributes[C.CONTACT_SPECIAL] is None:
del attributes[C.CONTACT_SPECIAL]
self._specials.remove(entity_bare)
else:
self._specials.add(entity_bare)
# now the attribute we keep in cache
for attribute, value in attributes.iteritems():
cache[attribute] = value
# we can update the display
self.update()
def getContacts(self):
"""Return contacts currently selected
@return (set): set of selected entities"""
return self._selected
def entityToShow(self, entity, check_resource=False):
"""Tell if the contact should be showed or hidden.
@param contact (jid.JID): jid of the contact
@param check_resource (bool): True if resource must be significant
@return: True if that contact should be showed in the list
"""
show = self.getCache(entity, C.PRESENCE_SHOW)
if check_resource:
alerts = self._alerts
selected = self._selected
else:
alerts = {alert.bare for alert in self._alerts}
selected = {selected.bare for selected in self._selected}
return ((show is not None and show != "unavailable")
or self.show_disconnected
or entity in alerts
or entity in selected)
def anyEntityToShow(self, entities, check_resources=False):
"""Tell if in a list of entities, at least one should be shown
@param entities (list[jid.JID]): list of jids
@param check_resources (bool): True if resources must be significant
@return: bool
"""
for entity in entities:
if self.entityToShow(entity, check_resources):
return True
return False
def remove(self, entity):
"""remove a contact from the list
@param entity(jid.JID): jid of the entity to remove (bare jid is used)
"""
entity_bare = entity.bare
try:
del self.specials[jid.bare]
groups = self._cache[entity_bare].get(C.CONTACT_GROUPS, set())
except KeyError:
pass
log.warning(_("Trying to delete an unknow entity [{}]").format(entity))
del self._cache[entity_bare]
for group in groups:
self._groups[group]['jids'].remove(entity_bare)
for set_ in (self._selected, self._alerts, self._specials, self._special_extras):
to_remove = set()
for set_entity in set_:
if set_entity.bare == entity.bare:
to_remove.add(set_entity)
set_.difference_update(to_remove)
self.update()
def add(self, jid, param_groups=None):
"""add a contact to the list"""
raise NotImplementedError
def getSpecial(self, jid):
"""Return special type of jid, or None if it's not special"""
return self.specials.get(jid.bare)
def setSpecial(self, jid, _type, show=False):
"""Set entity as a special
@param jid: jid of the entity
@param _type: special type (e.g.: "MUC")
@param show: True to display the dialog to chat with this entity
"""
self.specials[jid.bare] = _type
def updatePresence(self, jid, show, priority, statuses):
def updatePresence(self, entity, show, priority, statuses):
"""Update entity's presence status
@param jid: entity to update's jid
@param entity(jid.JID): entity to update's entity
@param show: availability
@parap priority: resource's priority
@param statuses: dict of statuses"""
self.setCache(jid, 'show', show)
self.setCache(jid, 'prority', priority)
self.setCache(jid, 'statuses', statuses)
self.update_jid(jid)
@param statuses: dict of statuses
"""
cache = self.getCache(entity)
if show == C.PRESENCE_UNAVAILABLE:
if not entity.resource:
cache[C.CONTACT_RESOURCES].clear()
cache[C.CONTACT_MAIN_RESOURCE]= None
else:
del cache[C.CONTACT_RESOURCES][entity.resource]
if not cache[C.CONTACT_RESOURCES]:
cache[C.CONTACT_MAIN_RESOURCE] = None
else:
assert entity.resource
resources_data = cache[C.CONTACT_RESOURCES]
resource_data = resources_data.setdefault(entity.resource, {})
resource_data[C.PRESENCE_SHOW] = show
resource_data[C.PRESENCE_PRIORITY] = int(priority)
resource_data[C.PRESENCE_STATUSES] = statuses
priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY])
cache[C.CONTACT_MAIN_RESOURCE] = priority_resource
self.update()
def unselectAll(self):
"""Unselect all contacts"""
self._selected.clear()
self.update()
def select(self, entity):
"""Select an entity
@param entity(jid.JID): entity to select (resource is significant)
"""
log.debug("select %s" % entity)
self._selected.add(entity)
self.update()
def setAlert(self, entity):
"""Set an alert on the entity (usually for a waiting message)
@param entity(jid.JID): entity which must displayed in alert mode (resource is significant)
"""
self._alerts.add(entity)
self.update()
def showOfflineContacts(self, show):
pass
show = C.bool(show)
if self.show_disconnected == show:
return
self.show_disconnected = show
self.update()
def showEmptyGroups(self, show):
pass
show = C.bool(show)
if self.show_empty_groups == show:
return
self.show_empty_groups = show
self.update()
def showResources(self, show):
show = C.bool(show)
if self.show_resources == show:
return
self.show_resources = show
self.update()

View file

@ -17,20 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat_frontends.tools.jid import JID
from os.path import exists, splitext
from sat_frontends.quick_frontend.constants import Const
def escapePrivate(ori_jid):
"""Escape a private jid"""
return JID(Const.PRIVATE_PREFIX + ori_jid.bare + '@' + ori_jid.resource)
def unescapePrivate(escaped_jid):
if not escaped_jid.startswith(Const.PRIVATE_PREFIX):
return escaped_jid
escaped_split = tuple(escaped_jid[len(Const.PRIVATE_PREFIX):].split('@'))
assert(len(escaped_split) == 3)
return JID("%s@%s/%s" % escaped_split)
def getNewPath(path):
""" Check if path exists, and find a non existant path if needed """

View file

@ -0,0 +1,187 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# helper class for making a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core import exceptions
classes_map = {}
def register(base_cls, child_cls=None):
"""Register a child class to use by default when a base class is needed
@param base_cls: "Quick..." base class (like QuickChat or QuickContact), must inherit from QuickWidget
@param child_cls: inherited class to use when Quick... class is requested, must inherit from base_cls.
Can be None if it's the base_cls itself which register
"""
classes_map[base_cls] = child_cls
class QuickWidgetsManager(object):
"""This class is used to manage all the widgets of a frontend
A widget can be a window, a graphical thing, or someting else depending of the frontend"""
def __init__(self, host):
self.host = host
self._widgets = {}
def getOrCreateWidget(self, class_, target, *args, **kwargs):
"""Get an existing widget or create a new one when necessary
If the widget is new, self.host.newWidget will be called with it.
@param class_(class): class of the widget to create
@param target: target depending of the widget, usually a JID instance
@param args(list): optional args to create a new instance of class_
@param kwargs(list): optional kwargs to create anew instance of class_
if 'profile' key is present, it will be popped and put in 'profiles'
if there is neither 'profile' nor 'profiles', None will be used for 'profiles'
if 'on_new_widget' is present it can have the following values:
'NEW_WIDGET' [default]: self.host.newWidget will be called on widget creation
[callable]: this method will be called instead of self.host.newWidget
None: do nothing
if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash
@return: a class_ instance, either new or already existing
"""
# class management
try:
cls = classes_map[class_]
except KeyError:
cls = class_
if cls is None:
raise exceptions.InternalError("There is not class registered for {}".format(class_))
# arguments management
_args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args
_kwargs = kwargs or {}
if 'profiles' in _kwargs and 'profile' in _kwargs:
raise ValueError("You can't have 'profile' and 'profiles' keys at the same time")
try:
_kwargs['profiles'] = _kwargs.pop('profile')
except KeyError:
if not 'profiles' in _kwargs:
_kwargs['profiles'] = None
# we get the hash
try:
hash_ = _kwargs.pop('force_hash')
except KeyError:
hash_ = cls.getWidgetHash(target, _kwargs['profiles'])
# widget creation or retrieval
widgets_list = self._widgets.setdefault(cls, {}) # we sorts widgets by classes
if not cls.SINGLE:
widget = None # if the class is not SINGLE, we always create a new widget
else:
try:
widget = widgets_list[hash_]
widget.addTarget(target)
except KeyError:
widget = None
if widget is None:
# we need to create a new widget
try:
#on_new_widget tell what to do for the new widget creation
on_new_widget = _kwargs.pop('on_new_widget')
except KeyError:
on_new_widget = 'NEW_WIDGET'
log.debug(u"Creating new widget for target {} {}".format(target, cls))
widget = cls(*_args, **_kwargs)
widgets_list[hash_] = widget
if on_new_widget == 'NEW_WIDGET':
self.host.newWidget(widget)
elif callable(on_new_widget):
on_new_widget(widget)
else:
assert on_new_widget is None
return widget
class QuickWidget(object):
"""generic widget base"""
SINGLE=True # if True, there can be only one widget per target(s)
PROFILES_MULTIPLE=False
PROFILES_ALLOW_NONE=False
def __init__(self, host, target, profiles=None):
"""
@param host: %(doc_host)s
@param target: target specific for this widget class
@param profiles: can be either:
- (unicode): used when widget class manage a unique profile
- (iterable): some widget class can manage several profiles, several at once can be specified here
- None: no profile is managed by this widget class (rare)
@raise: ValueError when (iterable) or None is given to profiles for a widget class which manage one unique profile.
"""
self.host = host
self.targets = set()
self.addTarget(target)
self.profiles = set()
if isinstance(profiles, basestring):
self.addProfile(profiles)
elif profiles is None:
if not self.PROFILES_ALLOW_NONE:
raise ValueError("profiles can't have a value of None")
else:
if not self.PROFILES_MULTIPLE:
raise ValueError("multiple profiles are not allowed")
for profile in profiles:
self.addProfile(profile)
@property
def profile(self):
assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
return list(self.profiles)[0]
def addTarget(self, target):
"""Add a target if it doesn't already exists
@param target: target to add
"""
self.targets.add(target)
def addProfile(self, profile):
"""Add a profile is if doesn't already exists
@param profile: profile to add
"""
if self.profiles and not self.PROFILES_MULTIPLE:
raise ValueError("multiple profiles are not allowed")
self.profiles.add(profile)
@staticmethod
def getWidgetHash(target, profiles):
"""Return the hash associated with this target for this widget class
some widget classes can manage several target on the same instance
(e.g.: a chat widget with multiple resources on the same bare jid),
this method allow to return a hash associated to one or several targets
to retrieve the good instance. For example, a widget managing JID targets,
and all resource of the same bare jid would return the bare jid as hash.
@param target: target to check
@param profiles: profile(s) associated to target, see __init__ docstring
@return: a hash (can correspond to one or many targets or profiles, depending of widget class)
"""
return unicode(target) # by defaut, there is one hash for one target

View file

@ -22,12 +22,12 @@ class JID(unicode):
"""This class help manage JID (Node@Domaine/Resource)"""
def __new__(cls, jid):
self = unicode.__new__(cls, cls.__normalize(jid))
self.__parse()
self = unicode.__new__(cls, cls._normalize(jid))
self._parse()
return self
@classmethod
def __normalize(cls, jid):
def _normalize(cls, jid):
"""Naive normalization before instantiating and parsing the JID"""
if not jid:
return jid
@ -35,21 +35,25 @@ class JID(unicode):
tokens[0] = tokens[0].lower() # force node and domain to lower-case
return '/'.join(tokens)
def __parse(self):
@property
def bare(self):
if not self.node:
return JID(self.domain)
return JID(u"{}@{}".format(self.node, self.domain))
def _parse(self):
"""Find node domain and resource"""
node_end = self.find('@')
if node_end < 0:
node_end = 0
domain_end = self.find('/')
if domain_end < 1:
if domain_end == 0:
raise ValueError("a jid can't start with '/'")
if domain_end == -1:
domain_end = len(self)
self.node = self[:node_end]
self.node = self[:node_end] or None
self.domain = self[(node_end + 1) if node_end else 0:domain_end]
self.resource = self[domain_end + 1:]
if not node_end:
self.bare = self
else:
self.bare = self.node + '@' + self.domain
self.resource = self[domain_end + 1:] or None
def is_valid(self):
"""

View file

@ -202,7 +202,7 @@ class XMLUIBase(object):
This class must not be instancied directly
"""
def __init__(self, host, parsed_dom, title = None, flags = None):
def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
"""Initialise the XMLUI instance
@param host: %(doc_host)s
@ -210,15 +210,18 @@ class XMLUIBase(object):
@param title: force the title, or use XMLUI one if None
@param flags: list of string which can be:
- NO_CANCEL: the UI can't be cancelled
@param callback: if present, will be use with launchAction
"""
self.host = host
top=parsed_dom.documentElement
self.session_id = top.getAttribute("session_id") or None
self.submit_id = top.getAttribute("submit") or None
self.title = title or top.getAttribute("title") or u""
self.xmlui_title = title or top.getAttribute("title") or u""
if flags is None:
flags = []
self.flags = flags
self.callback = callback
self.profile = profile
def _isAttrSet(self, name, node):
"""Returnw widget boolean attribute status
@ -253,7 +256,7 @@ class XMLUIBase(object):
self._xmluiLaunchAction(self.submit_id, data)
def _xmluiLaunchAction(self, action_id, data):
self.host.launchAction(action_id, data, profile_key = self.host.profile)
self.host.launchAction(action_id, data, callback=self.callback, profile=self.profile)
class XMLUIPanel(XMLUIBase):
@ -265,8 +268,8 @@ class XMLUIPanel(XMLUIBase):
"""
widget_factory = None
def __init__(self, host, parsed_dom, title = None, flags = None):
super(XMLUIPanel, self).__init__(host, parsed_dom, title = None, flags = None)
def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
super(XMLUIPanel, self).__init__(host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile)
self.ctrl_list = {} # usefull to access ctrl
self._main_cont = None
self.constructUI(parsed_dom)
@ -462,7 +465,7 @@ class XMLUIPanel(XMLUIBase):
raise NotImplementedError
def _xmluiSetParam(self, name, value, category):
self.host.bridge.setParam(name, value, category, profile_key=self.host.profile)
self.host.bridge.setParam(name, value, category, profile=self.profile)
##EVENTS##
@ -639,8 +642,8 @@ class XMLUIPanel(XMLUIBase):
class XMLUIDialog(XMLUIBase):
dialog_factory = None
def __init__(self, host, parsed_dom, title = None, flags = None):
super(XMLUIDialog, self).__init__(host, parsed_dom, title = None, flags = None)
def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
super(XMLUIDialog, self).__init__(host, parsed_dom, title=None, flags=None, callback=callback, profile=C.PROF_KEY_NONE)
top=parsed_dom.documentElement
dlg_elt = self._getChildNode(top, "dialog")
if dlg_elt is None:
@ -654,23 +657,23 @@ class XMLUIDialog(XMLUIBase):
level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO
if dlg_type == C.XMLUI_DIALOG_MESSAGE:
self.dlg = self.dialog_factory.createMessageDialog(self, self.title, message, level)
self.dlg = self.dialog_factory.createMessageDialog(self, self.xmlui_title, message, level)
elif dlg_type == C.XMLUI_DIALOG_NOTE:
self.dlg = self.dialog_factory.createNoteDialog(self, self.title, message, level)
self.dlg = self.dialog_factory.createNoteDialog(self, self.xmlui_title, message, level)
elif dlg_type == C.XMLUI_DIALOG_CONFIRM:
try:
buttons_elt = self._getChildNode(dlg_elt, "buttons")
buttons_set = buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT
except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT
self.dlg = self.dialog_factory.createConfirmDialog(self, self.title, message, level, buttons_set)
self.dlg = self.dialog_factory.createConfirmDialog(self, self.xmlui_title, message, level, buttons_set)
elif dlg_type == C.XMLUI_DIALOG_FILE:
try:
file_elt = self._getChildNode(dlg_elt, "file")
filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT
except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
filetype = C.XMLUI_DATA_FILETYPE_DEFAULT
self.dlg = self.dialog_factory.createFileDialog(self, self.title, message, level, filetype)
self.dlg = self.dialog_factory.createFileDialog(self, self.xmlui_title, message, level, filetype)
else:
raise ValueError("Unknown dialog type [%s]" % dlg_type)
@ -690,7 +693,7 @@ def registerClass(type_, class_):
class_map[type_] = class_
def create(host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None):
def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, callback=None, profile=C.PROF_KEY_NONE):
"""
@param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one
@param dom_free: method used to free the parsed DOM
@ -713,6 +716,6 @@ def create(host, xml_data, title = None, flags = None, dom_parse=None, dom_free=
except KeyError:
raise ClassNotRegistedError(_("You must register classes with registerClass before creating a XMLUI"))
xmlui = cls(host, parsed_dom, title, flags)
xmlui = cls(host, parsed_dom, title, flags, callback, profile)
dom_free(parsed_dom)
return xmlui