comparison cagou/core/common.py @ 404:f7476818f9fb

core (common): JidSelector + behaviors various improvments: - renamed *Behaviour => *Behavior to be consistent with Kivy + moved to new "core.behaviors" modules - use a dedicated property in ContactItem for notification counter (which is now named "badge") - in JidSelector, well-known strings now create use a dedicated layout, add separator (except if new `add_separators` property is set to False), and are added to attribute of the same name - a new `item_class` property is now used to indicate the class to instanciate for items (by default it's a ContactItem) - FilterBahavior.do_filter now expect the parent layout instead of directly the children, this is to allow a FilterBahavior to manage several children layout at once (used with JidSelector) - core.utils has been removed, as the behavior there has been moved to core.behaviors
author Goffi <goffi@goffi.org>
date Wed, 12 Feb 2020 20:02:58 +0100
parents 54f6a47cc60a
children 84ff5c917064
comparison
equal deleted inserted replaced
403:b0af45a92055 404:f7476818f9fb
25 from kivy.uix.widget import Widget 25 from kivy.uix.widget import Widget
26 from kivy.uix.image import Image 26 from kivy.uix.image import Image
27 from kivy.uix.label import Label 27 from kivy.uix.label import Label
28 from kivy.uix.behaviors import ButtonBehavior 28 from kivy.uix.behaviors import ButtonBehavior
29 from kivy.uix.behaviors import ToggleButtonBehavior 29 from kivy.uix.behaviors import ToggleButtonBehavior
30 from kivy.uix.stacklayout import StackLayout
30 from kivy.uix.boxlayout import BoxLayout 31 from kivy.uix.boxlayout import BoxLayout
31 from kivy.uix.scrollview import ScrollView 32 from kivy.uix.scrollview import ScrollView
32 from kivy.event import EventDispatcher 33 from kivy.event import EventDispatcher
33 from kivy.metrics import dp 34 from kivy.metrics import dp
34 from kivy import properties 35 from kivy import properties
35 from sat_frontends.quick_frontend import quick_chat 36 from sat_frontends.quick_frontend import quick_chat
36 from cagou.core.constants import Const as C 37 from .constants import Const as C
38 from .common_widgets import CategorySeparator
37 from cagou import G 39 from cagou import G
38 40
39 log = logging.getLogger(__name__) 41 log = logging.getLogger(__name__)
40 42
41 UNKNOWN_SYMBOL = 'Unknown symbol name' 43 UNKNOWN_SYMBOL = 'Unknown symbol name'
61 avatar. 63 avatar.
62 """ 64 """
63 base_width = dp(150) 65 base_width = dp(150)
64 avatar_layout = properties.ObjectProperty() 66 avatar_layout = properties.ObjectProperty()
65 avatar = properties.ObjectProperty() 67 avatar = properties.ObjectProperty()
68 badge = properties.ObjectProperty(allownone=True)
69 badge_text = properties.StringProperty('')
66 profile = properties.StringProperty() 70 profile = properties.StringProperty()
67 data = properties.DictProperty() 71 data = properties.DictProperty()
68 jid = properties.StringProperty('') 72 jid = properties.StringProperty('')
69 73
70 def on_kv_post(self, __): 74 def on_badge_text(self, wid, text):
71 if self.data and self.data.get('notifs'): 75 if text:
72 notif = NotifLabel( 76 if self.badge is not None:
73 pos_hint={"right": 0.8, "y": 0}, 77 self.badge.text = text
74 text=str(len(self.data['notifs'])) 78 else:
75 ) 79 self.badge = NotifLabel(
76 self.avatar_layout.add_widget(notif) 80 pos_hint={"right": 0.8, "y": 0},
81 text=text,
82 )
83 self.avatar_layout.add_widget(self.badge)
84 else:
85 if self.badge is not None:
86 self.avatar_layout.remove_widget(self.badge)
87 self.badge = None
77 88
78 89
79 class ContactButton(ButtonBehavior, ContactItem): 90 class ContactButton(ButtonBehavior, ContactItem):
80 pass 91 pass
81 92
172 else: 183 else:
173 icon_wid = ActionSymbol(symbol=symbol) 184 icon_wid = ActionSymbol(symbol=symbol)
174 self.add_widget(icon_wid) 185 self.add_widget(icon_wid)
175 186
176 187
188 class JidSelectorCategoryLayout(StackLayout):
189 pass
190
191
177 class JidSelector(ScrollView, EventDispatcher): 192 class JidSelector(ScrollView, EventDispatcher):
178 layout = properties.ObjectProperty(None) 193 layout = properties.ObjectProperty(None)
194 # if item_class is changed, the properties must be the same as for ContactButton
195 # and ordering must be supported
196 item_class = properties.ObjectProperty(ContactButton)
197 add_separators = properties.ObjectProperty(True)
179 # list of item to show, can be: 198 # list of item to show, can be:
180 # - a well-known string like: 199 # - a well-known string which can be:
181 # * "roster": to show all roster jids 200 # * "roster": all roster jids
182 # * "opened_chats": to show jids of all opened chat widgets 201 # * "opened_chats": all opened chat widgets
202 # * "bookmarks": MUC bookmarks
203 # A layout will be created each time and stored in the attribute of the same
204 # name.
205 # If add_separators is True, a CategorySeparator will be added on top of each
206 # layout.
183 # - a kivy Widget, which will be added to the layout (notable useful with 207 # - a kivy Widget, which will be added to the layout (notable useful with
184 # common_widgets.CategorySeparator) 208 # common_widgets.CategorySeparator)
185 # - a callable, which must return an iterable of kwargs for ContactButton 209 # - a callable, which must return an iterable of kwargs for ContactButton
186 to_show = properties.ListProperty(['roster']) 210 to_show = properties.ListProperty(['roster'])
187 # if True, update() is called automatically when widget is created 211 # if True, update() is called automatically when widget is created
188 # if False, you'll have to call update() at least once manually 212 # if False, you'll have to call update() at least once manually
189 implicit_update = properties.ObjectProperty(True) 213 implicit_update = properties.ObjectProperty(True)
190 214
191 def __init__(self, **kwargs): 215 def __init__(self, **kwargs):
192 self.register_event_type('on_select') 216 self.register_event_type('on_select')
217 # list of layouts containing items
218 self.items_layouts = []
219 # jid to list of ContactButton instances map
220 self.items_map = {}
193 super().__init__(**kwargs) 221 super().__init__(**kwargs)
194 222
195 def on_kv_post(self, wid): 223 def on_kv_post(self, wid):
196 if self.implicit_update: 224 if self.implicit_update:
197 self.update() 225 self.update()
207 G.host.addListener("contactsFilled", self.onContactsFilled) 235 G.host.addListener("contactsFilled", self.onContactsFilled)
208 236
209 def onContactsFilled(self, profile): 237 def onContactsFilled(self, profile):
210 log.debug("onContactsFilled event received") 238 log.debug("onContactsFilled event received")
211 self.update() 239 self.update()
240
241
242 def _createItem(self, **kwargs):
243 item = self.item_class(**kwargs)
244 jid = kwargs['jid']
245 self.items_map.setdefault(jid, []).append(item)
246 return item
212 247
213 def update(self): 248 def update(self):
214 log.debug("starting update") 249 log.debug("starting update")
215 self.layout.clear_widgets() 250 self.layout.clear_widgets()
216 for item in self.to_show: 251 for item in self.to_show:
226 elif isinstance(item, Widget): 261 elif isinstance(item, Widget):
227 self.layout.add_widget(item) 262 self.layout.add_widget(item)
228 elif callable(item): 263 elif callable(item):
229 items_kwargs = item() 264 items_kwargs = item()
230 for item_kwargs in items_kwargs: 265 for item_kwargs in items_kwargs:
231 item = ContactButton(**item_kwargs) 266 item = self._createItem(**items_kwargs)
232 item.bind(on_press=partial(self.dispatch, 'on_select')) 267 item.bind(on_press=partial(self.dispatch, 'on_select'))
233 self.layout.add_widget(item) 268 self.layout.add_widget(item)
234 else: 269 else:
235 log.error(f"unmanaged to_show item type: {item!r}") 270 log.error(f"unmanaged to_show item type: {item!r}")
236 271
272 def addCategoryLayout(self, label=None):
273 category_layout = JidSelectorCategoryLayout()
274
275 if label and self.add_separators:
276 category_layout.add_widget(CategorySeparator(text=label))
277
278 self.layout.add_widget(category_layout)
279 self.items_layouts.append(category_layout)
280 return category_layout
281
282 def getItemFromWid(self, wid):
283 """create JidSelector item from QuickChat widget"""
284 contact_list = G.host.contact_lists[wid.profile]
285 data=contact_list.getItem(wid.target)
286 try:
287 item = self._createItem(
288 jid=wid.target,
289 data=data,
290 profile=wid.profile,
291 )
292 except Exception as e:
293 log.warning(f"Can't add contact {wid.target}: {e}")
294 return
295 notifs = list(G.host.getNotifs(wid.target, profile=wid.profile))
296 if notifs:
297 item.badge_text = str(len(notifs))
298 item.bind(on_press=partial(self.dispatch, 'on_select'))
299 return item
300
237 def addOpenedChatsItems(self): 301 def addOpenedChatsItems(self):
238 opened_chats = G.host.widgets.getWidgets( 302 self.opened_chats = category_layout = self.addCategoryLayout(_("Opened chats"))
303 widgets = sorted(G.host.widgets.getWidgets(
239 quick_chat.QuickChat, 304 quick_chat.QuickChat,
240 profiles = G.host.profiles) 305 profiles = G.host.profiles,
241 306 with_duplicates=False))
242 for wid in opened_chats: 307
243 contact_list = G.host.contact_lists[wid.profile] 308 for wid in widgets:
244 data=contact_list.getItem(wid.target) 309 item = self.getItemFromWid(wid)
245 notifs = list(G.host.getNotifs(wid.target, profile=wid.profile)) 310 if item is None:
246 if notifs:
247 # we shallow copy the dict to have the notification displayed only with
248 # opened chats (otherwise, the counter would appear on each other
249 # instance of ContactButton for this entity, i.e. in roster too).
250 data = data.copy()
251 data['notifs'] = notifs
252 try:
253 item = ContactButton(
254 jid=wid.target,
255 data=data,
256 profile=wid.profile,
257 )
258 except Exception as e:
259 log.warning(f"Can't add contact {wid.target}: {e}")
260 continue 311 continue
261 item.bind(on_press=partial(self.dispatch, 'on_select')) 312 category_layout.add_widget(item)
262 self.layout.add_widget(item)
263 313
264 def addRosterItems(self): 314 def addRosterItems(self):
315 self.roster = category_layout = self.addCategoryLayout(_("Your contacts"))
265 for profile in G.host.profiles: 316 for profile in G.host.profiles:
266 contact_list = G.host.contact_lists[profile] 317 contact_list = G.host.contact_lists[profile]
267 for entity_jid in sorted(contact_list.roster): 318 for entity_jid in sorted(contact_list.roster):
268 item = ContactButton( 319 item = self._createItem(
269 jid=entity_jid, 320 jid=entity_jid,
270 data=contact_list.getItem(entity_jid), 321 data=contact_list.getItem(entity_jid),
271 profile=profile, 322 profile=profile,
272 ) 323 )
273 item.bind(on_press=partial(self.dispatch, 'on_select')) 324 item.bind(on_press=partial(self.dispatch, 'on_select'))
274 self.layout.add_widget(item) 325 category_layout.add_widget(item)
275 326
276 def addBookmarksItems(self): 327 def addBookmarksItems(self):
328 self.bookmarks = category_layout = self.addCategoryLayout(_("Your chat rooms"))
277 for profile in G.host.profiles: 329 for profile in G.host.profiles:
278 profile_manager = G.host.profiles[profile] 330 profile_manager = G.host.profiles[profile]
279 try: 331 try:
280 bookmarks = profile_manager._bookmarks 332 bookmarks = profile_manager._bookmarks
281 except AttributeError: 333 except AttributeError:
286 for entity_jid in bookmarks: 338 for entity_jid in bookmarks:
287 try: 339 try:
288 cache = contact_list.getItem(entity_jid) 340 cache = contact_list.getItem(entity_jid)
289 except KeyError: 341 except KeyError:
290 cache = {} 342 cache = {}
291 item = ContactButton( 343 item = self._createItem(
292 jid=entity_jid, 344 jid=entity_jid,
293 data=cache, 345 data=cache,
294 profile=profile, 346 profile=profile,
295 ) 347 )
296 item.bind(on_press=partial(self.dispatch, 'on_select')) 348 item.bind(on_press=partial(self.dispatch, 'on_select'))
297 self.layout.add_widget(item) 349 category_layout.add_widget(item)