Mercurial > libervia-desktop-kivy
annotate src/cagou/core/cagou_main.py @ 29:8b5827c43155
notes first draft:
Implementation of XMLUI notes. There is a new header on top of root widget which display notifications, and notes are shown for a couple of seconds.
A blue Cagou head appear when there are notes, and user can display 10 last when clicking on it.
This header will probably not be present on platforms such as Android, because there is already a system-wide notifications handler which can be used instead (saving visual space).
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 21 Aug 2016 15:15:25 +0200 |
parents | 9f9532eb835f |
children | 4f9e701d76b4 |
rev | line source |
---|---|
6
85649eca9f9b
core (logs): integrate Kivy logs with SàT:
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
1 #!/usr//bin/env python2 |
0 | 2 # -*- coding: utf-8 -*- |
3 | |
4 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client | |
5 # Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 | |
14 | 21 from sat.core.i18n import _ |
6
85649eca9f9b
core (logs): integrate Kivy logs with SàT:
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
22 import logging_setter |
85649eca9f9b
core (logs): integrate Kivy logs with SàT:
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
23 logging_setter.set_logging() |
85649eca9f9b
core (logs): integrate Kivy logs with SàT:
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
24 from constants import Const as C |
0 | 25 from sat.core import log as logging |
26 log = logging.getLogger(__name__) | |
27 from sat_frontends.quick_frontend.quick_app import QuickApp | |
28 from sat_frontends.bridge.DBus import DBusBridgeFrontend | |
29 import kivy | |
30 kivy.require('1.9.1') | |
31 import kivy.support | |
32 kivy.support.install_gobject_iteration() | |
33 from kivy.app import App | |
14 | 34 from kivy.lang import Builder |
29 | 35 from kivy import properties |
0 | 36 import xmlui |
37 from profile_manager import ProfileManager | |
13 | 38 from widgets_handler import WidgetsHandler |
29 | 39 from kivy.clock import Clock |
40 from kivy.uix.label import Label | |
9 | 41 from kivy.uix.boxlayout import BoxLayout |
29 | 42 from kivy.uix.screenmanager import ScreenManager, Screen |
43 from kivy.uix.dropdown import DropDown | |
11
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
44 from cagou_widget import CagouWidget |
29 | 45 from .common import IconButton |
14 | 46 from importlib import import_module |
9 | 47 import os.path |
14 | 48 import glob |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
49 import cagou.plugins |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
50 import cagou.kv |
9 | 51 |
52 | |
29 | 53 class NotifIcon(IconButton): |
54 | |
55 def __init__(self, callback, callback_args): | |
56 self._callback = callback | |
57 self._callback_args = callback_args | |
58 super(NotifIcon, self).__init__() | |
59 | |
60 def on_release(self): | |
61 self.parent.remove_widget(self) | |
62 self._callback(*self._callback_args) | |
63 | |
64 | |
65 class Note(Label): | |
66 title = properties.StringProperty() | |
67 message = properties.StringProperty() | |
68 level = properties.OptionProperty(C.XMLUI_DATA_LVL_DEFAULT, options=list(C.XMLUI_DATA_LVLS)) | |
69 | |
70 | |
71 class NoteDrop(Note): | |
72 pass | |
73 | |
74 | |
75 class NotesDrop(DropDown): | |
76 clear_btn = properties.ObjectProperty() | |
77 | |
78 def __init__(self, notes): | |
79 super(NotesDrop, self).__init__() | |
80 self.notes = notes | |
81 | |
82 def open(self, widget): | |
83 self.clear_widgets() | |
84 for n in self.notes: | |
85 self.add_widget(NoteDrop(title=n.title, message=n.message, level=n.level)) | |
86 self.add_widget(self.clear_btn) | |
87 super(NotesDrop, self).open(widget) | |
88 | |
89 | |
90 class RootHeadWidget(BoxLayout): | |
91 """Notifications widget""" | |
92 manager = properties.ObjectProperty() | |
93 notes = properties.ListProperty() | |
94 | |
95 def __init__(self): | |
96 super(RootHeadWidget, self).__init__() | |
97 self.notes_last = None | |
98 self.notes_event = None | |
99 self.notes_drop = NotesDrop(self.notes) # auto_with=False, width=100) | |
100 | |
101 def addNotif(self, callback, *args): | |
102 icon = NotifIcon(callback, args) | |
103 self.add_widget(icon) | |
104 | |
105 def addNote(self, title, message, level): | |
106 note = Note(title=title, message=message, level=level) | |
107 self.notes.append(note) | |
108 if len(self.notes) > 10: | |
109 del self.notes[:-10] | |
110 if self.notes_event is None: | |
111 self.notes_event = Clock.schedule_interval(self._displayNextNote, 5) | |
112 self._displayNextNote() | |
113 | |
114 def _displayNextNote(self, dummy=None): | |
115 screen = Screen() | |
116 try: | |
117 idx = self.notes.index(self.notes_last) + 1 | |
118 except ValueError: | |
119 idx = 0 | |
120 try: | |
121 note = self.notes_last = self.notes[idx] | |
122 except IndexError: | |
123 self.notes_event.cancel() | |
124 self.notes_event = None | |
125 else: | |
126 screen.add_widget(note) | |
127 self.manager.switch_to(screen) | |
128 | |
129 | |
9 | 130 class CagouRootWidget(BoxLayout): |
131 | |
29 | 132 def __init__(self, main_widget): |
9 | 133 super(CagouRootWidget, self).__init__(orientation=("vertical")) |
29 | 134 # header |
135 self._head_widget = RootHeadWidget() | |
136 self.add_widget(self._head_widget) | |
9 | 137 |
29 | 138 # body |
139 self._manager = ScreenManager() | |
140 main_screen = Screen(name='main') | |
141 main_screen.add_widget(main_widget) | |
142 self._manager.add_widget(main_screen) | |
143 self.add_widget(self._manager) | |
144 self.change_widget(main_widget) | |
145 | |
146 def change_widget(self, main_widget, screen="main"): | |
147 """change main widget""" | |
148 main_screen = self._manager.get_screen(screen) | |
149 main_screen.clear_widgets() | |
150 main_screen.add_widget(main_widget) | |
151 | |
152 def newAction(self, handler, action_data, id_, security_limit, profile): | |
153 """Add a notification for an action""" | |
154 self._head_widget.addNotif(handler, action_data, id_, security_limit, profile) | |
155 | |
156 def addNote(self, title, message, level): | |
157 self._head_widget.addNote(title, message, level) | |
0 | 158 |
159 | |
160 class CagouApp(App): | |
161 """Kivy App for Cagou""" | |
162 | |
163 def build(self): | |
29 | 164 return CagouRootWidget(ProfileManager()) |
0 | 165 |
28
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
166 def expand(self, path): |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
167 """expand path and replace known values |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
168 |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
169 useful in kv. Values which can be used: |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
170 - {media}: media dir |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
171 """ |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
172 return os.path.expanduser(path).format(media=self.host.media_dir) |
9f9532eb835f
core: added expand method to expand filename with magic values, specially useful in kv
Goffi <goffi@goffi.org>
parents:
25
diff
changeset
|
173 |
0 | 174 |
175 class Cagou(QuickApp): | |
176 MB_HANDLE = False | |
177 | |
178 def __init__(self): | |
179 super(Cagou, self).__init__(create_bridge=DBusBridgeFrontend, xmlui=xmlui) | |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
180 self._import_kv() |
0 | 181 self.app = CagouApp() |
29 | 182 self.app.host = self |
25
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
183 self.media_dir = self.app.media_dir = self.bridge.getConfig("", "media_dir") |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
184 self.app.default_avatar = os.path.join(self.media_dir, "misc/default_avatar.png") |
14 | 185 self._plg_wids = [] # widget plugins |
186 self._import_plugins() | |
0 | 187 |
188 def run(self): | |
189 self.app.run() | |
190 | |
16
ba14b596b90e
host can now be get as a global value:
Goffi <goffi@goffi.org>
parents:
15
diff
changeset
|
191 def _defaultFactory(self, plugin_info, target, profiles): |
14 | 192 """factory used to create widget instance when PLUGIN_INFO["factory"] is not set""" |
193 main_cls = plugin_info['main'] | |
19
c58b522607f4
main: fixed profiles value in _defaultFactory + getPluggedWidgets is now a generator
Goffi <goffi@goffi.org>
parents:
16
diff
changeset
|
194 return self.widgets.getOrCreateWidget(main_cls, target, on_new_widget=None, profiles=iter(self.profiles)) |
14 | 195 |
29 | 196 ## plugins & kv import ## |
197 | |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
198 def _import_kv(self): |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
199 """import all kv files in cagou.kv""" |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
200 path = os.path.dirname(cagou.kv.__file__) |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
201 for kv_path in glob.glob(os.path.join(path, "*.kv")): |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
202 Builder.load_file(kv_path) |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
203 log.debug(u"kv file {} loaded".format(kv_path)) |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
204 |
14 | 205 def _import_plugins(self): |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
206 """import all plugins""" |
14 | 207 self.default_wid = None |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
208 plugins_path = os.path.dirname(cagou.plugins.__file__) |
14 | 209 plug_lst = [os.path.splitext(p)[0] for p in map(os.path.basename, glob.glob(os.path.join(plugins_path, "plugin*.py")))] |
210 imported_names = set() # use to avoid loading 2 times plugin with same import name | |
211 for plug in plug_lst: | |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
212 plugin_path = 'cagou.plugins.' + plug |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
213 mod = import_module(plugin_path) |
14 | 214 try: |
215 plugin_info = mod.PLUGIN_INFO | |
216 except AttributeError: | |
217 plugin_info = {} | |
218 | |
219 # import name is used to differentiate plugins | |
220 if 'import_name' not in plugin_info: | |
221 plugin_info['import_name'] = plug | |
222 if 'import_name' in imported_names: | |
223 log.warning(_(u"there is already a plugin named {}, ignoring new one").format(plugin_info['import_name'])) | |
224 continue | |
225 if plugin_info['import_name'] == C.WID_SELECTOR: | |
226 # if WidgetSelector exists, it will be our default widget | |
227 self.default_wid = plugin_info | |
228 | |
229 # we want everything optional, so we use plugin file name | |
230 # if actual name is not found | |
231 if 'name' not in plugin_info: | |
232 plugin_info['name'] = plug[plug.rfind('_')+1:] | |
233 | |
234 # we need to load the kv file | |
235 if 'kv_file' not in plugin_info: | |
236 plugin_info['kv_file'] = u'{}.kv'.format(plug) | |
15
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
237 kv_path = os.path.join(plugins_path, plugin_info['kv_file']) |
56838ad5c84b
files reorganisation, cagou is now launched with python2 cagou.py in src/
Goffi <goffi@goffi.org>
parents:
14
diff
changeset
|
238 Builder.load_file(kv_path) |
14 | 239 |
240 # what is the main class ? | |
241 main_cls = getattr(mod, plugin_info['main']) | |
242 plugin_info['main'] = main_cls | |
243 | |
244 # factory is used to create the instance | |
245 # if not found, we use a defaut one with getOrCreateWidget | |
246 if 'factory' not in plugin_info: | |
247 plugin_info['factory'] = self._defaultFactory | |
248 | |
25
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
249 # icons |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
250 for size in ('small', 'medium'): |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
251 key = u'icon_{}'.format(size) |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
252 try: |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
253 path = plugin_info[key] |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
254 except KeyError: |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
255 path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir) |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
256 else: |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
257 path = path.format(media=self.media_dir) |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
258 if not os.path.isfile(path): |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
259 path = C.DEFAULT_WIDGET_ICON.format(media=self.media_dir) |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
260 plugin_info[key] = path |
d09bd16dbbe2
code (cagou widget), selector: icons handling + use of new muchoslava icon set
Goffi <goffi@goffi.org>
parents:
19
diff
changeset
|
261 |
14 | 262 self._plg_wids.append(plugin_info) |
263 if not self._plg_wids: | |
264 log.error(_(u"no widget plugin found")) | |
265 return | |
266 | |
267 # we want widgets sorted by names | |
268 self._plg_wids.sort(key=lambda p: p['name'].lower()) | |
269 | |
270 if self.default_wid is None: | |
271 # we have no selector widget, we use the first widget as default | |
272 self.default_wid = self._plg_wids[0] | |
273 | |
274 def getPluggedWidgets(self, except_cls=None): | |
275 """get available widgets plugin infos | |
276 | |
277 @param except_cls(None, class): if not None, | |
278 widgets from this class will be excluded | |
279 @return (list[dict]): available widgets plugin infos | |
280 """ | |
19
c58b522607f4
main: fixed profiles value in _defaultFactory + getPluggedWidgets is now a generator
Goffi <goffi@goffi.org>
parents:
16
diff
changeset
|
281 for plugin_data in self._plg_wids: |
c58b522607f4
main: fixed profiles value in _defaultFactory + getPluggedWidgets is now a generator
Goffi <goffi@goffi.org>
parents:
16
diff
changeset
|
282 if plugin_data['main'] == except_cls: |
c58b522607f4
main: fixed profiles value in _defaultFactory + getPluggedWidgets is now a generator
Goffi <goffi@goffi.org>
parents:
16
diff
changeset
|
283 continue |
c58b522607f4
main: fixed profiles value in _defaultFactory + getPluggedWidgets is now a generator
Goffi <goffi@goffi.org>
parents:
16
diff
changeset
|
284 yield plugin_data |
14 | 285 |
29 | 286 ## widgets handling |
9 | 287 |
11
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
288 def switchWidget(self, old, new): |
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
289 """Replace old widget by new one |
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
290 |
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
291 old(CagouWidget): CagouWidget instance or a child |
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
292 new(CagouWidget): new widget instance |
49d30fc15884
core: added switchWidget method, to change a CagouWidget for an other one
Goffi <goffi@goffi.org>
parents:
9
diff
changeset
|
293 """ |
14 | 294 to_change = None |
295 if isinstance(old, CagouWidget): | |
296 to_change = old | |
297 else: | |
298 for w in old.walk_reverse(): | |
299 if isinstance(w, CagouWidget): | |
300 to_change = w | |
301 break | |
302 | |
303 if to_change is None: | |
304 log.error(u"no CagouWidget found when trying to switch widget") | |
305 else: | |
306 parent = to_change.parent | |
307 idx = parent.children.index(to_change) | |
308 parent.remove_widget(to_change) | |
309 parent.add_widget(new, index=idx) | |
29 | 310 |
311 ## misc ## | |
312 | |
313 def plugging_profiles(self): | |
314 self.app.root.change_widget(WidgetsHandler()) | |
315 | |
316 def setPresenceStatus(self, show='', status=None, profile=C.PROF_KEY_NONE): | |
317 log.info(u"Profile presence status set to {show}/{status}".format(show=show, status=status)) | |
318 | |
319 def addNote(self, title, message, level): | |
320 """add a note (message which disappear) to root widget's header""" | |
321 self.app.root.addNote(title, message, level) | |
322 | |
323 ## signals handling ## | |
324 | |
325 def actionNewHandler(self, action_data, id_, security_limit, profile): | |
326 handler = super(Cagou, self).actionNewHandler | |
327 # FIXME: temporarily deactivated | |
328 # if 'xmlui' in action_data: | |
329 # self.app.root.newAction(handler, action_data, id_, security_limit, profile) | |
330 # else: | |
331 # handler(action_data, id_, security_limit, profile) | |
332 handler(action_data, id_, security_limit, profile) |