view cagou/core/common.py @ 397:54f6a47cc60a

core (common): added a notifications counter on ContactButton and use it in JidSelector: A notifications counter is drawned above avatar if `nofifs` key is present and not empty in `data`. This is used in JidSelector to show counter when necessary with opened chats.
author Goffi <goffi@goffi.org>
date Sun, 09 Feb 2020 23:47:29 +0100
parents 442756495a96
children f7476818f9fb
line wrap: on
line source

#!/usr/bin/env python3

# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
# Copyright (C) 2016-2020 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/>.

"""common simple widgets"""

import json
from functools import partial
from sat.core.i18n import _
from sat.core import log as logging
from kivy.uix.widget import Widget
from kivy.uix.image import Image
from kivy.uix.label import Label
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView
from kivy.event import EventDispatcher
from kivy.metrics import dp
from kivy import properties
from sat_frontends.quick_frontend import quick_chat
from cagou.core.constants import Const as C
from cagou import G

log = logging.getLogger(__name__)

UNKNOWN_SYMBOL = 'Unknown symbol name'


class IconButton(ButtonBehavior, Image):
    pass


class Avatar(Image):
    pass


class NotifLabel(Label):
    pass


class ContactItem(BoxLayout):
    """An item from ContactList

    The item will drawn as an icon (JID avatar) with its jid below.
    If "notifs" are present in data, a notification counter will be drawn above the
    avatar.
    """
    base_width = dp(150)
    avatar_layout = properties.ObjectProperty()
    avatar = properties.ObjectProperty()
    profile = properties.StringProperty()
    data = properties.DictProperty()
    jid = properties.StringProperty('')

    def on_kv_post(self, __):
        if self.data and self.data.get('notifs'):
            notif = NotifLabel(
                pos_hint={"right": 0.8, "y": 0},
                text=str(len(self.data['notifs']))
            )
            self.avatar_layout.add_widget(notif)


class ContactButton(ButtonBehavior, ContactItem):
    pass


class JidItem(BoxLayout):
    bg_color = properties.ListProperty([0.2, 0.2, 0.2, 1])
    color = properties.ListProperty([1, 1, 1, 1])
    jid = properties.StringProperty()
    profile = properties.StringProperty()
    nick = properties.StringProperty()
    avatar = properties.ObjectProperty()

    def on_avatar(self, wid, jid_):
        if self.jid and self.profile:
            self.getImage()

    def on_jid(self, wid, jid_):
        if self.profile and self.avatar:
            self.getImage()

    def on_profile(self, wid, profile):
        if self.jid and self.avatar:
            self.getImage()

    def getImage(self):
        host = G.host
        if host.contact_lists[self.profile].isRoom(self.jid.bare):
            self.avatar.opacity = 0
            self.avatar.source = ""
        else:
            self.avatar.source = (
                host.getAvatar(self.jid, profile=self.profile)
                or host.getDefaultAvatar(self.jid)
            )


class JidButton(ButtonBehavior, JidItem):
    pass


class JidToggle(ToggleButtonBehavior, JidItem):
    selected_color = properties.ListProperty(C.COLOR_SEC_DARK)


class Symbol(Label):
    symbol_map = None
    symbol = properties.StringProperty()

    def __init__(self, **kwargs):
        if self.symbol_map is None:
            with open(G.host.app.expand('{media}/fonts/fontello/config.json')) as f:
                fontello_conf = json.load(f)
            Symbol.symbol_map = {g['css']:g['code'] for g in fontello_conf['glyphs']}

        super(Symbol, self).__init__(**kwargs)

    def on_symbol(self, instance, symbol):
        try:
            code = self.symbol_map[symbol]
        except KeyError:
            log.warning(_("Invalid symbol {symbol}").format(symbol=symbol))
        else:
            self.text = chr(code)


class SymbolButton(ButtonBehavior, Symbol):
    pass


class SymbolLabel(ButtonBehavior, BoxLayout):
    symbol = properties.StringProperty("")
    text = properties.StringProperty("")
    color = properties.ListProperty(C.COLOR_SEC)
    bold = properties.BooleanProperty(True)
    symbol_wid = properties.ObjectProperty()
    label = properties.ObjectProperty()


class ActionSymbol(Symbol):
    pass


class ActionIcon(BoxLayout):
    plugin_info = properties.DictProperty()

    def on_plugin_info(self, instance, plugin_info):
        self.clear_widgets()
        try:
            symbol = plugin_info['icon_symbol']
        except KeyError:
            icon_src = plugin_info['icon_medium']
            icon_wid = Image(source=icon_src, allow_stretch=True)
            self.add_widget(icon_wid)
        else:
            icon_wid = ActionSymbol(symbol=symbol)
            self.add_widget(icon_wid)


class JidSelector(ScrollView, EventDispatcher):
    layout = properties.ObjectProperty(None)
    # list of item to show, can be:
    #    - a well-known string like:
    #       * "roster": to show all roster jids
    #       * "opened_chats": to show jids of all opened chat widgets
    #    - a kivy Widget, which will be added to the layout (notable useful with
    #      common_widgets.CategorySeparator)
    #    - a callable, which must return an iterable of kwargs for ContactButton
    to_show = properties.ListProperty(['roster'])
    # if True, update() is called automatically when widget is created
    # if False, you'll have to call update() at least once manually
    implicit_update = properties.ObjectProperty(True)

    def __init__(self, **kwargs):
        self.register_event_type('on_select')
        super().__init__(**kwargs)

    def on_kv_post(self, wid):
        if self.implicit_update:
            self.update()

    def on_select(self, wid):
        pass

    def on_parent(self, wid, parent):
        if parent is None:
            log.debug("removing contactsFilled listener")
            G.host.removeListener("contactsFilled", self.onContactsFilled)
        else:
            G.host.addListener("contactsFilled", self.onContactsFilled)

    def onContactsFilled(self, profile):
        log.debug("onContactsFilled event received")
        self.update()

    def update(self):
        log.debug("starting update")
        self.layout.clear_widgets()
        for item in self.to_show:
            if isinstance(item, str):
                if item == 'roster':
                    self.addRosterItems()
                elif item == 'bookmarks':
                    self.addBookmarksItems()
                elif item == 'opened_chats':
                    self.addOpenedChatsItems()
                else:
                    log.error(f'unknown "to_show" magic string {item!r}')
            elif isinstance(item, Widget):
                self.layout.add_widget(item)
            elif callable(item):
                items_kwargs = item()
                for item_kwargs in items_kwargs:
                    item = ContactButton(**item_kwargs)
                    item.bind(on_press=partial(self.dispatch, 'on_select'))
                    self.layout.add_widget(item)
            else:
                log.error(f"unmanaged to_show item type: {item!r}")

    def addOpenedChatsItems(self):
        opened_chats = G.host.widgets.getWidgets(
            quick_chat.QuickChat,
            profiles = G.host.profiles)

        for wid in opened_chats:
            contact_list = G.host.contact_lists[wid.profile]
            data=contact_list.getItem(wid.target)
            notifs = list(G.host.getNotifs(wid.target, profile=wid.profile))
            if notifs:
                # we shallow copy the dict to have the notification displayed only with
                # opened chats (otherwise, the counter would appear on each other
                # instance of ContactButton for this entity, i.e. in roster too).
                data = data.copy()
                data['notifs'] = notifs
            try:
                item = ContactButton(
                    jid=wid.target,
                    data=data,
                    profile=wid.profile,
                )
            except Exception as e:
                log.warning(f"Can't add contact {wid.target}: {e}")
                continue
            item.bind(on_press=partial(self.dispatch, 'on_select'))
            self.layout.add_widget(item)

    def addRosterItems(self):
        for profile in G.host.profiles:
            contact_list = G.host.contact_lists[profile]
            for entity_jid in sorted(contact_list.roster):
                item = ContactButton(
                    jid=entity_jid,
                    data=contact_list.getItem(entity_jid),
                    profile=profile,
                )
                item.bind(on_press=partial(self.dispatch, 'on_select'))
                self.layout.add_widget(item)

    def addBookmarksItems(self):
        for profile in G.host.profiles:
            profile_manager = G.host.profiles[profile]
            try:
                bookmarks = profile_manager._bookmarks
            except AttributeError:
                log.warning(f"no bookmark in cache for profile {profile}")
                continue

            contact_list = G.host.contact_lists[profile]
            for entity_jid in bookmarks:
                try:
                    cache = contact_list.getItem(entity_jid)
                except KeyError:
                    cache = {}
                item = ContactButton(
                    jid=entity_jid,
                    data=cache,
                    profile=profile,
                )
                item.bind(on_press=partial(self.dispatch, 'on_select'))
                self.layout.add_widget(item)