Skip to content
Merged
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
148 changes: 148 additions & 0 deletions checkbox-support/checkbox_support/dbus/gnome_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Copyright 2024 Canonical Ltd.
# Written by:
# Paolo Gentili <paolo.gentili@canonical.com>
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# This file 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file. If not, see <http://www.gnu.org/licenses/>.
"""
This modules includes a utility to get display information and set
a new logical monitor configuration via DBus and Mutter.

Original script that inspired this class:
- https://gitlab.gnome.org/GNOME/mutter/-/blob/main/tools/get-state.py
"""

from collections import namedtuple
from typing import Dict, List, Tuple
from gi.repository import GLib, Gio

from checkbox_support.monitor_config import MonitorConfig

Mode = namedtuple("Mode", ["id", "resolution", "is_preferred", "is_current"])


class MonitorConfigGnome(MonitorConfig):
"""
Get and modify the current Monitor configuration via DBus.

DBus interface doc at:
https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/
org.gnome.Mutter.DisplayConfig.xml
"""

NAME = "org.gnome.Mutter.DisplayConfig"
INTERFACE = "org.gnome.Mutter.DisplayConfig"
OBJECT_PATH = "/org/gnome/Mutter/DisplayConfig"

def __init__(self):
self._proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type=Gio.BusType.SESSION,
flags=Gio.DBusProxyFlags.NONE,
info=None,
name=self.NAME,
object_path=self.OBJECT_PATH,
interface_name=self.INTERFACE,
cancellable=None,
)

def get_current_resolutions(self) -> Dict[str, str]:
"""Get current active resolutions for each monitor."""

state = self._get_current_state()
return {
monitor: mode.resolution
for monitor, modes in state[1].items()
for mode in modes
if mode.is_current
}

def set_extended_mode(self):
"""
Set to extend mode so that each monitor can be displayed
at preferred resolution.
"""
state = self._get_current_state()

extended_logical_monitors = []

position_x = 0
for monitor, modes in state[1].items():
preferred = next(mode for mode in modes if mode.is_preferred)
extended_logical_monitors.append(
(
position_x,
0,
1.0,
0,
position_x == 0, # first monitor is primary
[(monitor, preferred.id, {})],
)
)
position_x += int(preferred.resolution.split("x")[0])

self._apply_monitors_config(state[0], extended_logical_monitors)

def _get_current_state(self) -> Tuple[str, Dict[str, List[Mode]]]:
"""
Using DBus signal 'GetCurrentState' to get the available monitors
and related modes.

Check the related DBus XML definition for details over the expected
output data format.
"""
state = self._proxy.call_sync(
method_name="GetCurrentState",
parameters=None,
flags=Gio.DBusCallFlags.NO_AUTO_START,
timeout_msec=-1,
cancellable=None,
)

return (
state[0],
{
monitor[0][0]: [
Mode(
mode[0],
"{}x{}".format(mode[1], mode[2]),
mode[6].get("is-preferred", False),
mode[6].get("is-current", False),
)
for mode in monitor[1]
]
for monitor in state[1]
},
)

def _apply_monitors_config(self, serial: str, logical_monitors: List):
"""
Using DBus signal 'ApplyMonitorsConfig' to apply the given monitor
configuration.

Check the related DBus XML definition for details over the expected
input data format.
"""
self._proxy.call_sync(
method_name="ApplyMonitorsConfig",
parameters=GLib.Variant(
"(uua(iiduba(ssa{sv}))a{sv})",
(
serial,
1, # temporary setting
logical_monitors,
{},
),
),
flags=Gio.DBusCallFlags.NONE,
timeout_msec=-1,
cancellable=None,
)
164 changes: 164 additions & 0 deletions checkbox-support/checkbox_support/dbus/tests/test_gnome_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import sys
import unittest
from unittest.mock import patch, Mock, MagicMock

sys.modules["dbus"] = MagicMock()
sys.modules["dbus.mainloop.glib"] = MagicMock()
sys.modules["gi"] = MagicMock()
sys.modules["gi.repository"] = MagicMock()

from gi.repository import GLib, Gio
from checkbox_support.dbus.gnome_monitor import MonitorConfigGnome


class MonitorConfigGnomeTests(unittest.TestCase):
"""This class provides test cases for the MonitorConfig DBus class."""

@patch("checkbox_support.dbus.gnome_monitor.Gio.DBusProxy")
def test_get_current_resolution(self, mock_dbus_proxy):
"""
Test whether the function returns a dictionary of
monitor-id:resolution for any active monitors.
"""

mock_proxy = Mock()
mock_dbus_proxy.new_for_bus_sync.return_value = mock_proxy

gnome_monitor = MonitorConfigGnome()
mock_proxy.call_sync.return_value = (
1,
[
(
("eDP-1", "LGD", "0x06b3", "0x00000000"),
[
(
"1920x1200@59.950",
1920,
1200,
59.950172424316406,
1.0,
[1.0, 2.0],
{
"is-current": GLib.Variant("b", True),
"is-preferred": GLib.Variant("b", True),
},
)
],
{
"is-builtin": GLib.Variant("b", True),
"display-name": GLib.Variant("s", "Built-in display"),
},
),
(
("HDMI-1", "LGD", "0x06b3", "0x00000000"),
[
(
"2560x1440@59.950",
2560,
1440,
59.950172424316406,
1.0,
[1.0, 2.0],
{
"is-current": GLib.Variant("b", True),
"is-preferred": GLib.Variant("b", True),
},
)
],
{
"is-builtin": GLib.Variant("b", False),
"display-name": GLib.Variant("s", "External Display"),
},
),
],
[],
{},
)
resolutions = gnome_monitor.get_current_resolutions()
self.assertEqual(
resolutions, {"eDP-1": "1920x1200", "HDMI-1": "2560x1440"}
)

@patch("checkbox_support.dbus.gnome_monitor.Gio.DBusProxy")
def test_set_extended_mode(self, mock_dbus_proxy):
"""
Test whether the function set the logical display
configuration to two screens at preferred resolution
placed horizontally.
"""

mock_proxy = Mock()
mock_dbus_proxy.new_for_bus_sync.return_value = mock_proxy

gnome_monitor = MonitorConfigGnome()
mock_proxy.call_sync.return_value = (
1,
[
(
("eDP-1", "LGD", "0x06b3", "0x00000000"),
[
(
"1920x1200@59.950",
1920,
1200,
59.950172424316406,
1.0,
[1.0, 2.0],
{
"is-current": GLib.Variant("b", True),
"is-preferred": GLib.Variant("b", True),
},
)
],
{
"is-builtin": GLib.Variant("b", True),
"display-name": GLib.Variant("s", "Built-in display"),
},
),
(
("HDMI-1", "LGD", "0x06b3", "0x00000000"),
[
(
"2560x1440@59.950",
2560,
1440,
59.950172424316406,
1.0,
[1.0, 2.0],
{
"is-current": GLib.Variant("b", True),
"is-preferred": GLib.Variant("b", True),
},
)
],
{
"is-builtin": GLib.Variant("b", False),
"display-name": GLib.Variant("s", "External Display"),
},
),
],
[],
{},
)
gnome_monitor.set_extended_mode()

logical_monitors = [
(0, 0, 1.0, 0, True, [("eDP-1", "1920x1200@59.950", {})]),
(1920, 0, 1.0, 0, False, [("HDMI-1", "2560x1440@59.950", {})]),
]
expected_logical_monitors = GLib.Variant(
"(uua(iiduba(ssa{sv}))a{sv})",
(
1,
1,
logical_monitors,
{},
),
)
mock_proxy.call_sync.assert_called_with(
method_name="ApplyMonitorsConfig",
parameters=expected_logical_monitors,
flags=Gio.DBusCallFlags.NONE,
timeout_msec=-1,
cancellable=None,
)
37 changes: 37 additions & 0 deletions checkbox-support/checkbox_support/helpers/display_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This file is part of Checkbox.
#
# Copyright 2024 Canonical Ltd.
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# Checkbox 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
"""
This module provides a display system agnostic way to get information about
the available displays connected to the system.
"""

import os
from checkbox_support.monitor_config import MonitorConfig
from checkbox_support.dbus.gnome_monitor import MonitorConfigGnome
from checkbox_support.parsers.xrandr import MonitorConfigX11


def get_monitor_config() -> MonitorConfig:
"""
Depending on the current host, initiate and return
an appropriate MonitorConfig.
"""
if "GNOME" in os.getenv("XDG_CURRENT_DESKTOP", ""):
return MonitorConfigGnome()
elif "x11" == os.getenv("XDG_SESSION_TYPE", ""):
return MonitorConfigX11()

raise ValueError("Can't find a proper MonitorConfig.")
Loading