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))