Mercurial > libervia-desktop-kivy
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 |