1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# KDE, App-Indicator or Qt Systray
5# Copyright (C) 2011-2018 Filipe Coelho <falktx@falktx.com>
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# 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 General Public License for more details.
16#
17# For a full copy of the GNU General Public License see the COPYING file
18
19# Imports (Global)
20import os, sys
21
22if True:
23    from PyQt5.QtCore import QTimer
24    from PyQt5.QtGui import QIcon
25    from PyQt5.QtWidgets import QAction, QMainWindow, QMenu, QSystemTrayIcon
26else:
27    from PyQt4.QtCore import QTimer
28    from PyQt4.QtGui import QIcon
29    from PyQt4.QtGui import QAction, QMainWindow, QMenu, QSystemTrayIcon
30
31try:
32    if False and os.getenv("DESKTOP_SESSION") in ("ubuntu", "ubuntu-2d") and not os.path.exists("/var/cadence/no_app_indicators"):
33        from gi import require_version
34        require_version('Gtk', '3.0')
35        from gi.repository import Gtk
36        require_version('AppIndicator3', '0.1')
37        from gi.repository import AppIndicator3 as AppIndicator
38        TrayEngine = "AppIndicator"
39
40    #elif os.getenv("KDE_SESSION_VERSION") >= 5:
41        #TrayEngine = "Qt"
42
43    #elif os.getenv("KDE_FULL_SESSION") or os.getenv("DESKTOP_SESSION") == "kde-plasma":
44        #from PyKDE5.kdeui import KAction, KIcon, KMenu, KStatusNotifierItem
45        #TrayEngine = "KDE"
46
47    else:
48        TrayEngine = "Qt"
49except:
50    TrayEngine = "Qt"
51
52print("Using Tray Engine '%s'" % TrayEngine)
53
54iActNameId = 0
55iActWidget = 1
56iActParentMenuId = 2
57iActFunc   = 3
58
59iSepNameId = 0
60iSepWidget = 1
61iSepParentMenuId = 2
62
63iMenuNameId = 0
64iMenuWidget = 1
65iMenuParentMenuId = 2
66
67# Get Icon from user theme, using our own as backup (Oxygen)
68def getIcon(icon, size=16):
69    return QIcon.fromTheme(icon, QIcon(":/%ix%i/%s.png" % (size, size, icon)))
70
71# Global Systray class
72class GlobalSysTray(object):
73    def __init__(self, parent, name, icon):
74        object.__init__(self)
75
76        self._app    = None
77        self._parent = parent
78        self._gtk_running = False
79        self._quit_added  = False
80
81        self.act_indexes  = []
82        self.sep_indexes  = []
83        self.menu_indexes = []
84
85        if TrayEngine == "KDE":
86            self.menu = KMenu(parent)
87            self.menu.setTitle(name)
88            self.tray = KStatusNotifierItem()
89            self.tray.setAssociatedWidget(parent)
90            self.tray.setCategory(KStatusNotifierItem.ApplicationStatus)
91            self.tray.setContextMenu(self.menu)
92            self.tray.setIconByPixmap(getIcon(icon))
93            self.tray.setTitle(name)
94            self.tray.setToolTipTitle(" ")
95            self.tray.setToolTipIconByPixmap(getIcon(icon))
96            # Double-click is managed by KDE
97
98        elif TrayEngine == "AppIndicator":
99            self.menu = Gtk.Menu()
100            self.tray = AppIndicator.Indicator.new(name, icon, AppIndicator.IndicatorCategory.APPLICATION_STATUS)
101            self.tray.set_menu(self.menu)
102            # Double-click is not possible with App-Indicators
103
104        elif TrayEngine == "Qt":
105            self.menu = QMenu(parent)
106            self.tray = QSystemTrayIcon(getIcon(icon))
107            self.tray.setContextMenu(self.menu)
108            self.tray.setParent(parent)
109            self.tray.activated.connect(self.qt_systray_clicked)
110
111    # -------------------------------------------------------------------------------------------
112
113    def addAction(self, act_name_id, act_name_string, is_check=False):
114        if TrayEngine == "KDE":
115            act_widget = KAction(act_name_string, self.menu)
116            act_widget.setCheckable(is_check)
117            self.menu.addAction(act_widget)
118
119        elif TrayEngine == "AppIndicator":
120            if is_check:
121                act_widget = Gtk.CheckMenuItem(act_name_string)
122            else:
123                act_widget = Gtk.ImageMenuItem(act_name_string)
124                act_widget.set_image(None)
125            act_widget.show()
126            self.menu.append(act_widget)
127
128        elif TrayEngine == "Qt":
129            act_widget = QAction(act_name_string, self.menu)
130            act_widget.setCheckable(is_check)
131            self.menu.addAction(act_widget)
132
133        else:
134            act_widget = None
135
136        act_obj = [None, None, None, None]
137        act_obj[iActNameId] = act_name_id
138        act_obj[iActWidget] = act_widget
139
140        self.act_indexes.append(act_obj)
141
142    def addSeparator(self, sep_name_id):
143        if TrayEngine == "KDE":
144            sep_widget = self.menu.addSeparator()
145
146        elif TrayEngine == "AppIndicator":
147            sep_widget = Gtk.SeparatorMenuItem()
148            sep_widget.show()
149            self.menu.append(sep_widget)
150
151        elif TrayEngine == "Qt":
152            sep_widget = self.menu.addSeparator()
153
154        else:
155            sep_widget = None
156
157        sep_obj = [None, None, None]
158        sep_obj[iSepNameId] = sep_name_id
159        sep_obj[iSepWidget] = sep_widget
160
161        self.sep_indexes.append(sep_obj)
162
163    def addMenu(self, menu_name_id, menu_name_string):
164        if TrayEngine == "KDE":
165            menu_widget = KMenu(menu_name_string, self.menu)
166            self.menu.addMenu(menu_widget)
167
168        elif TrayEngine == "AppIndicator":
169            menu_widget = Gtk.MenuItem(menu_name_string)
170            menu_parent = Gtk.Menu()
171            menu_widget.set_submenu(menu_parent)
172            menu_widget.show()
173            self.menu.append(menu_widget)
174
175        elif TrayEngine == "Qt":
176            menu_widget = QMenu(menu_name_string, self.menu)
177            self.menu.addMenu(menu_widget)
178
179        else:
180            menu_widget = None
181
182        menu_obj = [None, None, None]
183        menu_obj[iMenuNameId] = menu_name_id
184        menu_obj[iMenuWidget] = menu_widget
185
186        self.menu_indexes.append(menu_obj)
187
188    # -------------------------------------------------------------------------------------------
189
190    def addMenuAction(self, menu_name_id, act_name_id, act_name_string, is_check=False):
191        i = self.get_menu_index(menu_name_id)
192        if i < 0: return
193
194        menu_widget = self.menu_indexes[i][iMenuWidget]
195
196        if TrayEngine == "KDE":
197            act_widget = KAction(act_name_string, menu_widget)
198            act_widget.setCheckable(is_check)
199            menu_widget.addAction(act_widget)
200
201        elif TrayEngine == "AppIndicator":
202            menu_widget = menu_widget.get_submenu()
203            if is_check:
204                act_widget = Gtk.CheckMenuItem(act_name_string)
205            else:
206                act_widget = Gtk.ImageMenuItem(act_name_string)
207                act_widget.set_image(None)
208            act_widget.show()
209            menu_widget.append(act_widget)
210
211        elif TrayEngine == "Qt":
212            act_widget = QAction(act_name_string, menu_widget)
213            act_widget.setCheckable(is_check)
214            menu_widget.addAction(act_widget)
215
216        else:
217            act_widget = None
218
219        act_obj = [None, None, None, None]
220        act_obj[iActNameId] = act_name_id
221        act_obj[iActWidget] = act_widget
222        act_obj[iActParentMenuId] = menu_name_id
223
224        self.act_indexes.append(act_obj)
225
226    def addMenuSeparator(self, menu_name_id, sep_name_id):
227        i = self.get_menu_index(menu_name_id)
228        if i < 0: return
229
230        menu_widget = self.menu_indexes[i][iMenuWidget]
231
232        if TrayEngine == "KDE":
233            sep_widget = menu_widget.addSeparator()
234
235        elif TrayEngine == "AppIndicator":
236            menu_widget = menu_widget.get_submenu()
237            sep_widget = Gtk.SeparatorMenuItem()
238            sep_widget.show()
239            menu_widget.append(sep_widget)
240
241        elif TrayEngine == "Qt":
242            sep_widget = menu_widget.addSeparator()
243
244        else:
245            sep_widget = None
246
247        sep_obj = [None, None, None]
248        sep_obj[iSepNameId] = sep_name_id
249        sep_obj[iSepWidget] = sep_widget
250        sep_obj[iSepParentMenuId] = menu_name_id
251
252        self.sep_indexes.append(sep_obj)
253
254    #def addSubMenu(self, menu_name_id, new_menu_name_id, new_menu_name_string):
255        #menu_index = self.get_menu_index(menu_name_id)
256        #if menu_index < 0: return
257        #menu_widget = self.menu_indexes[menu_index][1]
258        ##if TrayEngine == "KDE":
259            ##new_menu_widget = KMenu(new_menu_name_string, self.menu)
260            ##menu_widget.addMenu(new_menu_widget)
261        ##elif TrayEngine == "AppIndicator":
262            ##new_menu_widget = Gtk.MenuItem(new_menu_name_string)
263            ##new_menu_widget.show()
264            ##menu_widget.get_submenu().append(new_menu_widget)
265            ##parent_menu_widget = Gtk.Menu()
266            ##new_menu_widget.set_submenu(parent_menu_widget)
267        ##else:
268        #if (1):
269            #new_menu_widget = QMenu(new_menu_name_string, self.menu)
270            #menu_widget.addMenu(new_menu_widget)
271        #self.menu_indexes.append([new_menu_name_id, new_menu_widget, menu_name_id])
272
273    # -------------------------------------------------------------------------------------------
274
275    def connect(self, act_name_id, act_func):
276        i = self.get_act_index(act_name_id)
277        if i < 0: return
278
279        act_widget = self.act_indexes[i][iActWidget]
280
281        if TrayEngine == "AppIndicator":
282            act_widget.connect("activate", self.gtk_call_func, act_name_id)
283
284        elif TrayEngine in ("KDE", "Qt"):
285            act_widget.triggered.connect(act_func)
286
287        self.act_indexes[i][iActFunc] = act_func
288
289    # -------------------------------------------------------------------------------------------
290
291    #def setActionChecked(self, act_name_id, yesno):
292        #index = self.get_act_index(act_name_id)
293        #if index < 0: return
294        #act_widget = self.act_indexes[index][1]
295        ##if TrayEngine == "KDE":
296            ##act_widget.setChecked(yesno)
297        ##elif TrayEngine == "AppIndicator":
298            ##if type(act_widget) != Gtk.CheckMenuItem:
299                ##return # Cannot continue
300            ##act_widget.set_active(yesno)
301        ##else:
302        #if (1):
303            #act_widget.setChecked(yesno)
304
305    def setActionEnabled(self, act_name_id, yesno):
306        i = self.get_act_index(act_name_id)
307        if i < 0: return
308
309        act_widget = self.act_indexes[i][iActWidget]
310
311        if TrayEngine == "KDE":
312            act_widget.setEnabled(yesno)
313
314        elif TrayEngine == "AppIndicator":
315            act_widget.set_sensitive(yesno)
316
317        elif TrayEngine == "Qt":
318            act_widget.setEnabled(yesno)
319
320    def setActionIcon(self, act_name_id, icon):
321        i = self.get_act_index(act_name_id)
322        if i < 0: return
323
324        act_widget = self.act_indexes[i][iActWidget]
325
326        if TrayEngine == "KDE":
327            act_widget.setIcon(KIcon(icon))
328
329        elif TrayEngine == "AppIndicator":
330            if not isinstance(act_widget, Gtk.ImageMenuItem):
331                # Cannot use icons here
332                return
333
334            act_widget.set_image(Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU))
335            #act_widget.set_always_show_image(True)
336
337        elif TrayEngine == "Qt":
338            act_widget.setIcon(getIcon(icon))
339
340    def setActionText(self, act_name_id, text):
341        i = self.get_act_index(act_name_id)
342        if i < 0: return
343
344        act_widget = self.act_indexes[i][iActWidget]
345
346        if TrayEngine == "KDE":
347            act_widget.setText(text)
348
349        elif TrayEngine == "AppIndicator":
350            if isinstance(act_widget, Gtk.ImageMenuItem):
351                # Fix icon reset
352                last_icon = act_widget.get_image()
353                act_widget.set_label(text)
354                act_widget.set_image(last_icon)
355            else:
356                act_widget.set_label(text)
357
358        elif TrayEngine == "Qt":
359            act_widget.setText(text)
360
361    def setIcon(self, icon):
362        if TrayEngine == "KDE":
363            self.tray.setIconByPixmap(getIcon(icon))
364            #self.tray.setToolTipIconByPixmap(getIcon(icon))
365
366        elif TrayEngine == "AppIndicator":
367            self.tray.set_icon(icon)
368
369        elif TrayEngine == "Qt":
370            self.tray.setIcon(getIcon(icon))
371
372    def setToolTip(self, text):
373        if TrayEngine == "KDE":
374            self.tray.setToolTipSubTitle(text)
375
376        elif TrayEngine == "AppIndicator":
377            # ToolTips are disabled in App-Indicators by design
378            pass
379
380        elif TrayEngine == "Qt":
381            self.tray.setToolTip(text)
382
383    # -------------------------------------------------------------------------------------------
384
385    #def removeAction(self, act_name_id):
386        #index = self.get_act_index(act_name_id)
387        #if index < 0: return
388        #act_widget = self.act_indexes[index][1]
389        #parent_menu_widget = self.get_parent_menu_widget(self.act_indexes[index][2])
390        ##if TrayEngine == "KDE":
391            ##parent_menu_widget.removeAction(act_widget)
392        ##elif TrayEngine == "AppIndicator":
393            ##act_widget.hide()
394            ##parent_menu_widget.remove(act_widget)
395        ##else:
396        #if (1):
397            #parent_menu_widget.removeAction(act_widget)
398        #self.act_indexes.pop(index)
399
400    #def removeSeparator(self, sep_name_id):
401        #index = self.get_sep_index(sep_name_id)
402        #if index < 0: return
403        #sep_widget = self.sep_indexes[index][1]
404        #parent_menu_widget = self.get_parent_menu_widget(self.sep_indexes[index][2])
405        ##if TrayEngine == "KDE":
406            ##parent_menu_widget.removeAction(sep_widget)
407        ##elif TrayEngine == "AppIndicator":
408            ##sep_widget.hide()
409            ##parent_menu_widget.remove(sep_widget)
410        ##else:
411        #if (1):
412            #parent_menu_widget.removeAction(sep_widget)
413        #self.sep_indexes.pop(index)
414
415    #def removeMenu(self, menu_name_id):
416        #index = self.get_menu_index(menu_name_id)
417        #if index < 0: return
418        #menu_widget = self.menu_indexes[index][1]
419        #parent_menu_widget = self.get_parent_menu_widget(self.menu_indexes[index][2])
420        ##if TrayEngine == "KDE":
421            ##parent_menu_widget.removeAction(menu_widget.menuAction())
422        ##elif TrayEngine == "AppIndicator":
423            ##menu_widget.hide()
424            ##parent_menu_widget.remove(menu_widget.get_submenu())
425        ##else:
426        #if (1):
427            #parent_menu_widget.removeAction(menu_widget.menuAction())
428        #self.remove_actions_by_menu_name_id(menu_name_id)
429        #self.remove_separators_by_menu_name_id(menu_name_id)
430        #self.remove_submenus_by_menu_name_id(menu_name_id)
431
432    # -------------------------------------------------------------------------------------------
433
434    #def clearAll(self):
435        ##if TrayEngine == "KDE":
436            ##self.menu.clear()
437        ##elif TrayEngine == "AppIndicator":
438            ##for child in self.menu.get_children():
439                ##self.menu.remove(child)
440        ##else:
441        #if (1):
442            #self.menu.clear()
443
444        #self.act_indexes = []
445        #self.sep_indexes = []
446        #self.menu_indexes = []
447
448    #def clearMenu(self, menu_name_id):
449        #menu_index = self.get_menu_index(menu_name_id)
450        #if menu_index < 0: return
451        #menu_widget = self.menu_indexes[menu_index][1]
452        ##if TrayEngine == "KDE":
453            ##menu_widget.clear()
454        ##elif TrayEngine == "AppIndicator":
455            ##for child in menu_widget.get_submenu().get_children():
456                ##menu_widget.get_submenu().remove(child)
457        ##else:
458        #if (1):
459            #menu_widget.clear()
460        #list_of_submenus = [menu_name_id]
461        #for x in range(0, 10): # 10x level deep, should cover all cases...
462            #for this_menu_name_id, menu_widget, parent_menu_id in self.menu_indexes:
463                #if parent_menu_id in list_of_submenus and this_menu_name_id not in list_of_submenus:
464                    #list_of_submenus.append(this_menu_name_id)
465        #for this_menu_name_id in list_of_submenus:
466            #self.remove_actions_by_menu_name_id(this_menu_name_id)
467            #self.remove_separators_by_menu_name_id(this_menu_name_id)
468            #self.remove_submenus_by_menu_name_id(this_menu_name_id)
469
470    # -------------------------------------------------------------------------------------------
471
472    def getTrayEngine(self):
473        return TrayEngine
474
475    def isTrayAvailable(self):
476        if TrayEngine in ("KDE", "Qt"):
477            # Ask Qt
478            return QSystemTrayIcon.isSystemTrayAvailable()
479
480        if TrayEngine == "AppIndicator":
481            # Ubuntu/Unity always has a systray
482            return True
483
484        return False
485
486    def handleQtCloseEvent(self, event):
487        if self.isTrayAvailable() and self._parent.isVisible():
488            event.accept()
489            self.__hideShowCall()
490            return
491
492        self.close()
493        QMainWindow.closeEvent(self._parent, event)
494
495    # -------------------------------------------------------------------------------------------
496
497    def show(self):
498        if not self._quit_added:
499            self._quit_added = True
500
501            if TrayEngine != "KDE":
502                self.addSeparator("_quit")
503                self.addAction("show", self._parent.tr("Minimize"))
504                self.addAction("quit", self._parent.tr("Quit"))
505                self.setActionIcon("quit", "application-exit")
506                self.connect("show", self.__hideShowCall)
507                self.connect("quit", self.__quitCall)
508
509        if TrayEngine == "KDE":
510            self.tray.setStatus(KStatusNotifierItem.Active)
511        elif TrayEngine == "AppIndicator":
512            self.tray.set_status(AppIndicator.IndicatorStatus.ACTIVE)
513        elif TrayEngine == "Qt":
514            self.tray.show()
515
516    def hide(self):
517        if TrayEngine == "KDE":
518            self.tray.setStatus(KStatusNotifierItem.Passive)
519        elif TrayEngine == "AppIndicator":
520            self.tray.set_status(AppIndicator.IndicatorStatus.PASSIVE)
521        elif TrayEngine == "Qt":
522            self.tray.hide()
523
524    def close(self):
525        if TrayEngine == "KDE":
526            self.menu.close()
527        elif TrayEngine == "AppIndicator":
528            if self._gtk_running:
529                self._gtk_running = False
530                Gtk.main_quit()
531        elif TrayEngine == "Qt":
532            self.menu.close()
533
534    def exec_(self, app):
535        self._app = app
536        if TrayEngine == "AppIndicator":
537            self._gtk_running = True
538            return Gtk.main()
539        else:
540            return app.exec_()
541
542    # -------------------------------------------------------------------------------------------
543
544    def get_act_index(self, act_name_id):
545        for i in range(len(self.act_indexes)):
546            if self.act_indexes[i][iActNameId] == act_name_id:
547                return i
548        else:
549            print("systray.py - Failed to get action index for %s" % act_name_id)
550            return -1
551
552    def get_sep_index(self, sep_name_id):
553        for i in range(len(self.sep_indexes)):
554            if self.sep_indexes[i][iSepNameId] == sep_name_id:
555                return i
556        else:
557            print("systray.py - Failed to get separator index for %s" % sep_name_id)
558            return -1
559
560    def get_menu_index(self, menu_name_id):
561        for i in range(len(self.menu_indexes)):
562            if self.menu_indexes[i][iMenuNameId] == menu_name_id:
563                return i
564        else:
565            print("systray.py - Failed to get menu index for %s" % menu_name_id)
566            return -1
567
568    #def get_parent_menu_widget(self, parent_menu_id):
569        #if parent_menu_id != None:
570            #menu_index = self.get_menu_index(parent_menu_id)
571            #if menu_index >= 0:
572                #return self.menu_indexes[menu_index][1]
573            #else:
574                #print("systray.py::Failed to get parent Menu widget for", parent_menu_id)
575                #return None
576        #else:
577            #return self.menu
578
579    #def remove_actions_by_menu_name_id(self, menu_name_id):
580        #h = 0
581        #for i in range(len(self.act_indexes)):
582            #act_name_id, act_widget, parent_menu_id, act_func = self.act_indexes[i - h]
583            #if parent_menu_id == menu_name_id:
584                #self.act_indexes.pop(i - h)
585                #h += 1
586
587    #def remove_separators_by_menu_name_id(self, menu_name_id):
588        #h = 0
589        #for i in range(len(self.sep_indexes)):
590            #sep_name_id, sep_widget, parent_menu_id = self.sep_indexes[i - h]
591            #if parent_menu_id == menu_name_id:
592                #self.sep_indexes.pop(i - h)
593                #h += 1
594
595    #def remove_submenus_by_menu_name_id(self, submenu_name_id):
596        #h = 0
597        #for i in range(len(self.menu_indexes)):
598            #menu_name_id, menu_widget, parent_menu_id = self.menu_indexes[i - h]
599            #if parent_menu_id == submenu_name_id:
600                #self.menu_indexes.pop(i - h)
601                #h += 1
602
603    # -------------------------------------------------------------------------------------------
604
605    def gtk_call_func(self, gtkmenu, act_name_id):
606        i = self.get_act_index(act_name_id)
607        if i < 0: return None
608
609        return self.act_indexes[i][iActFunc]
610
611    def qt_systray_clicked(self, reason):
612        if reason in (QSystemTrayIcon.DoubleClick, QSystemTrayIcon.Trigger):
613            self.__hideShowCall()
614
615    # -------------------------------------------------------------------------------------------
616
617    def __hideShowCall(self):
618        if self._parent.isVisible():
619            self.setActionText("show", self._parent.tr("Restore"))
620            self._parent.hide()
621
622            if self._app:
623                self._app.setQuitOnLastWindowClosed(False)
624
625        else:
626            self.setActionText("show", self._parent.tr("Minimize"))
627
628            if self._parent.isMaximized():
629                self._parent.showMaximized()
630            else:
631                self._parent.showNormal()
632
633            if self._app:
634                self._app.setQuitOnLastWindowClosed(True)
635
636            QTimer.singleShot(500, self.__raiseWindow)
637
638    def __quitCall(self):
639        if self._app:
640            self._app.setQuitOnLastWindowClosed(True)
641
642        self._parent.hide()
643        self._parent.close()
644
645        if self._app:
646            self._app.quit()
647
648    def __raiseWindow(self):
649        self._parent.activateWindow()
650        self._parent.raise_()
651
652#--------------- main ------------------
653if __name__ == '__main__':
654    from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
655
656    class ExampleGUI(QDialog):
657        def __init__(self, parent=None):
658            QDialog.__init__(self, parent)
659
660            self.setWindowIcon(getIcon("audacity"))
661
662            self.systray = GlobalSysTray(self, "Claudia", "claudia")
663            self.systray.addAction("about", self.tr("About"))
664            self.systray.setIcon("audacity")
665            self.systray.setToolTip("Demo systray app")
666
667            self.systray.connect("about", self.about)
668
669            self.systray.show()
670
671        def about(self):
672            QMessageBox.about(self, self.tr("About"), self.tr("Systray Demo"))
673
674        def done(self, r):
675            QDialog.done(self, r)
676            self.close()
677
678        def closeEvent(self, event):
679            self.systray.close()
680            QDialog.closeEvent(self, event)
681
682    app = QApplication(sys.argv)
683    gui = ExampleGUI()
684    gui.show()
685    sys.exit(gui.systray.exec_(app))
686