Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor desktop notifications and add unit tests #2609

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,46 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Implements the desktop notification service."""

import importlib
import logging

logger = logging.getLogger(__name__)

try:
import gi

gi.require_version("Notify", "0.7")
gi.require_version("Gtk", "3.0")
from gi.repository import GLib # this import is allowed to fail making the entire feature unavailable
from gi.repository import Gtk # this import is allowed to fail making the entire feature unavailable
from gi.repository import Notify # this import is allowed to fail making the entire feature unavailable
def notifications_available():
"""Checks if notification service is available."""
notifications_supported = False
try:
import gi

gi.require_version("Notify", "0.7")
gi.require_version("Gtk", "3.0")

importlib.util.find_spec("gi.repository.GLib")
importlib.util.find_spec("gi.repository.Gtk")
importlib.util.find_spec("gi.repository.Notify")

notifications_supported = True
except ValueError as e:
logger.warning(f"Notification service is not available: {e}")
return notifications_supported


available = True
except (ValueError, ImportError):
available = False
available = notifications_available()

if available:
from gi.repository import GLib
from gi.repository import Gtk
from gi.repository import Notify

# cache references to shown notifications here to allow reuse
_notifications = {}
_ICON_LISTS = {}

def init():
"""Init the notifications system."""
"""Initialize desktop notifications."""
global available
if available:
if not Notify.is_initted():
Expand All @@ -50,13 +67,14 @@ def init():
return available and Notify.is_initted()

def uninit():
"""Stop desktop notifications."""
if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info("stopping desktop notifications")
_notifications.clear()
Notify.uninit()

def show(dev, message, icon=None):
def show(dev, message: str, icon=None):
"""Show a notification with title and text."""
if available and (Notify.is_initted() or init()):
summary = dev.name
Expand All @@ -68,30 +86,27 @@ def show(dev, message, icon=None):
n.set_urgency(Notify.Urgency.NORMAL)
n.set_hint("desktop-entry", GLib.Variant("s", "solaar")) # replace with better name late
try:
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("showing %s", n)
n.show()
except Exception:
logger.exception("showing %s", n)

_ICON_LISTS = {}
logger.exception(f"showing {n}")

def device_icon_list(name="_", kind=None):
icon_list = _ICON_LISTS.get(name)
if icon_list is None:
# names of possible icons, in reverse order of likelihood
# the theme will hopefully pick up the most appropriate
icon_list = ["preferences-desktop-peripherals"]
kind = str(kind)
if kind:
if str(kind) == "numpad":
if kind == "numpad":
icon_list += ("input-keyboard", "input-dialpad")
elif str(kind) == "touchpad":
elif kind == "touchpad":
icon_list += ("input-mouse", "input-tablet")
elif str(kind) == "trackball":
elif kind == "trackball":
icon_list += ("input-mouse",)
elif str(kind) == "headset":
elif kind == "headset":
icon_list += ("audio-headphones", "audio-headset")
icon_list += ("input-" + str(kind),)
icon_list += (f"input-{kind}",)
_ICON_LISTS[name] = icon_list
return icon_list

Expand Down
27 changes: 19 additions & 8 deletions lib/logitech_receiver/settings_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@
import traceback

from time import time
from typing import Callable

from solaar.i18n import _

from . import base
from . import common
from . import descriptors
from . import desktop_notifications
from . import diversion
from . import hidpp10_constants
from . import hidpp20
from . import hidpp20_constants
from . import notify
from . import settings
from . import special_keys
from .hidpp10_constants import Registers
Expand Down Expand Up @@ -728,6 +729,15 @@ def build(cls, setting_class, device):


class DpiSlidingXY(settings.RawXYProcessing):
def __init__(
self,
*args,
show_notification: Callable[[str, str], bool],
**kwargs,
):
super().__init__(*args, **kwargs)
self._show_notification = show_notification

def activate_action(self):
self.dpiSetting = next(filter(lambda s: s.name == "dpi" or s.name == "dpi_extended", self.device.settings), None)
self.dpiChoices = list(self.dpiSetting.choices)
Expand All @@ -745,12 +755,11 @@ def setNewDpi(self, newDpiIdx):
self.device.setting_callback(self.device, type(self.dpiSetting), [newDpi])

def displayNewDpi(self, newDpiIdx):
if notify.available:
selected_dpi = self.dpiChoices[newDpiIdx]
min_dpi = self.dpiChoices[0]
max_dpi = self.dpiChoices[-1]
reason = f"DPI {selected_dpi} [min {min_dpi}, max {max_dpi}]"
notify.show(self.device, reason)
selected_dpi = self.dpiChoices[newDpiIdx]
min_dpi = self.dpiChoices[0]
max_dpi = self.dpiChoices[-1]
reason = f"DPI {selected_dpi} [min {min_dpi}, max {max_dpi}]"
self._show_notification(self.device, reason)

def press_action(self, key): # start tracking
self.starting = True
Expand Down Expand Up @@ -912,7 +921,9 @@ def build(cls, setting_class, device):
if _F.ADJUSTABLE_DPI in device.features:
choices[k.key] = setting_class.choices_universe
if sliding is None:
sliding = DpiSlidingXY(device, name="DpiSlding")
sliding = DpiSlidingXY(
device, name="DpiSliding", show_notification=desktop_notifications.show
)
else:
choices[k.key] = setting_class.choices_divert
if not choices:
Expand Down
8 changes: 4 additions & 4 deletions lib/solaar/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
from solaar.ui.window import find_device

from . import common
from . import desktop_notifications
from . import diversion_rules
from . import notify
from . import tray
from . import window

Expand All @@ -47,7 +47,7 @@ def _startup(app, startup_hook, use_tray, show_window):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("startup registered=%s, remote=%s", app.get_is_registered(), app.get_is_remote())
common.start_async()
notify.init()
desktop_notifications.init()
if use_tray:
tray.init(lambda _ignore: window.destroy())
window.init(show_window, use_tray)
Expand Down Expand Up @@ -85,7 +85,7 @@ def _shutdown(app, shutdown_hook):
shutdown_hook()
common.stop_async()
tray.destroy()
notify.uninit()
desktop_notifications.uninit()


def run_loop(startup_hook, shutdown_hook, use_tray, show_window):
Expand Down Expand Up @@ -120,7 +120,7 @@ def _status_changed(device, alert, reason, refresh=False):
diversion_rules.update_devices()

if alert & (Alert.NOTIFICATION | Alert.ATTENTION):
notify.show(device, reason)
desktop_notifications.show(device, reason)


def status_changed(device, alert=Alert.NONE, reason=None, refresh=False):
Expand Down
35 changes: 22 additions & 13 deletions lib/solaar/ui/notify.py → lib/solaar/ui/desktop_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import importlib

# Optional desktop notifications.

import logging

from solaar import NAME
Expand All @@ -26,27 +26,35 @@
logger = logging.getLogger(__name__)


try:
import gi
def notifications_available():
"""Checks if notification service is available."""
notifications_supported = False
try:
import gi

gi.require_version("Notify", "0.7")
# this import is allowed to fail, in which case the entire feature is unavailable
from gi.repository import GLib
from gi.repository import Notify
gi.require_version("Notify", "0.7")

importlib.util.find_spec("gi.repository.GLib")
importlib.util.find_spec("gi.repository.Notify")

notifications_supported = True
except ValueError as e:
logger.warning(f"Notification service is not available: {e}")
return notifications_supported

# assumed to be working since the import succeeded
available = True

except (ValueError, ImportError):
available = False
available = notifications_available()

if available:
from gi.repository import GLib
from gi.repository import Notify

# cache references to shown notifications here, so if another status comes
# while its notification is still visible we don't create another one
_notifications = {}

def init():
"""Init the notifications system."""
"""Initialize desktop notifications."""
global available
if available:
if not Notify.is_initted():
Expand All @@ -60,6 +68,7 @@ def init():
return available and Notify.is_initted()

def uninit():
"""Stop desktop notifications."""
if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info("stopping desktop notifications")
Expand Down Expand Up @@ -117,7 +126,7 @@ def show(dev, reason=None, icon=None, progress=None):
try:
n.show()
except Exception:
logger.exception("showing %s", n)
logger.exception(f"showing {n}")

else:

Expand Down
23 changes: 23 additions & 0 deletions tests/logitech_receiver/test_desktop_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from unittest import mock

from logitech_receiver import desktop_notifications


def test_notifications_available():
result = desktop_notifications.notifications_available()

assert not result


def test_init():
assert not desktop_notifications.init()


def test_uninit():
assert desktop_notifications.uninit() is None


def test_show():
dev = mock.MagicMock()
reason = "unknown"
assert desktop_notifications.show(dev, reason) is None
28 changes: 28 additions & 0 deletions tests/solaar/ui/test_desktop_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from unittest import mock

from solaar.ui import desktop_notifications


def test_notifications_available():
result = desktop_notifications.notifications_available()

assert not result


def test_init():
assert not desktop_notifications.init()


def test_uninit():
assert desktop_notifications.uninit() is None


def test_alert():
reason = "unknown"
assert desktop_notifications.alert(reason) is None


def test_show():
dev = mock.MagicMock()
reason = "unknown"
assert desktop_notifications.show(dev, reason) is None
Loading