view frontends/src/quick_frontend/quick_widgets.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 frontends/src/quick_frontend/quick_chat_list.py@75025461141f
children faa1129559b8
line wrap: on
line source

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

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

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

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

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

from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core import exceptions


classes_map = {}


def register(base_cls, child_cls=None):
    """Register a child class to use by default when a base class is needed

    @param base_cls: "Quick..." base class (like QuickChat or QuickContact), must inherit from QuickWidget
    @param child_cls: inherited class to use when Quick... class is requested, must inherit from base_cls.
        Can be None if it's the base_cls itself which register
    """
    classes_map[base_cls] = child_cls


class QuickWidgetsManager(object):
    """This class is used to manage all the widgets of a frontend
    A widget can be a window, a graphical thing, or someting else depending of the frontend"""

    def __init__(self, host):
        self.host = host
        self._widgets = {}

    def getOrCreateWidget(self, class_, target, *args, **kwargs):
        """Get an existing widget or create a new one when necessary

        If the widget is new, self.host.newWidget will be called with it.
        @param class_(class): class of the widget to create
        @param target: target depending of the widget, usually a JID instance
        @param args(list): optional args to create a new instance of class_
        @param kwargs(list): optional kwargs to create anew instance of class_
            if 'profile' key is present, it will be popped and put in 'profiles'
            if there is neither 'profile' nor 'profiles', None will be used for 'profiles'
            if 'on_new_widget' is present it can have the following values:
                'NEW_WIDGET' [default]: self.host.newWidget will be called on widget creation
                [callable]: this method will be called instead of self.host.newWidget
                None: do nothing
            if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash
        @return: a class_ instance, either new or already existing
        """
        # class management
        try:
            cls = classes_map[class_]
        except KeyError:
            cls = class_
        if cls is None:
            raise exceptions.InternalError("There is not class registered for {}".format(class_))

        # arguments management
        _args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args
        _kwargs = kwargs or {}
        if 'profiles' in _kwargs and 'profile' in _kwargs:
            raise ValueError("You can't have 'profile' and 'profiles' keys at the same time")
        try:
            _kwargs['profiles'] = _kwargs.pop('profile')
        except KeyError:
            if not 'profiles' in _kwargs:
                _kwargs['profiles'] = None

        # we get the hash
        try:
            hash_ = _kwargs.pop('force_hash')
        except KeyError:
            hash_ = cls.getWidgetHash(target, _kwargs['profiles'])

        # widget creation or retrieval
        widgets_list = self._widgets.setdefault(cls, {}) # we sorts widgets by classes
        if not cls.SINGLE:
            widget = None # if the class is not SINGLE, we always create a new widget
        else:
            try:
                widget = widgets_list[hash_]
                widget.addTarget(target)
            except KeyError:
                widget = None

        if widget is None:
            # we need to create a new widget
            try:
                #on_new_widget tell what to do for the new widget creation
                on_new_widget = _kwargs.pop('on_new_widget')
            except KeyError:
                on_new_widget = 'NEW_WIDGET'

            log.debug(u"Creating new widget for target {} {}".format(target, cls))
            widget = cls(*_args, **_kwargs)
            widgets_list[hash_] = widget

            if on_new_widget == 'NEW_WIDGET':
                self.host.newWidget(widget)
            elif callable(on_new_widget):
                on_new_widget(widget)
            else:
                assert on_new_widget is None

        return widget


class QuickWidget(object):
    """generic widget base"""
    SINGLE=True # if True, there can be only one widget per target(s)
    PROFILES_MULTIPLE=False
    PROFILES_ALLOW_NONE=False

    def __init__(self, host, target, profiles=None):
        """
        @param host: %(doc_host)s
        @param target: target specific for this widget class
        @param profiles: can be either:
            - (unicode): used when widget class manage a unique profile
            - (iterable): some widget class can manage several profiles, several at once can be specified here
            - None: no profile is managed by this widget class (rare)
        @raise: ValueError when (iterable) or None is given to profiles for a widget class which manage one unique profile.
        """
        self.host = host
        self.targets = set()
        self.addTarget(target)
        self.profiles = set()
        if isinstance(profiles, basestring):
            self.addProfile(profiles)
        elif profiles is None:
            if not self.PROFILES_ALLOW_NONE:
                raise ValueError("profiles can't have a value of None")
        else:
            if not self.PROFILES_MULTIPLE:
                raise ValueError("multiple profiles are not allowed")
            for profile in profiles:
                self.addProfile(profile)

    @property
    def profile(self):
        assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
        return list(self.profiles)[0]

    def addTarget(self, target):
        """Add a target if it doesn't already exists

        @param target: target to add
        """
        self.targets.add(target)

    def addProfile(self, profile):
        """Add a profile is if doesn't already exists

        @param profile: profile to add
        """
        if self.profiles and not self.PROFILES_MULTIPLE:
            raise ValueError("multiple profiles are not allowed")
        self.profiles.add(profile)

    @staticmethod
    def getWidgetHash(target, profiles):
        """Return the hash associated with this target for this widget class

        some widget classes can manage several target on the same instance
        (e.g.: a chat widget with multiple resources on the same bare jid),
        this method allow to return a hash associated to one or several targets
        to retrieve the good instance. For example, a widget managing JID targets,
        and all resource of the same bare jid would return the bare jid as hash.

        @param target: target to check
        @param profiles: profile(s) associated to target, see __init__ docstring
        @return: a hash (can correspond to one or many targets or profiles, depending of widget class)
        """
        return unicode(target) # by defaut, there is one hash for one target