Mercurial > libervia-desktop-kivy
comparison libervia/desktop_kivy/core/menu.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/menu.py@203755bbe0fe |
children |
comparison
equal
deleted
inserted
replaced
492:5114bbb5daa3 | 493:b3cedbee561d |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 #Libervia Desktop-Kivy | |
5 # Copyright (C) 2016-2021 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 | |
21 from libervia.backend.core.i18n import _ | |
22 from libervia.backend.core import log as logging | |
23 from libervia.desktop_kivy.core.constants import Const as C | |
24 from libervia.desktop_kivy.core.common import JidToggle | |
25 from kivy.uix.boxlayout import BoxLayout | |
26 from kivy.uix.label import Label | |
27 from kivy.uix.button import Button | |
28 from kivy.uix.popup import Popup | |
29 from .behaviors import FilterBehavior | |
30 from kivy import properties | |
31 from kivy.core.window import Window | |
32 from kivy.animation import Animation | |
33 from kivy.metrics import dp | |
34 from libervia.desktop_kivy import G | |
35 from functools import partial | |
36 import webbrowser | |
37 | |
38 log = logging.getLogger(__name__) | |
39 | |
40 ABOUT_TITLE = _("About {}").format(C.APP_NAME) | |
41 ABOUT_CONTENT = _("""[b]{app_name} ({app_name_alt})[/b] | |
42 | |
43 [u]{app_name} version[/u]: | |
44 {version} | |
45 | |
46 [u]backend version[/u]: | |
47 {backend_version} | |
48 | |
49 {app_name} is a libre communication tool based on libre standard XMPP. | |
50 | |
51 {app_name} is part of the "Libervia" project ({app_component} frontend) | |
52 more informations at [color=5500ff][ref=website]salut-a-toi.org[/ref][/color] | |
53 """) | |
54 | |
55 | |
56 class AboutContent(Label): | |
57 | |
58 def on_ref_press(self, value): | |
59 if value == "website": | |
60 webbrowser.open("https://salut-a-toi.org") | |
61 | |
62 | |
63 class AboutPopup(Popup): | |
64 | |
65 def on_touch_down(self, touch): | |
66 if self.collide_point(*touch.pos): | |
67 self.dismiss() | |
68 return super(AboutPopup, self).on_touch_down(touch) | |
69 | |
70 | |
71 class TransferItem(BoxLayout): | |
72 plug_info = properties.DictProperty() | |
73 | |
74 def on_touch_up(self, touch): | |
75 if not self.collide_point(*touch.pos): | |
76 return super(TransferItem, self).on_touch_up(touch) | |
77 else: | |
78 transfer_menu = self.parent | |
79 while not isinstance(transfer_menu, TransferMenu): | |
80 transfer_menu = transfer_menu.parent | |
81 transfer_menu.do_callback(self.plug_info) | |
82 return True | |
83 | |
84 | |
85 class SideMenu(BoxLayout): | |
86 size_hint_close = (0, 1) | |
87 size_hint_open = (0.4, 1) | |
88 size_close = (100, 100) | |
89 size_open = (0, 0) | |
90 bg_color = properties.ListProperty([0, 0, 0, 1]) | |
91 # callback will be called with arguments relevant to menu | |
92 callback = properties.ObjectProperty() | |
93 # call do_callback even when menu is cancelled | |
94 callback_on_close = properties.BooleanProperty(False) | |
95 # cancel callback need to remove the widget for UI | |
96 # will be called with the widget to remove as argument | |
97 cancel_cb = properties.ObjectProperty() | |
98 | |
99 def __init__(self, **kwargs): | |
100 super(SideMenu, self).__init__(**kwargs) | |
101 if self.cancel_cb is None: | |
102 self.cancel_cb = self.on_menu_cancelled | |
103 | |
104 def _set_anim_kw(self, kw, size_hint, size): | |
105 """Set animation keywords | |
106 | |
107 for each value of size_hint it is used if not None, | |
108 else size is used. | |
109 If one value of size is bigger than the respective one of Window | |
110 the one of Window is used | |
111 """ | |
112 size_hint_x, size_hint_y = size_hint | |
113 width, height = size | |
114 if size_hint_x is not None: | |
115 kw['size_hint_x'] = size_hint_x | |
116 elif width is not None: | |
117 kw['width'] = min(width, Window.width) | |
118 | |
119 if size_hint_y is not None: | |
120 kw['size_hint_y'] = size_hint_y | |
121 elif height is not None: | |
122 kw['height'] = min(height, Window.height) | |
123 | |
124 def show(self, caller_wid=None): | |
125 Window.bind(on_keyboard=self.key_input) | |
126 G.host.app.root.add_widget(self) | |
127 kw = {'d': 0.3, 't': 'out_back'} | |
128 self._set_anim_kw(kw, self.size_hint_open, self.size_open) | |
129 Animation(**kw).start(self) | |
130 | |
131 def _remove_from_parent(self, anim, menu): | |
132 # self.parent can already be None if the widget has been removed by a callback | |
133 # before the animation started. | |
134 if self.parent is not None: | |
135 self.parent.remove_widget(self) | |
136 | |
137 def hide(self): | |
138 Window.unbind(on_keyboard=self.key_input) | |
139 kw = {'d': 0.2} | |
140 self._set_anim_kw(kw, self.size_hint_close, self.size_close) | |
141 anim = Animation(**kw) | |
142 anim.bind(on_complete=self._remove_from_parent) | |
143 anim.start(self) | |
144 if self.callback_on_close: | |
145 self.do_callback() | |
146 | |
147 def on_touch_down(self, touch): | |
148 # we remove the menu if we click outside | |
149 # else we want to handle the event, but not | |
150 # transmit it to parents | |
151 if not self.collide_point(*touch.pos): | |
152 self.hide() | |
153 else: | |
154 return super(SideMenu, self).on_touch_down(touch) | |
155 return True | |
156 | |
157 def key_input(self, window, key, scancode, codepoint, modifier): | |
158 if key == 27: | |
159 self.hide() | |
160 return True | |
161 | |
162 def on_menu_cancelled(self, wid, cleaning_cb=None): | |
163 self._close_ui(wid) | |
164 if cleaning_cb is not None: | |
165 cleaning_cb() | |
166 | |
167 def _close_ui(self, wid): | |
168 G.host.close_ui() | |
169 | |
170 def do_callback(self, *args, **kwargs): | |
171 log.warning("callback not implemented") | |
172 | |
173 | |
174 class ExtraMenuItem(Button): | |
175 pass | |
176 | |
177 | |
178 class ExtraSideMenu(SideMenu): | |
179 """Menu with general app actions like showing the about widget""" | |
180 | |
181 def __init__(self, **kwargs): | |
182 super().__init__(**kwargs) | |
183 G.local_platform.on_extra_menu_init(self) | |
184 | |
185 def add_item(self, label, callback): | |
186 self.add_widget( | |
187 ExtraMenuItem( | |
188 text=label, | |
189 on_press=partial(self.on_item_press, callback=callback), | |
190 ), | |
191 # we want the new item above "About" and last empty Widget | |
192 index=2) | |
193 | |
194 def on_item_press(self, *args, callback): | |
195 self.hide() | |
196 callback() | |
197 | |
198 def on_about(self): | |
199 self.hide() | |
200 about = AboutPopup() | |
201 about.title = ABOUT_TITLE | |
202 about.content = AboutContent( | |
203 text=ABOUT_CONTENT.format( | |
204 app_name = C.APP_NAME, | |
205 app_name_alt = C.APP_NAME_ALT, | |
206 app_component = C.APP_COMPONENT, | |
207 backend_version = G.host.backend_version, | |
208 version=G.host.version | |
209 ), | |
210 markup=True) | |
211 about.open() | |
212 | |
213 | |
214 class TransferMenu(SideMenu): | |
215 """transfer menu which handle display and callbacks""" | |
216 # callback will be called with path to file to transfer | |
217 # profiles if set will be sent to transfer widget, may be used to get specific files | |
218 profiles = properties.ObjectProperty() | |
219 transfer_txt = properties.StringProperty() | |
220 transfer_info = properties.ObjectProperty() | |
221 upload_btn = properties.ObjectProperty() | |
222 encrypted = properties.BooleanProperty(False) | |
223 items_layout = properties.ObjectProperty() | |
224 size_hint_close = (1, 0) | |
225 size_hint_open = (1, 0.5) | |
226 | |
227 def __init__(self, **kwargs): | |
228 super(TransferMenu, self).__init__(**kwargs) | |
229 if self.profiles is None: | |
230 self.profiles = iter(G.host.profiles) | |
231 for plug_info in G.host.get_plugged_widgets(type_=C.PLUG_TYPE_TRANSFER): | |
232 item = TransferItem( | |
233 plug_info = plug_info | |
234 ) | |
235 self.items_layout.add_widget(item) | |
236 | |
237 def on_kv_post(self, __): | |
238 self.update_transfer_info() | |
239 | |
240 def get_transfer_info(self): | |
241 if self.upload_btn.state == "down": | |
242 # upload | |
243 if self.encrypted: | |
244 return _( | |
245 "The file will be [color=00aa00][b]encrypted[/b][/color] and sent to " | |
246 "your server\nServer admin(s) can delete the file, but they won't be " | |
247 "able to see its content" | |
248 ) | |
249 else: | |
250 return _( | |
251 "Beware! The file will be sent to your server and stay " | |
252 "[color=ff0000][b]unencrypted[/b][/color] there\nServer admin(s) " | |
253 "can see the file, and they choose how, when and if it will be " | |
254 "deleted" | |
255 ) | |
256 else: | |
257 # P2P | |
258 if self.encrypted: | |
259 return _( | |
260 "The file will be sent [color=ff0000][b]unencrypted[/b][/color] " | |
261 "directly to your contact (it may be transiting by the " | |
262 "server if direct connection is not possible).\n[color=ff0000]" | |
263 "Please note that end-to-end encryption is not yet implemented for " | |
264 "P2P transfer." | |
265 ) | |
266 else: | |
267 return _( | |
268 "The file will be sent [color=ff0000][b]unencrypted[/b][/color] " | |
269 "directly to your contact (it [i]may be[/i] transiting by the " | |
270 "server if direct connection is not possible)." | |
271 ) | |
272 | |
273 def update_transfer_info(self): | |
274 self.transfer_info.text = self.get_transfer_info() | |
275 | |
276 def _on_transfer_cb(self, file_path, cleaning_cb=None, external=False, wid_cont=None): | |
277 if not external: | |
278 wid = wid_cont[0] | |
279 self._close_ui(wid) | |
280 self.callback( | |
281 file_path, | |
282 transfer_type = (C.TRANSFER_UPLOAD | |
283 if self.ids['upload_btn'].state == "down" else C.TRANSFER_SEND), | |
284 cleaning_cb=cleaning_cb, | |
285 ) | |
286 | |
287 def _check_plugin_permissions_cb(self, plug_info): | |
288 external = plug_info.get('external', False) | |
289 wid_cont = [] | |
290 wid_cont.append(plug_info['factory']( | |
291 plug_info, | |
292 partial(self._on_transfer_cb, external=external, wid_cont=wid_cont), | |
293 self.cancel_cb, | |
294 self.profiles)) | |
295 if not external: | |
296 G.host.show_extra_ui(wid_cont[0]) | |
297 | |
298 def do_callback(self, plug_info): | |
299 self.parent.remove_widget(self) | |
300 if self.callback is None: | |
301 log.warning("TransferMenu callback is not set") | |
302 else: | |
303 G.local_platform.check_plugin_permissions( | |
304 plug_info, | |
305 callback=partial(self._check_plugin_permissions_cb, plug_info), | |
306 errback=lambda: G.host.add_note( | |
307 _("permission refused"), | |
308 _("this transfer menu can't be used if you refuse the requested " | |
309 "permission"), | |
310 C.XMLUI_DATA_LVL_WARNING) | |
311 ) | |
312 | |
313 | |
314 class EntitiesSelectorMenu(SideMenu, FilterBehavior): | |
315 """allow to select entities from roster""" | |
316 profiles = properties.ObjectProperty() | |
317 layout = properties.ObjectProperty() | |
318 instructions = properties.StringProperty(_("Please select entities")) | |
319 filter_input = properties.ObjectProperty() | |
320 size_hint_close = (None, 1) | |
321 size_hint_open = (None, 1) | |
322 size_open = (dp(250), 100) | |
323 size_close = (0, 100) | |
324 | |
325 def __init__(self, **kwargs): | |
326 super(EntitiesSelectorMenu, self).__init__(**kwargs) | |
327 self.filter_input.bind(text=self.do_filter_input) | |
328 if self.profiles is None: | |
329 self.profiles = iter(G.host.profiles) | |
330 for profile in self.profiles: | |
331 for jid_, jid_data in G.host.contact_lists[profile].all_iter: | |
332 jid_wid = JidToggle( | |
333 jid=jid_, | |
334 profile=profile) | |
335 self.layout.add_widget(jid_wid) | |
336 | |
337 def do_callback(self): | |
338 if self.callback is not None: | |
339 jids = [c.jid for c in self.layout.children if c.state == 'down'] | |
340 self.callback(jids) | |
341 | |
342 def do_filter_input(self, filter_input, text): | |
343 self.layout.spacing = 0 if text else dp(5) | |
344 self.do_filter(self.layout, | |
345 text, | |
346 lambda c: c.jid, | |
347 width_cb=lambda c: c.width, | |
348 height_cb=lambda c: dp(70)) |