nordvpntray/venv/lib/python3.12/site-packages/pystray/_util/gtk.py

184 lines
5.4 KiB
Python
Raw Permalink Normal View History

2024-12-23 11:03:07 +01:00
# coding=utf-8
# pystray
# Copyright (C) 2016-2022 Moses Palmér
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import atexit
import functools
import os
import signal
import tempfile
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, GObject, Gtk
from pystray import _base
from . import notify_dbus
# Make sure Gtk works
if not Gtk.init_check()[0]:
raise ImportError('Gtk could not be initialised')
def mainloop(f):
"""Marks a function to be executed in the main loop.
The function will be scheduled to be executed later in the mainloop.
:param callable f: The function to execute. Its return value is discarded.
"""
@functools.wraps(f)
def inner(*args, **kwargs):
def callback(*args, **kwargs):
"""A callback that executes ``f`` and then returns ``False``.
"""
try:
f(*args, **kwargs)
finally:
return False
# Execute the callback as an idle function
GObject.idle_add(callback, *args, **kwargs)
return inner
class GtkIcon(_base.Icon):
def __init__(self, *args, **kwargs):
super(GtkIcon, self).__init__(*args, **kwargs)
self._loop = None
self._icon_path = None
self._notifier = None
def _run(self):
self._loop = GLib.MainLoop.new(None, False)
self._initialize()
try:
self._loop.run()
except:
self._log.error(
'An error occurred in the main loop', exc_info=True)
finally:
self._finalize()
def _run_detached(self):
self._initialize()
atexit.register(lambda: self._finalize())
def _initialize(self):
"""Performs shared initialisation steps.
"""
# Make sure that we do not inhibit ctrl+c; this is only possible from
# the main thread
try:
signal.signal(signal.SIGINT, signal.SIG_DFL)
except ValueError:
pass
self._notifier = notify_dbus.Notifier()
self._mark_ready()
@mainloop
def _notify(self, message, title=None):
self._notifier.notify(title or self.title, message, self._icon_path)
@mainloop
def _remove_notification(self):
self._notifier.hide()
@mainloop
def _stop(self):
if self._loop is not None:
self._loop.quit()
def _create_menu(self, descriptors):
"""Creates a :class:`Gtk.Menu` from a :class:`pystray.Menu` instance.
:param descriptors: The menu descriptors. If this is falsy, ``None`` is
returned.
:return: a :class:`Gtk.Menu` or ``None``
"""
if not descriptors:
return None
else:
menu = Gtk.Menu.new()
for descriptor in descriptors:
menu.append(self._create_menu_item(descriptor))
menu.show_all()
return menu
def _create_menu_item(self, descriptor):
"""Creates a :class:`Gtk.MenuItem` from a :class:`pystray.MenuItem`
instance.
:param descriptor: The menu item descriptor.
:return: a :class:`Gtk.MenuItem`
"""
if descriptor is _base.Menu.SEPARATOR:
return Gtk.SeparatorMenuItem()
else:
if descriptor.checked is not None:
menu_item = Gtk.CheckMenuItem.new_with_label(descriptor.text)
menu_item.set_active(descriptor.checked)
menu_item.set_draw_as_radio(descriptor.radio)
else:
menu_item = Gtk.MenuItem.new_with_label(descriptor.text)
if descriptor.submenu:
menu_item.set_submenu(self._create_menu(descriptor.submenu))
else:
menu_item.connect('activate', self._handler(descriptor))
if descriptor.default:
menu_item.get_children()[0].set_markup(
'<b>%s</b>' % GLib.markup_escape_text(descriptor.text))
menu_item.set_sensitive(descriptor.enabled)
return menu_item
def _finalize(self):
self._remove_fs_icon()
self._notifier.hide()
def _remove_fs_icon(self):
"""Removes the temporary file used for the icon.
"""
try:
if self._icon_path:
os.unlink(self._icon_path)
self._icon_path = None
except:
pass
self._icon_valid = False
def _update_fs_icon(self):
"""Updates the icon file.
This method will update :attr:`_icon_path` and create a new image file.
If an icon is already set, call :meth:`_remove_fs_icon` first to ensure
that the old file is removed.
"""
self._icon_path = tempfile.mktemp()
with open(self._icon_path, 'wb') as f:
self.icon.save(f, 'PNG')
self._icon_valid = True