From e4708c9cbb6d298333bdb3d2a7000ac6c886688e Mon Sep 17 00:00:00 2001 From: Goffi Date: Wed, 10 Dec 2014 19:00:09 +0100 Subject: [PATCH] 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ;)) --- frontends/src/bridge/DBus.py | 15 +- frontends/src/primitivus/chat.py | 135 ++--- frontends/src/primitivus/constants.py | 14 + frontends/src/primitivus/contact_list.py | 433 +++++++-------- frontends/src/primitivus/keys.py | 5 +- frontends/src/primitivus/primitivus | 288 +++++----- frontends/src/primitivus/profile_manager.py | 244 ++++++--- frontends/src/primitivus/progress.py | 8 +- frontends/src/primitivus/widget.py | 102 ++++ frontends/src/primitivus/xmlui.py | 14 +- frontends/src/quick_frontend/constants.py | 13 +- frontends/src/quick_frontend/quick_app.py | 505 +++++++++--------- frontends/src/quick_frontend/quick_chat.py | 135 +++-- .../src/quick_frontend/quick_chat_list.py | 39 -- .../src/quick_frontend/quick_contact_list.py | 339 ++++++++++-- frontends/src/quick_frontend/quick_utils.py | 13 - frontends/src/quick_frontend/quick_widgets.py | 187 +++++++ frontends/src/tools/jid.py | 26 +- frontends/src/tools/xmlui.py | 31 +- 19 files changed, 1580 insertions(+), 966 deletions(-) create mode 100644 frontends/src/primitivus/widget.py delete mode 100644 frontends/src/quick_frontend/quick_chat_list.py create mode 100644 frontends/src/quick_frontend/quick_widgets.py diff --git a/frontends/src/bridge/DBus.py b/frontends/src/bridge/DBus.py index 32ef6a88..d5a402b1 100644 --- a/frontends/src/bridge/DBus.py +++ b/frontends/src/bridge/DBus.py @@ -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) diff --git a/frontends/src/primitivus/chat.py b/frontends/src/primitivus/chat.py index 6a1ae7be..0b7c9d56 100644 --- a/frontends/src/primitivus/chat.py +++ b/frontends/src/primitivus/chat.py @@ -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) diff --git a/frontends/src/primitivus/constants.py b/frontends/src/primitivus/constants.py index 9bc4132d..68f915a0 100644 --- a/frontends/src/primitivus/constants.py +++ b/frontends/src/primitivus/constants.py @@ -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='(*) ' diff --git a/frontends/src/primitivus/contact_list.py b/frontends/src/primitivus/contact_list.py index 875a79d9..f7c39ca2 100644 --- a/frontends/src/primitivus/contact_list.py +++ b/frontends/src/primitivus/contact_list.py @@ -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) diff --git a/frontends/src/primitivus/keys.py b/frontends/src/primitivus/keys.py index eb9efc01..8191d186 100644 --- a/frontends/src/primitivus/keys.py +++ b/frontends/src/primitivus/keys.py @@ -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 diff --git a/frontends/src/primitivus/primitivus b/frontends/src/primitivus/primitivus index c804b63c..8a85d6de 100755 --- a/frontends/src/primitivus/primitivus +++ b/frontends/src/primitivus/primitivus @@ -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 ' 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 ' 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 diff --git a/frontends/src/primitivus/profile_manager.py b/frontends/src/primitivus/profile_manager.py index c0402b01..8e576c56 100644 --- a/frontends/src/primitivus/profile_manager.py +++ b/frontends/src/primitivus/profile_manager.py @@ -18,29 +18,69 @@ # along with this program. If not, see . 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) diff --git a/frontends/src/primitivus/progress.py b/frontends/src/primitivus/progress.py index 4ae78e0d..936d1ebb 100644 --- a/frontends/src/primitivus/progress.py +++ b/frontends/src/primitivus/progress.py @@ -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 = {} diff --git a/frontends/src/primitivus/widget.py b/frontends/src/primitivus/widget.py new file mode 100644 index 00000000..a254767f --- /dev/null +++ b/frontends/src/primitivus/widget.py @@ -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 . + +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 diff --git a/frontends/src/primitivus/xmlui.py b/frontends/src/primitivus/xmlui.py index bde1d790..c69a6ef7 100644 --- a/frontends/src/primitivus/xmlui.py +++ b/frontends/src/primitivus/xmlui.py @@ -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() diff --git a/frontends/src/quick_frontend/constants.py b/frontends/src/quick_frontend/constants.py index b1673255..18ca2893 100644 --- a/frontends/src/quick_frontend/constants.py +++ b/frontends/src/quick_frontend/constants.py @@ -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 diff --git a/frontends/src/quick_frontend/quick_app.py b/frontends/src/quick_frontend/quick_app.py index f53778d0..67398580 100644 --- a/frontends/src/quick_frontend/quick_app.py +++ b/frontends/src/quick_frontend/quick_app.py @@ -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) diff --git a/frontends/src/quick_frontend/quick_chat.py b/frontends/src/quick_frontend/quick_chat.py index f81d2256..5a22ffab 100644 --- a/frontends/src/quick_frontend/quick_chat.py +++ b/frontends/src/quick_frontend/quick_chat.py @@ -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) diff --git a/frontends/src/quick_frontend/quick_chat_list.py b/frontends/src/quick_frontend/quick_chat_list.py deleted file mode 100644 index f72d1887..00000000 --- a/frontends/src/quick_frontend/quick_chat_list.py +++ /dev/null @@ -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 . - -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 diff --git a/frontends/src/quick_frontend/quick_contact_list.py b/frontends/src/quick_frontend/quick_contact_list.py index d92da0a1..b6cd3ae5 100644 --- a/frontends/src/quick_frontend/quick_contact_list.py +++ b/frontends/src/quick_frontend/quick_contact_list.py @@ -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() diff --git a/frontends/src/quick_frontend/quick_utils.py b/frontends/src/quick_frontend/quick_utils.py index 62cd0eb6..18f9c430 100644 --- a/frontends/src/quick_frontend/quick_utils.py +++ b/frontends/src/quick_frontend/quick_utils.py @@ -17,20 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 """ diff --git a/frontends/src/quick_frontend/quick_widgets.py b/frontends/src/quick_frontend/quick_widgets.py new file mode 100644 index 00000000..72142cd7 --- /dev/null +++ b/frontends/src/quick_frontend/quick_widgets.py @@ -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 . + +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 diff --git a/frontends/src/tools/jid.py b/frontends/src/tools/jid.py index cd6e8344..c1d5e6a1 100644 --- a/frontends/src/tools/jid.py +++ b/frontends/src/tools/jid.py @@ -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): """ diff --git a/frontends/src/tools/xmlui.py b/frontends/src/tools/xmlui.py index 574f424f..46319ada 100644 --- a/frontends/src/tools/xmlui.py +++ b/frontends/src/tools/xmlui.py @@ -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