view frontends/src/primitivus/profile_manager.py @ 1265:e3a9ea76de35 frontends_multi_profiles

quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p): This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments: - profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions - Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far - all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour - widgets are created in a dedicated manager, with facilities to react on new widget creation or other events - quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles - each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid - better management of CHAT_GROUP mode for Chat widgets - some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses - no more (un)escapePrivate/PRIVATE_PREFIX - contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed - resources can be displayed in Primitivus, and their status messages - profiles are managed in QuickFrontend with dedicated managers This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:00:09 +0100
parents 49d39b619e5d
children 7cf32aeeebdb
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Primitivus: a SAT frontend
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from sat.core.i18n import _
from sat.core import log as logging
log = logging.getLogger(__name__)
from sat_frontends.primitivus.constants import Const as C
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, autoconnect=None):
        """Create the manager

        @param host: %(doc_host)s
        @param autoconnect(iterable): list of profiles to connect automatically
        """
        self.host = host
        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 = sat_widgets.AdvancedEdit(_('Login:'), align='center')
        self.pass_wid = sat_widgets.Password(_('Password:'), align='center')

        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),
                  urwid.Button(_("Delete"), self.onDeleteProfile)]
        buttons_flow = urwid.GridFlow(buttons, max([len(button.get_label()) for button in buttons])+4, 1, 1, 'center')

        #second part: login information:
        divider = urwid.Divider('-')

        #connect button
        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])
        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']:
            self.host.onExit()
            raise urwid.ExitMainLoop()
        elif key in (a_key['FOCUS_UP'], a_key['FOCUS_DOWN']):
            focus_diff = 1 if key==a_key['FOCUS_DOWN'] else -1
            list_box = self.main_widget.base_widget.body
            current_focus = list_box.body.get_focus()[1]
            if current_focus is None:
                return
            while True:
                current_focus += focus_diff
                if current_focus < 0 or current_focus >= len(list_box.body):
                    break
                if list_box.body[current_focus].selectable():
                    list_box.set_focus(current_focus, 'above' if focus_diff == 1 else 'below')
                    list_box._invalidate()
                    return
        return super(ProfileManager, self).keypress(size, key)

    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()

    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)

    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):
        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 = _("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):
        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 = 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):
        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 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.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.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
        """
        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 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)
            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:
            # All profiles in the list are already validated, so we can plug them directly
            self.host.plug_profiles(profiles)

    def getParamError(self, dummy):
        popup = sat_widgets.Alert("Error", _("Can't get profile parameter"), ok_cb=self.host.removePopUp)
        self.host.showPopUp(popup)