comparison libervia/desktop_kivy/core/cagou_main.py @ 493:b3cedbee561d

refactoring: rename `cagou` to `libervia.desktop_kivy` + update imports and names following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 18:26:16 +0200
parents cagou/core/cagou_main.py@203755bbe0fe
children 232a723aae45
comparison
equal deleted inserted replaced
492:5114bbb5daa3 493:b3cedbee561d
1 #!/usr/bin/env python3
2
3 #Libervia Desktop-Kivy
4 # Copyright (C) 2016-2021 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19
20 import os.path
21 import glob
22 import sys
23 from pathlib import Path
24 from urllib import parse as urlparse
25 from functools import partial
26 from libervia.backend.core.i18n import _
27 from . import kivy_hack
28 kivy_hack.do_hack()
29 from .constants import Const as C
30 from libervia.backend.core import log as logging
31 from libervia.backend.core import exceptions
32 from libervia.frontends.quick_frontend.quick_app import QuickApp
33 from libervia.frontends.quick_frontend import quick_widgets
34 from libervia.frontends.quick_frontend import quick_chat
35 from libervia.frontends.quick_frontend import quick_utils
36 from libervia.frontends.tools import jid
37 from libervia.backend.tools import utils as libervia_utils
38 from libervia.backend.tools import config
39 from libervia.backend.tools.common import data_format
40 from libervia.backend.tools.common import dynamic_import
41 from libervia.backend.tools.common import files_utils
42 import kivy
43 kivy.require('1.11.0')
44 import kivy.support
45 main_config = config.parse_main_conf(log_filenames=True)
46 bridge_name = config.config_get(main_config, '', 'bridge', 'dbus')
47 # FIXME: event loop is choosen according to bridge_name, a better way should be used
48 if 'dbus' in bridge_name:
49 kivy.support.install_gobject_iteration()
50 elif bridge_name in ('pb', 'embedded'):
51 kivy.support.install_twisted_reactor()
52 from kivy.app import App
53 from kivy.lang import Builder
54 from kivy import properties
55 from . import xmlui
56 from .profile_manager import ProfileManager
57 from kivy.clock import Clock
58 from kivy.uix.label import Label
59 from kivy.uix.boxlayout import BoxLayout
60 from kivy.uix.floatlayout import FloatLayout
61 from kivy.uix.screenmanager import (ScreenManager, Screen,
62 FallOutTransition, RiseInTransition)
63 from kivy.uix.dropdown import DropDown
64 from kivy.uix.behaviors import ButtonBehavior
65 from kivy.core.window import Window
66 from kivy.animation import Animation
67 from kivy.metrics import dp
68 from .cagou_widget import LiberviaDesktopKivyWidget
69 from .share_widget import ShareWidget
70 from . import widgets_handler
71 from .common import IconButton
72 from . import dialog
73 from importlib import import_module
74 import libervia.backend
75 import libervia.desktop_kivy
76 import libervia.desktop_kivy.plugins
77 import libervia.desktop_kivy.kv
78
79
80 log = logging.getLogger(__name__)
81
82
83 try:
84 from plyer import notification
85 except ImportError:
86 notification = None
87 log.warning(_("Can't import plyer, some features disabled"))
88
89
90 ## platform specific settings ##
91
92 from . import platform_
93 local_platform = platform_.create()
94 local_platform.init_platform()
95
96
97 ## General Configuration ##
98
99 # we want white background by default
100 Window.clearcolor = (1, 1, 1, 1)
101
102
103 class NotifsIcon(IconButton):
104 notifs = properties.ListProperty()
105
106 def on_release(self):
107 callback, args, kwargs = self.notifs.pop(0)
108 callback(*args, **kwargs)
109
110 def add_notif(self, callback, *args, **kwargs):
111 self.notifs.append((callback, args, kwargs))
112
113
114 class Note(Label):
115 title = properties.StringProperty()
116 message = properties.StringProperty()
117 level = properties.OptionProperty(C.XMLUI_DATA_LVL_DEFAULT,
118 options=list(C.XMLUI_DATA_LVLS))
119 symbol = properties.StringProperty()
120 action = properties.ObjectProperty()
121
122
123 class NoteDrop(ButtonBehavior, BoxLayout):
124 title = properties.StringProperty()
125 message = properties.StringProperty()
126 level = properties.OptionProperty(C.XMLUI_DATA_LVL_DEFAULT,
127 options=list(C.XMLUI_DATA_LVLS))
128 symbol = properties.StringProperty()
129 action = properties.ObjectProperty()
130
131 def on_press(self):
132 if self.action is not None:
133 self.parent.parent.select(self.action)
134
135
136 class NotesDrop(DropDown):
137 clear_btn = properties.ObjectProperty()
138
139 def __init__(self, notes):
140 super(NotesDrop, self).__init__()
141 self.notes = notes
142
143 def open(self, widget):
144 self.clear_widgets()
145 for n in self.notes:
146 kwargs = {
147 'title': n.title,
148 'message': n.message,
149 'level': n.level
150 }
151 if n.symbol is not None:
152 kwargs['symbol'] = n.symbol
153 if n.action is not None:
154 kwargs['action'] = n.action
155 self.add_widget(NoteDrop(title=n.title, message=n.message, level=n.level,
156 symbol=n.symbol, action=n.action))
157 self.add_widget(self.clear_btn)
158 super(NotesDrop, self).open(widget)
159
160 def on_select(self, action_kwargs):
161 app = App.get_running_app()
162 app.host.do_action(**action_kwargs)
163
164
165 class RootHeadWidget(BoxLayout):
166 """Notifications widget"""
167 manager = properties.ObjectProperty()
168 notifs_icon = properties.ObjectProperty()
169 notes = properties.ListProperty()
170 HEIGHT = dp(35)
171
172 def __init__(self):
173 super(RootHeadWidget, self).__init__()
174 self.notes_last = None
175 self.notes_event = None
176 self.notes_drop = NotesDrop(self.notes)
177
178 def add_notif(self, callback, *args, **kwargs):
179 """add a notification with a callback attached
180
181 when notification is pressed, callback is called
182 @param *args, **kwargs: arguments of callback
183 """
184 self.notifs_icon.add_notif(callback, *args, **kwargs)
185
186 def add_note(self, title, message, level, symbol, action):
187 kwargs = {
188 'title': title,
189 'message': message,
190 'level': level
191 }
192 if symbol is not None:
193 kwargs['symbol'] = symbol
194 if action is not None:
195 kwargs['action'] = action
196 note = Note(**kwargs)
197 self.notes.append(note)
198 if self.notes_event is None:
199 self.notes_event = Clock.schedule_interval(self._display_next_note, 5)
200 self._display_next_note()
201
202 def add_notif_ui(self, ui):
203 self.notifs_icon.add_notif(ui.show, force=True)
204
205 def add_notif_widget(self, widget):
206 app = App.get_running_app()
207 self.notifs_icon.add_notif(app.host.show_extra_ui, widget=widget)
208
209 def _display_next_note(self, __=None):
210 screen = Screen()
211 try:
212 idx = self.notes.index(self.notes_last) + 1
213 except ValueError:
214 idx = 0
215 try:
216 note = self.notes_last = self.notes[idx]
217 except IndexError:
218 self.notes_event.cancel()
219 self.notes_event = None
220 else:
221 screen.add_widget(note)
222 self.manager.switch_to(screen)
223
224
225 class RootBody(BoxLayout):
226 pass
227
228
229 class LiberviaDesktopKivyRootWidget(FloatLayout):
230 root_body = properties.ObjectProperty
231
232 def __init__(self, main_widget):
233 super(LiberviaDesktopKivyRootWidget, self).__init__()
234 # header
235 self.head_widget = RootHeadWidget()
236 self.root_body.add_widget(self.head_widget)
237 # body
238 self._manager = ScreenManager()
239 # main widgets
240 main_screen = Screen(name='main')
241 main_screen.add_widget(main_widget)
242 self._manager.add_widget(main_screen)
243 # backend XMLUI (popups, forms, etc)
244 xmlui_screen = Screen(name='xmlui')
245 self._manager.add_widget(xmlui_screen)
246 # extra (file chooser, audio record, etc)
247 extra_screen = Screen(name='extra')
248 self._manager.add_widget(extra_screen)
249 self.root_body.add_widget(self._manager)
250
251 def change_widget(self, widget, screen_name="main"):
252 """change main widget"""
253 if self._manager.transition.is_active:
254 # FIXME: workaround for what seems a Kivy bug
255 # TODO: report this upstream
256 self._manager.transition.stop()
257 screen = self._manager.get_screen(screen_name)
258 screen.clear_widgets()
259 screen.add_widget(widget)
260
261 def show(self, screen="main"):
262 if self._manager.transition.is_active:
263 # FIXME: workaround for what seems a Kivy bug
264 # TODO: report this upstream
265 self._manager.transition.stop()
266 if self._manager.current == screen:
267 return
268 if screen == "main":
269 self._manager.transition = FallOutTransition()
270 else:
271 self._manager.transition = RiseInTransition()
272 self._manager.current = screen
273
274 def new_action(self, handler, action_data, id_, security_limit, profile):
275 """Add a notification for an action"""
276 self.head_widget.add_notif(handler, action_data, id_, security_limit, profile)
277
278 def add_note(self, title, message, level, symbol, action):
279 self.head_widget.add_note(title, message, level, symbol, action)
280
281 def add_notif_ui(self, ui):
282 self.head_widget.add_notif_ui(ui)
283
284 def add_notif_widget(self, widget):
285 self.head_widget.add_notif_widget(widget)
286
287
288 class LiberviaDesktopKivyApp(App):
289 """Kivy App for LiberviaDesktopKivy"""
290 c_prim = properties.ListProperty(C.COLOR_PRIM)
291 c_prim_light = properties.ListProperty(C.COLOR_PRIM_LIGHT)
292 c_prim_dark = properties.ListProperty(C.COLOR_PRIM_DARK)
293 c_sec = properties.ListProperty(C.COLOR_SEC)
294 c_sec_light = properties.ListProperty(C.COLOR_SEC_LIGHT)
295 c_sec_dark = properties.ListProperty(C.COLOR_SEC_DARK)
296 connected = properties.BooleanProperty(False)
297 # we have to put those constants here and not in core/constants.py
298 # because of the use of dp(), which would import Kivy too early
299 # and prevent the log hack
300 MARGIN_LEFT = MARGIN_RIGHT = dp(10)
301
302 def _install_settings_keys(self, window):
303 # we don't want default Kivy's behaviour of displaying
304 # a settings screen when pressing F1 or platform specific key
305 return
306
307 def build(self):
308 Window.bind(on_keyboard=self.key_input)
309 Window.bind(on_dropfile=self.on_dropfile)
310 wid = LiberviaDesktopKivyRootWidget(Label(text=_("Loading please wait")))
311 local_platform.on_app_build(wid)
312 return wid
313
314 def show_profile_manager(self):
315 self._profile_manager = ProfileManager()
316 self.root.change_widget(self._profile_manager)
317
318 def expand(self, path, *args, **kwargs):
319 """expand path and replace known values
320
321 useful in kv. Values which can be used:
322 - {media}: media dir
323 @param path(unicode): path to expand
324 @param *args: additional arguments used in format
325 @param **kwargs: additional keyword arguments used in format
326 """
327 return os.path.expanduser(path).format(*args, media=self.host.media_dir, **kwargs)
328
329 def init_frontend_state(self):
330 """Init state to handle paused/stopped/running on mobile OSes"""
331 local_platform.on_init_frontend_state()
332
333 def on_pause(self):
334 return local_platform.on_pause()
335
336 def on_resume(self):
337 return local_platform.on_resume()
338
339 def on_stop(self):
340 return local_platform.on_stop()
341
342 def show_head_widget(self, show=None, animation=True):
343 """Show/Hide the head widget
344
345 @param show(bool, None): True to show, False to hide, None to switch
346 @param animation(bool): animate the show/hide if True
347 """
348 head = self.root.head_widget
349 if bool(self.root.head_widget.height) == show:
350 return
351 if head.height:
352 if animation:
353 Animation(height=0, opacity=0, duration=0.3).start(head)
354 else:
355 head.height = head.opacity = 0
356 else:
357 if animation:
358 Animation(height=head.HEIGHT, opacity=1, duration=0.3).start(head)
359 else:
360 head.height = head.HEIGHT
361 head.opacity = 1
362
363 def key_input(self, window, key, scancode, codepoint, modifier):
364
365 # we first check if selected widget handles the key
366 if ((self.host.selected_widget is not None
367 and hasattr(self.host.selected_widget, 'key_input')
368 and self.host.selected_widget.key_input(window, key, scancode, codepoint,
369 modifier))):
370 return True
371
372 if key == 27:
373 if ((self.host.selected_widget is None
374 or self.host.selected_widget.__class__ == self.host.default_class)):
375 # we are on root widget, or nothing is selected
376 return local_platform.on_key_back_root()
377
378 # we disable [esc] handling, because default action is to quit app
379 return True
380 elif key == 292:
381 # F11: full screen
382 if not Window.fullscreen:
383 Window.fullscreen = 'auto'
384 else:
385 Window.fullscreen = False
386 return True
387 elif key == 110 and 'alt' in modifier:
388 # M-n we hide/show notifications
389 self.show_head_widget()
390 return True
391 else:
392 return False
393
394 def on_dropfile(self, __, file_path):
395 if self.host.selected_widget is not None:
396 try:
397 on_drop_file = self.host.selected_widget.on_drop_file
398 except AttributeError:
399 log.info(
400 f"Select widget {self.host.selected_widget} doesn't handle file "
401 f"dropping")
402 else:
403 on_drop_file(Path(file_path.decode()))
404
405
406 class LiberviaDesktopKivy(QuickApp):
407 MB_HANDLE = False
408 AUTO_RESYNC = False
409
410 def __init__(self):
411 if bridge_name == 'embedded':
412 from libervia.backend.core import main
413 self.libervia_backend = main.LiberviaBackend()
414
415 bridge_module = dynamic_import.bridge(bridge_name, 'libervia.frontends.bridge')
416 if bridge_module is None:
417 log.error(f"Can't import {bridge_name} bridge")
418 sys.exit(3)
419 else:
420 log.debug(f"Loading {bridge_name} bridge")
421 super(LiberviaDesktopKivy, self).__init__(bridge_factory=bridge_module.bridge,
422 xmlui=xmlui,
423 check_options=quick_utils.check_options,
424 connect_bridge=False)
425 self._import_kv()
426 self.app = LiberviaDesktopKivyApp()
427 self.app.host = self
428 self.media_dir = self.app.media_dir = config.config_get(main_config, '',
429 'media_dir')
430 self.downloads_dir = self.app.downloads_dir = config.config_get(main_config, '',
431 'downloads_dir')
432 if not os.path.exists(self.downloads_dir):
433 try:
434 os.makedirs(self.downloads_dir)
435 except OSError as e:
436 log.warnings(_("Can't create downloads dir: {reason}").format(reason=e))
437 self.app.default_avatar = os.path.join(self.media_dir, "misc/default_avatar.png")
438 self.app.icon = os.path.join(
439 self.media_dir, "icons/muchoslava/png/cagou_profil_bleu_96.png"
440 )
441 self.app.title = C.APP_NAME
442 # main widgets plugins
443 self._plg_wids = []
444 # transfer widgets plugins
445 self._plg_wids_transfer = []
446 self._import_plugins()
447 # visible widgets by classes
448 self._visible_widgets = {}
449 # used to keep track of last selected widget in "main" screen when changing
450 # root screen
451 self._selected_widget_main = None
452 self.backend_version = libervia.backend.__version__ # will be replaced by version_get()
453 if C.APP_VERSION.endswith('D'):
454 self.version = "{} {}".format(
455 C.APP_VERSION,
456 libervia_utils.get_repository_data(libervia.desktop_kivy)
457 )
458 else:
459 self.version = C.APP_VERSION
460
461 self.tls_validation = not C.bool(config.config_get(main_config,
462 C.CONFIG_SECTION,
463 'no_certificate_validation',
464 C.BOOL_FALSE))
465 if not self.tls_validation:
466 from libervia.desktop_kivy.core import patches
467 patches.disable_tls_validation()
468 log.warning("SSL certificate validation is disabled, this is unsecure!")
469
470 local_platform.on_host_init(self)
471
472 @property
473 def visible_widgets(self):
474 for w_list in self._visible_widgets.values():
475 for w in w_list:
476 yield w
477
478 @property
479 def default_class(self):
480 if self.default_wid is None:
481 return None
482 return self.default_wid['main']
483
484 @QuickApp.sync.setter
485 def sync(self, state):
486 QuickApp.sync.fset(self, state)
487 # widget are resynchronised in on_visible event,
488 # so we must call resync for widgets which are already visible
489 if state:
490 for w in self.visible_widgets:
491 try:
492 resync = w.resync
493 except AttributeError:
494 pass
495 else:
496 resync()
497 self.contact_lists.fill()
498
499 def config_get(self, section, name, default=None):
500 return config.config_get(main_config, section, name, default)
501
502 def on_bridge_connected(self):
503 super(LiberviaDesktopKivy, self).on_bridge_connected()
504 self.register_signal("otr_state", iface="plugin")
505
506 def _bridge_eb(self, failure):
507 if bridge_name == "pb" and sys.platform == "android":
508 try:
509 self.retried += 1
510 except AttributeError:
511 self.retried = 1
512 if ((isinstance(failure, exceptions.BridgeExceptionNoService)
513 and self.retried < 100)):
514 if self.retried % 20 == 0:
515 log.debug("backend not ready, retrying ({})".format(self.retried))
516 Clock.schedule_once(lambda __: self.connect_bridge(), 0.05)
517 return
518 super(LiberviaDesktopKivy, self)._bridge_eb(failure)
519
520 def run(self):
521 self.connect_bridge()
522 self.app.bind(on_stop=self.onStop)
523 self.app.run()
524
525 def onStop(self, obj):
526 try:
527 libervia_instance = self.libervia_backend
528 except AttributeError:
529 pass
530 else:
531 libervia_instance.stopService()
532
533 def _get_version_cb(self, version):
534 self.backend_version = version
535
536 def on_backend_ready(self):
537 super().on_backend_ready()
538 self.app.show_profile_manager()
539 self.bridge.version_get(callback=self._get_version_cb)
540 self.app.init_frontend_state()
541 if local_platform.do_post_init():
542 self.post_init()
543
544 def post_init(self, __=None):
545 # FIXME: resize doesn't work with SDL2 on android, so we use below_target for now
546 self.app.root_window.softinput_mode = "below_target"
547 profile_manager = self.app._profile_manager
548 del self.app._profile_manager
549 super(LiberviaDesktopKivy, self).post_init(profile_manager)
550
551 def profile_plugged(self, profile):
552 super().profile_plugged(profile)
553 # FIXME: this won't work with multiple profiles
554 self.app.connected = self.profiles[profile].connected
555
556 def _bookmarks_list_cb(self, bookmarks_dict, profile):
557 bookmarks = set()
558 for data in bookmarks_dict.values():
559 bookmarks.update({jid.JID(k) for k in data.keys()})
560 self.profiles[profile]._bookmarks = sorted(bookmarks)
561
562 def profile_connected(self, profile):
563 self.bridge.bookmarks_list(
564 "muc", "all", profile,
565 callback=partial(self._bookmarks_list_cb, profile=profile),
566 errback=partial(self.errback, title=_("Bookmark error")))
567
568 def _default_factory_main(self, plugin_info, target, profiles):
569 """default factory used to create main widgets instances
570
571 used when PLUGIN_INFO["factory"] is not set
572 @param plugin_info(dict): plugin datas
573 @param target: QuickWidget target
574 @param profiles(iterable): list of profiles
575 """
576 main_cls = plugin_info['main']
577 return self.widgets.get_or_create_widget(main_cls,
578 target,
579 on_new_widget=None,
580 profiles=iter(self.profiles))
581
582 def _default_factory_transfer(self, plugin_info, callback, cancel_cb, profiles):
583 """default factory used to create transfer widgets instances
584
585 @param plugin_info(dict): plugin datas
586 @param callback(callable): method to call with path to file to transfer
587 @param cancel_cb(callable): call when transfer is cancelled
588 transfer widget must be used as first argument
589 @param profiles(iterable): list of profiles
590 None if not specified
591 """
592 main_cls = plugin_info['main']
593 return main_cls(callback=callback, cancel_cb=cancel_cb)
594
595 ## plugins & kv import ##
596
597 def _import_kv(self):
598 """import all kv files in libervia.desktop_kivy.kv"""
599 path = os.path.dirname(libervia.desktop_kivy.kv.__file__)
600 kv_files = glob.glob(os.path.join(path, "*.kv"))
601 # we want to be sure that base.kv is loaded first
602 # as it override some Kivy widgets properties
603 for kv_file in kv_files:
604 if kv_file.endswith('base.kv'):
605 kv_files.remove(kv_file)
606 kv_files.insert(0, kv_file)
607 break
608 else:
609 raise exceptions.InternalError("base.kv is missing")
610
611 for kv_file in kv_files:
612 Builder.load_file(kv_file)
613 log.debug(f"kv file {kv_file} loaded")
614
615 def _import_plugins(self):
616 """import all plugins"""
617 self.default_wid = None
618 plugins_path = os.path.dirname(libervia.desktop_kivy.plugins.__file__)
619 plugin_glob = "plugin*." + C.PLUGIN_EXT
620 plug_lst = [os.path.splitext(p)[0] for p in
621 map(os.path.basename, glob.glob(os.path.join(plugins_path,
622 plugin_glob)))]
623
624 imported_names_main = set() # used to avoid loading 2 times
625 # plugin with same import name
626 imported_names_transfer = set()
627 for plug in plug_lst:
628 plugin_path = 'libervia.desktop_kivy.plugins.' + plug
629
630 # we get type from plugin name
631 suff = plug[7:]
632 if '_' not in suff:
633 log.error("invalid plugin name: {}, skipping".format(plug))
634 continue
635 plugin_type = suff[:suff.find('_')]
636
637 # and select the variable to use according to type
638 if plugin_type == C.PLUG_TYPE_WID:
639 imported_names = imported_names_main
640 default_factory = self._default_factory_main
641 elif plugin_type == C.PLUG_TYPE_TRANSFER:
642 imported_names = imported_names_transfer
643 default_factory = self._default_factory_transfer
644 else:
645 log.error("unknown plugin type {type_} for plugin {file_}, skipping"
646 .format(
647 type_ = plugin_type,
648 file_ = plug
649 ))
650 continue
651 plugins_set = self._get_plugins_set(plugin_type)
652
653 mod = import_module(plugin_path)
654 try:
655 plugin_info = mod.PLUGIN_INFO
656 except AttributeError:
657 plugin_info = {}
658
659 plugin_info['plugin_file'] = plug
660 plugin_info['plugin_type'] = plugin_type
661
662 if 'platforms' in plugin_info:
663 if sys.platform not in plugin_info['platforms']:
664 log.info("{plugin_file} is not used on this platform, skipping"
665 .format(**plugin_info))
666 continue
667
668 # import name is used to differentiate plugins
669 if 'import_name' not in plugin_info:
670 plugin_info['import_name'] = plug
671 if plugin_info['import_name'] in imported_names:
672 log.warning(_("there is already a plugin named {}, "
673 "ignoring new one").format(plugin_info['import_name']))
674 continue
675 if plugin_info['import_name'] == C.WID_SELECTOR:
676 if plugin_type != C.PLUG_TYPE_WID:
677 log.error("{import_name} import name can only be used with {type_} "
678 "type, skipping {name}".format(type_=C.PLUG_TYPE_WID,
679 **plugin_info))
680 continue
681 # if WidgetSelector exists, it will be our default widget
682 self.default_wid = plugin_info
683
684 # we want everything optional, so we use plugin file name
685 # if actual name is not found
686 if 'name' not in plugin_info:
687 name_start = 8 + len(plugin_type)
688 plugin_info['name'] = plug[name_start:]
689
690 # we need to load the kv file
691 if 'kv_file' not in plugin_info:
692 plugin_info['kv_file'] = '{}.kv'.format(plug)
693 kv_path = os.path.join(plugins_path, plugin_info['kv_file'])
694 if not os.path.exists(kv_path):
695 log.debug("no kv found for {plugin_file}".format(**plugin_info))
696 else:
697 Builder.load_file(kv_path)
698
699 # what is the main class ?
700 main_cls = getattr(mod, plugin_info['main'])
701 plugin_info['main'] = main_cls
702
703 # factory is used to create the instance
704 # if not found, we use a defaut one with get_or_create_widget
705 if 'factory' not in plugin_info:
706 plugin_info['factory'] = default_factory
707
708 # icons
709 for size in ('small', 'medium'):
710 key = 'icon_{}'.format(size)
711 try:
712 path = plugin_info[key]
713 except KeyError:
714 path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir)
715 else:
716 path = path.format(media=self.media_dir)
717 if not os.path.isfile(path):
718 path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir)
719 plugin_info[key] = path
720
721 plugins_set.append(plugin_info)
722 if not self._plg_wids:
723 log.error(_("no widget plugin found"))
724 return
725
726 # we want widgets sorted by names
727 self._plg_wids.sort(key=lambda p: p['name'].lower())
728 self._plg_wids_transfer.sort(key=lambda p: p['name'].lower())
729
730 if self.default_wid is None:
731 # we have no selector widget, we use the first widget as default
732 self.default_wid = self._plg_wids[0]
733
734 def _get_plugins_set(self, type_):
735 if type_ == C.PLUG_TYPE_WID:
736 return self._plg_wids
737 elif type_ == C.PLUG_TYPE_TRANSFER:
738 return self._plg_wids_transfer
739 else:
740 raise KeyError("{} plugin type is unknown".format(type_))
741
742 def get_plugged_widgets(self, type_=C.PLUG_TYPE_WID, except_cls=None):
743 """get available widgets plugin infos
744
745 @param type_(unicode): type of widgets to get
746 one of C.PLUG_TYPE_* constant
747 @param except_cls(None, class): if not None,
748 widgets from this class will be excluded
749 @return (iter[dict]): available widgets plugin infos
750 """
751 plugins_set = self._get_plugins_set(type_)
752 for plugin_data in plugins_set:
753 if plugin_data['main'] == except_cls:
754 continue
755 yield plugin_data
756
757 def get_plugin_info(self, type_=C.PLUG_TYPE_WID, **kwargs):
758 """get first plugin info corresponding to filters
759
760 @param type_(unicode): type of widgets to get
761 one of C.PLUG_TYPE_* constant
762 @param **kwargs: filter(s) to use, each key present here must also
763 exist and be of the same value in requested plugin info
764 @return (dict, None): found plugin info or None
765 """
766 plugins_set = self._get_plugins_set(type_)
767 for plugin_info in plugins_set:
768 for k, w in kwargs.items():
769 try:
770 if plugin_info[k] != w:
771 continue
772 except KeyError:
773 continue
774 return plugin_info
775
776 ## widgets handling
777
778 def new_widget(self, widget):
779 log.debug("new widget created: {}".format(widget))
780 if isinstance(widget, quick_chat.QuickChat) and widget.type == C.CHAT_GROUP:
781 self.add_note("", _("room {} has been joined").format(widget.target))
782
783 def switch_widget(self, old, new=None):
784 """Replace old widget by new one
785
786 @param old(LiberviaDesktopKivyWidget, None): LiberviaDesktopKivyWidget instance or a child
787 None to select automatically widget to switch
788 @param new(LiberviaDesktopKivyWidget): new widget instance
789 None to use default widget
790 @return (LiberviaDesktopKivyWidget): new widget
791 """
792 if old is None:
793 old = self.get_widget_to_switch()
794 if new is None:
795 factory = self.default_wid['factory']
796 try:
797 profiles = old.profiles
798 except AttributeError:
799 profiles = None
800 new = factory(self.default_wid, None, profiles=profiles)
801 to_change = None
802 if isinstance(old, LiberviaDesktopKivyWidget):
803 to_change = old
804 else:
805 for w in old.walk_reverse():
806 if isinstance(w, LiberviaDesktopKivyWidget):
807 to_change = w
808 break
809
810 if to_change is None:
811 raise exceptions.InternalError("no LiberviaDesktopKivyWidget found when "
812 "trying to switch widget")
813
814 # selected_widget can be modified in change_widget, so we need to set it before
815 self.selected_widget = new
816 if to_change == new:
817 log.debug("switch_widget called with old==new, nothing to do")
818 return new
819 to_change.whwrapper.change_widget(new)
820 return new
821
822 def _add_visible_widget(self, widget):
823 """declare a widget visible
824
825 for internal use only!
826 """
827 assert isinstance(widget, LiberviaDesktopKivyWidget)
828 log.debug(f"Visible widget: {widget}")
829 self._visible_widgets.setdefault(widget.__class__, set()).add(widget)
830 log.debug(f"visible widgets list: {self.get_visible_list(None)}")
831 widget.on_visible()
832
833 def _remove_visible_widget(self, widget, ignore_missing=False):
834 """declare a widget not visible anymore
835
836 for internal use only!
837 """
838 log.debug(f"Widget not visible anymore: {widget}")
839 try:
840 self._visible_widgets[widget.__class__].remove(widget)
841 except KeyError as e:
842 if not ignore_missing:
843 log.error(f"trying to remove a not visible widget ({widget}): {e}")
844 return
845 log.debug(f"visible widgets list: {self.get_visible_list(None)}")
846 if isinstance(widget, LiberviaDesktopKivyWidget):
847 widget.on_not_visible()
848 if isinstance(widget, quick_widgets.QuickWidget):
849 self.widgets.delete_widget(widget)
850
851 def get_visible_list(self, cls):
852 """get list of visible widgets for a given class
853
854 @param cls(type): type of widgets to get
855 None to get all visible widgets
856 @return (set[type]): visible widgets of this class
857 """
858 if cls is None:
859 ret = set()
860 for widgets in self._visible_widgets.values():
861 for w in widgets:
862 ret.add(w)
863 return ret
864 try:
865 return self._visible_widgets[cls]
866 except KeyError:
867 return set()
868
869 def delete_unused_widget_instances(self, widget):
870 """Delete instance of this widget which are not attached to a WHWrapper
871
872 @param widget(quick_widgets.QuickWidget): reference widget
873 other instance of this widget will be deleted if they have no parent
874 """
875 to_delete = []
876 if isinstance(widget, quick_widgets.QuickWidget):
877 for w in self.widgets.get_widget_instances(widget):
878 if w.whwrapper is None and w != widget:
879 to_delete.append(w)
880 for w in to_delete:
881 log.debug("cleaning widget: {wid}".format(wid=w))
882 self.widgets.delete_widget(w)
883
884 def get_or_clone(self, widget, **kwargs):
885 """Get a QuickWidget if it is not in a WHWrapper, else clone it
886
887 if an other instance of this widget exist without being in a WHWrapper
888 (i.e. if it is not already in use) it will be used.
889 """
890 if widget.whwrapper is None:
891 if widget.parent is not None:
892 widget.parent.remove_widget(widget)
893 self.delete_unused_widget_instances(widget)
894 return widget
895 for w in self.widgets.get_widget_instances(widget):
896 if w.whwrapper is None:
897 if w.parent is not None:
898 w.parent.remove_widget(w)
899 self.delete_unused_widget_instances(w)
900 return w
901 targets = list(widget.targets)
902 w = self.widgets.get_or_create_widget(widget.__class__,
903 targets[0],
904 on_new_widget=None,
905 on_existing_widget=C.WIDGET_RECREATE,
906 profiles=widget.profiles,
907 **kwargs)
908 for t in targets[1:]:
909 w.add_target(t)
910 return w
911
912 def get_widget_to_switch(self):
913 """Choose best candidate when we need to switch widget and old is not specified
914
915 @return (LiberviaDesktopKivyWidget): widget to switch
916 """
917 if (self._selected_widget_main is not None
918 and self._selected_widget_main.whwrapper is not None):
919 # we are not on the main screen, we want to switch a widget from main screen
920 return self._selected_widget_main
921 elif (self.selected_widget is not None
922 and isinstance(self.selected_widget, LiberviaDesktopKivyWidget)
923 and self.selected_widget.whwrapper is not None):
924 return self.selected_widget
925 # no widget is selected we check if we have any default widget
926 default_cls = self.default_class
927 for w in self.visible_widgets:
928 if isinstance(w, default_cls):
929 return w
930
931 # no default widget found, we return the first widget
932 return next(iter(self.visible_widgets))
933
934 def do_action(self, action, target, profiles):
935 """Launch an action handler by a plugin
936
937 @param action(unicode): action to do, can be:
938 - chat: open a chat widget
939 @param target(unicode): target of the action
940 @param profiles(list[unicode]): profiles to use
941 @return (LiberviaDesktopKivyWidget, None): new widget
942 """
943 try:
944 # FIXME: Q&D way to get chat plugin, should be replaced by a clean method
945 # in host
946 plg_infos = [p for p in self.get_plugged_widgets()
947 if action in p['import_name']][0]
948 except IndexError:
949 log.warning("No plugin widget found to do {action}".format(action=action))
950 else:
951 try:
952 # does the widget already exist?
953 wid = next(self.widgets.get_widgets(
954 plg_infos['main'],
955 target=target,
956 profiles=profiles))
957 except StopIteration:
958 # no, let's create a new one
959 factory = plg_infos['factory']
960 wid = factory(plg_infos, target=target, profiles=profiles)
961
962 return self.switch_widget(None, wid)
963
964 ## bridge handlers ##
965
966 def otr_state_handler(self, state, dest_jid, profile):
967 """OTR state has changed for on destinee"""
968 # XXX: this method could be in QuickApp but it's here as
969 # it's only used by LiberviaDesktopKivy so far
970 dest_jid = jid.JID(dest_jid)
971 bare_jid = dest_jid.bare
972 for widget in self.widgets.get_widgets(quick_chat.QuickChat, profiles=(profile,)):
973 if widget.type == C.CHAT_ONE2ONE and widget.target == bare_jid:
974 widget.on_otr_state(state, dest_jid, profile)
975
976 def _debug_handler(self, action, parameters, profile):
977 if action == "visible_widgets_dump":
978 from pprint import pformat
979 log.info("Visible widgets dump:\n{data}".format(
980 data=pformat(self._visible_widgets)))
981 else:
982 return super(LiberviaDesktopKivy, self)._debug_handler(action, parameters, profile)
983
984 def connected_handler(self, jid_s, profile):
985 # FIXME: this won't work with multiple profiles
986 super().connected_handler(jid_s, profile)
987 self.app.connected = True
988
989 def disconnected_handler(self, profile):
990 # FIXME: this won't work with multiple profiles
991 super().disconnected_handler(profile)
992 self.app.connected = False
993
994 ## misc ##
995
996 def plugging_profiles(self):
997 self.widgets_handler = widgets_handler.WidgetsHandler()
998 self.app.root.change_widget(self.widgets_handler)
999
1000 def set_presence_status(self, show='', status=None, profile=C.PROF_KEY_NONE):
1001 log.info("Profile presence status set to {show}/{status}".format(show=show,
1002 status=status))
1003
1004 def errback(self, failure_, title=_('error'),
1005 message=_('error while processing: {msg}')):
1006 self.add_note(title, message.format(msg=failure_), level=C.XMLUI_DATA_LVL_WARNING)
1007
1008 def add_note(self, title, message, level=C.XMLUI_DATA_LVL_INFO, symbol=None,
1009 action=None):
1010 """add a note (message which disappear) to root widget's header"""
1011 self.app.root.add_note(title, message, level, symbol, action)
1012
1013 def add_notif_ui(self, ui):
1014 """add a notification with a XMLUI attached
1015
1016 @param ui(xmlui.XMLUIPanel): XMLUI instance to show when notification is selected
1017 """
1018 self.app.root.add_notif_ui(ui)
1019
1020 def add_notif_widget(self, widget):
1021 """add a notification with a Kivy widget attached
1022
1023 @param widget(kivy.uix.Widget): widget to attach to notification
1024 """
1025 self.app.root.add_notif_widget(widget)
1026
1027 def show_ui(self, ui):
1028 """show a XMLUI"""
1029 self.app.root.change_widget(ui, "xmlui")
1030 self.app.root.show("xmlui")
1031 self._selected_widget_main = self.selected_widget
1032 self.selected_widget = ui
1033
1034 def show_extra_ui(self, widget):
1035 """show any extra widget"""
1036 self.app.root.change_widget(widget, "extra")
1037 self.app.root.show("extra")
1038 self._selected_widget_main = self.selected_widget
1039 self.selected_widget = widget
1040
1041 def close_ui(self):
1042 self.app.root.show()
1043 self.selected_widget = self._selected_widget_main
1044 self._selected_widget_main = None
1045 screen = self.app.root._manager.get_screen("extra")
1046 screen.clear_widgets()
1047
1048 def get_default_avatar(self, entity=None):
1049 return self.app.default_avatar
1050
1051 def _dialog_cb(self, cb, *args, **kwargs):
1052 """generic dialog callback
1053
1054 close dialog then call the callback with given arguments
1055 """
1056 def callback():
1057 self.close_ui()
1058 cb(*args, **kwargs)
1059 return callback
1060
1061 def show_dialog(self, message, title, type="info", answer_cb=None, answer_data=None):
1062 if type in ('info', 'warning', 'error'):
1063 self.add_note(title, message, type)
1064 elif type == "yes/no":
1065 wid = dialog.ConfirmDialog(title=title, message=message,
1066 yes_cb=self._dialog_cb(answer_cb,
1067 True,
1068 answer_data),
1069 no_cb=self._dialog_cb(answer_cb,
1070 False,
1071 answer_data)
1072 )
1073 self.add_notif_widget(wid)
1074 else:
1075 log.warning(_("unknown dialog type: {dialog_type}").format(dialog_type=type))
1076
1077 def share(self, media_type, data):
1078 share_wid = ShareWidget(media_type=media_type, data=data)
1079 try:
1080 self.show_extra_ui(share_wid)
1081 except Exception as e:
1082 log.error(e)
1083 self.close_ui()
1084
1085 def download_url(
1086 self, url, callback, errback=None, options=None, dest=C.FILE_DEST_DOWNLOAD,
1087 profile=C.PROF_KEY_NONE):
1088 """Download an URL (decrypt it if necessary)
1089
1090 @param url(str, parse.SplitResult): url to download
1091 @param callback(callable): method to call when download is complete
1092 @param errback(callable, None): method to call in case of error
1093 if None, default errback will be called
1094 @param dest(str): where the file should be downloaded:
1095 - C.FILE_DEST_DOWNLOAD: in platform download directory
1096 - C.FILE_DEST_CACHE: in SàT cache
1097 @param options(dict, None): options to pass to bridge.file_download_complete
1098 """
1099 if not isinstance(url, urlparse.ParseResult):
1100 url = urlparse.urlparse(url)
1101 if errback is None:
1102 errback = partial(
1103 self.errback,
1104 title=_("Download error"),
1105 message=_("Error while downloading {url}: {{msg}}").format(url=url.geturl()))
1106 name = Path(url.path).name.strip() or C.FILE_DEFAULT_NAME
1107 log.info(f"downloading/decrypting file {name!r}")
1108 if dest == C.FILE_DEST_DOWNLOAD:
1109 dest_path = files_utils.get_unique_name(Path(self.downloads_dir)/name)
1110 elif dest == C.FILE_DEST_CACHE:
1111 dest_path = ''
1112 else:
1113 raise exceptions.InternalError(f"Invalid dest_path: {dest_path!r}")
1114 self.bridge.file_download_complete(
1115 data_format.serialise({"uri": url.geturl()}),
1116 str(dest_path),
1117 '' if not options else data_format.serialise(options),
1118 profile,
1119 callback=callback,
1120 errback=errback
1121 )
1122
1123 def notify(self, type_, entity=None, message=None, subject=None, callback=None,
1124 cb_args=None, widget=None, profile=C.PROF_KEY_NONE):
1125 super().notify(
1126 type_=type_, entity=entity, message=message, subject=subject,
1127 callback=callback, cb_args=cb_args, widget=widget, profile=profile)
1128 self.desktop_notif(message, title=subject)
1129
1130 def desktop_notif(self, message, title='', duration=5):
1131 global notification
1132 if notification is not None:
1133 try:
1134 log.debug(
1135 f"sending desktop notification (duration: {duration}):\n"
1136 f"{title}\n"
1137 f"{message}"
1138 )
1139 notification.notify(title=title,
1140 message=message,
1141 app_name=C.APP_NAME,
1142 app_icon=self.app.icon,
1143 timeout=duration)
1144 except Exception as e:
1145 log.warning(_("Can't use notifications, disabling: {msg}").format(
1146 msg = e))
1147 notification = None
1148
1149 def get_parent_wh_wrapper(self, wid):
1150 """Retrieve parent WHWrapper instance managing a widget
1151
1152 @param wid(Widget): widget to check
1153 @return (WHWrapper, None): found instance if any, else None
1154 """
1155 wh = self.get_ancestor_widget(wid, widgets_handler.WHWrapper)
1156 if wh is None:
1157 # we may have a screen
1158 try:
1159 sm = wid.screen_manager
1160 except (exceptions.InternalError, exceptions.NotFound):
1161 return None
1162 else:
1163 wh = self.get_ancestor_widget(sm, widgets_handler.WHWrapper)
1164 return wh
1165
1166 def get_ancestor_widget(self, wid, cls):
1167 """Retrieve an ancestor of given class
1168
1169 @param wid(Widget): current widget
1170 @param cls(type): class of the ancestor to retrieve
1171 @return (Widget, None): found instance or None
1172 """
1173 parent = wid.parent
1174 while parent and not isinstance(parent, cls):
1175 parent = parent.parent
1176 return parent