diff --git a/checkbox-support/checkbox_support/dbus/gnome_monitor.py b/checkbox-support/checkbox_support/dbus/gnome_monitor.py index 43fb4e1ccc..ee87197e71 100644 --- a/checkbox-support/checkbox_support/dbus/gnome_monitor.py +++ b/checkbox-support/checkbox_support/dbus/gnome_monitor.py @@ -22,8 +22,9 @@ """ from collections import namedtuple -from typing import Dict, List, Tuple, Set +from typing import Dict, List, Tuple, Set, Callable, Any from gi.repository import GLib, Gio +import itertools from checkbox_support.monitor_config import MonitorConfig @@ -104,6 +105,84 @@ def set_extended_mode(self) -> Dict[str, str]: self._apply_monitors_config(state[0], extended_logical_monitors) return configuration + def cycle( + self, + resolution: bool = True, + transform: bool = False, + resolution_filter: Callable[[List[Mode]], List[Mode]] = None, + action: Callable[..., Any] = None, + **kwargs + ): + """ + Automatically cycle through the supported monitor configurations. + + Args: + resolution: Cycling the resolution or not. + + transform: Cycling the transform or not. + + resolution_filter: For filtering resolution then returning needed, + it will take List[Mode] as parameter and return + the same data type + + action: For extra steps for each cycle, + the string is constructed by + [monitor name]_[resolution]_[transform]_. + Please note that the delay is needed inside this + callback to wait the monitors to response + """ + monitors = [] + modes_list = [] + # ["normal": 0, "left": 1, "inverted": 6, "right": 3] + trans_list = [0, 1, 6, 3] if transform else [0] + + # for multiple monitors, we need to create resolution combination + state = self._get_current_state() + for monitor, modes in state[1].items(): + monitors.append(monitor) + if resolution_filter: + modes_list.append(resolution_filter(modes)) + else: + modes_list.append(modes) + mode_combination = list(itertools.product(*modes_list)) + + for mode in mode_combination: + for trans in trans_list: + logical_monitors = [] + position_x = 0 + uni_string = "" + for monitor, m in zip(monitors, mode): + uni_string += "{}_{}_{}_".format( + monitor, + m.resolution, + { + 0: "normal", + 1: "left", + 3: "right", + 6: "inverted", + }.get(trans), + ) + logical_monitors.append( + ( + position_x, + 0, + 1.0, + trans, + position_x == 0, # first monitor is primary + [(monitor, m.id, {})], + ) + ) + # left and right should convert x and y + xy = 1 if (trans == 1 or trans == 3) else 0 + position_x += int(m.resolution.split("x")[xy]) + self._apply_monitors_config(state[0], logical_monitors) + if action: + action(uni_string, **kwargs) + if not resolution: + break + # change back to preferred monitor configuration + self.set_extended_mode() + def _get_current_state(self) -> Tuple[str, Dict[str, List[Mode]]]: """ Using DBus signal 'GetCurrentState' to get the available monitors diff --git a/checkbox-support/checkbox_support/dbus/tests/test_gnome_monitor.py b/checkbox-support/checkbox_support/dbus/tests/test_gnome_monitor.py index cda56c2108..10526ec291 100644 --- a/checkbox-support/checkbox_support/dbus/tests/test_gnome_monitor.py +++ b/checkbox-support/checkbox_support/dbus/tests/test_gnome_monitor.py @@ -1,3 +1,4 @@ +import re import sys import unittest from unittest.mock import patch, Mock, MagicMock @@ -230,3 +231,185 @@ def test_set_extended_mode(self, mock_dbus_proxy): "HDMI-1": "2560x1440", } self.assertDictEqual(configuration, expected) + + @patch("checkbox_support.dbus.gnome_monitor.Gio.DBusProxy") + def test_cycle(self, mock_dbus_proxy): + """ + Test the cycle could get the right monitors configuration + and send to ApplyMonitorsConfig. + """ + + 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.cycle() + + 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, + ) + + @patch("checkbox_support.dbus.gnome_monitor.Gio.DBusProxy") + def test_cycle_no_cycling(self, mock_dbus_proxy): + """ + Test the cycle could get the right monitors configuration + (without res and transform change) and send to ApplyMonitorsConfig. + """ + + 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"), + }, + ), + ], + [], + {}, + ) + # mock callback + mock_callback = MagicMock() + gnome_monitor.cycle( + resolution=False, + transform=False, + resoultion_filter=mock_callback, + action=mock_callback, + ) + + 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, + ) + argument_string = mock_callback.call_args[0][0] + p1 = "HDMI-1_2560x1440_normal_" + p2 = "eDP-1_1920x1200_normal_" + pattern = re.compile("{}{}|{}{}".format(p1, p2, p2, p1)) + assert pattern.match(argument_string)