From 677cef8107301f8f3f5f04ba1b6765bfbf7df46d Mon Sep 17 00:00:00 2001 From: Kalmat Date: Mon, 28 Aug 2023 12:35:40 +0200 Subject: [PATCH] ALL: Reorganized to avoid IDEs showing external and / or private elements findMonitor() returns a list of Monitor instances MACOS: Added contrast(), setContrast(), isOn() and isAttached(), improved setMode() --- CHANGES.txt | 5 +- README.md | 5 +- ...-any.whl => PyMonCtl-0.1-py3-none-any.whl} | Bin 82003 -> 83502 bytes setup.py | 2 +- src/ewmhlib/Props.py | 2 - src/ewmhlib/Structs.py | 53 +- src/ewmhlib/__init__.py | 21 +- src/ewmhlib/_main.py | 9 + src/pymonctl/__init__.py | 865 +----------------- src/pymonctl/_main.py | 853 +++++++++++++++++ src/pymonctl/_pymonctl_linux.py | 193 ++-- src/pymonctl/_pymonctl_macos.py | 19 +- src/pymonctl/_pymonctl_win.py | 187 ++-- src/pymonctl/{structs.py => _structs.py} | 27 +- tests/test_pymonctl.py | 2 +- 15 files changed, 1143 insertions(+), 1100 deletions(-) rename dist/{PyMonCtl-0.0.12-py3-none-any.whl => PyMonCtl-0.1-py3-none-any.whl} (51%) create mode 100644 src/ewmhlib/_main.py create mode 100644 src/pymonctl/_main.py rename src/pymonctl/{structs.py => _structs.py} (84%) diff --git a/CHANGES.txt b/CHANGES.txt index d968730..4d2b9b6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ -0.0.12, 2023/08/24 -- ALL: findMonitor() returns a list of Monitor instances - MACOS: Added contrast(), setContrast(), isOn() and isAttached(), improved setMode() +0.1, 2023/08/25 -- ALL: Reorganized to avoid IDEs showing external and / or private elements + findMonitor() returns a list of Monitor instances + MACOS: Added contrast(), setContrast(), isOn() and isAttached(), improved setMode() 0.0.11, 2023/08/23 -- MACOS: Added display_manager_lib (thanks to University of Utah - Marriott Library - Apple Infrastructure) WIN32: Fixed setScale() 0.0.10, 2023/08/21 -- ALL: Fixed watchdog thread diff --git a/README.md b/README.md index 34e308c..03ca64a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Cross-Platform module which provides a set of features to get info on and control monitors. -#### My most sincere thanks and appreciation to the University of Utah Student Computing Labs for their awesome work on the display_manager_lib module, for sharing it so generously, and most especially for allowing to be integrated into PyMonCtl - External tools/extensions/APIs used: - Linux: - Xlib's randr extension @@ -16,6 +14,9 @@ External tools/extensions/APIs used: - macOS: - pmset command-line tool + +My most sincere thanks and appreciation to the University of Utah Student Computing Labs for their awesome work on the display_manager_lib module, for sharing it so generously, and most especially for allowing to be integrated into PyMonCtl + ## General Functions Functions to get monitor instances, get info and manage monitors plugged to the system. diff --git a/dist/PyMonCtl-0.0.12-py3-none-any.whl b/dist/PyMonCtl-0.1-py3-none-any.whl similarity index 51% rename from dist/PyMonCtl-0.0.12-py3-none-any.whl rename to dist/PyMonCtl-0.1-py3-none-any.whl index 8a55f5f23ca2c01193bf0c827f161101f204faa8..016c6a7a0b5664e009858804415f22c37dcac1a4 100644 GIT binary patch delta 39111 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 delta 37725 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 sys_platform == 'win32'", diff --git a/src/ewmhlib/Props.py b/src/ewmhlib/Props.py index 3019b59..1efca2c 100644 --- a/src/ewmhlib/Props.py +++ b/src/ewmhlib/Props.py @@ -135,5 +135,3 @@ class StackMode(IntEnum): class HintAction(IntEnum): KEEP = -1 REMOVE = -2 - - diff --git a/src/ewmhlib/Structs.py b/src/ewmhlib/Structs.py index b61a4f8..357e769 100644 --- a/src/ewmhlib/Structs.py +++ b/src/ewmhlib/Structs.py @@ -9,6 +9,14 @@ class ScreensInfo(TypedDict): + """ + Container class to handle ScreensInfo struct: + + - screen_number (str): int (sequential) + - is_default (bool): ''True'' if the screen is the default screen + - screen (Xlib.Struct): screen Struct (see Xlib documentation) + - root (Xlib.xobject.drawable.Window): root X-Window object belonging to screen + """ screen_number: str is_default: bool screen: Struct @@ -16,6 +24,13 @@ class ScreensInfo(TypedDict): class DisplaysInfo(TypedDict): + """ + Container class to handle DisplaysInfo struct: + + - name: Display name (as per Xlib.display.Display(name)) + - is_default: ''True'' if the display is the default display + - screens: list of ScreensInfo structs belonging to display + """ name: str is_default: bool screens: List[ScreensInfo] @@ -25,7 +40,22 @@ class DisplaysInfo(TypedDict): Perhaps unnecesary since structs below are defined in Xlib.xobject.icccm.*, though in a more complex way. """ class WmHints(TypedDict): - # {'flags': 103, 'input': 1, 'initial_state': 1, 'icon_pixmap': , 'icon_window': , 'icon_x': 0, 'icon_y': 0, 'icon_mask': , 'window_group': } + """ + Container class to handle WmHints struct: + + Example: + { + 'flags': 103, + 'input': 1, + 'initial_state': 1, + 'icon_pixmap': , + 'icon_window': , + 'icon_x': 0, + 'icon_y': 0, + 'icon_mask': , + 'window_group': + } + """ flags: int input_mode: int initial_state: int @@ -38,12 +68,31 @@ class WmHints(TypedDict): class Aspect(TypedDict): + """Container class to handle Aspect struct (num, denum)""" num: int denum: int class WmNormalHints(TypedDict): - # {'flags': 848, 'min_width': 387, 'min_height': 145, 'max_width': 0, 'max_height': 0, 'width_inc': 9, 'height_inc': 18, 'min_aspect': ({'num': 0, 'denum': 0}), 'max_aspect': ({'num': 0, 'denum': 0}), 'base_width': 66, 'base_height': 101, 'win_gravity': 1} + """ + Container class to handle WmNormalHints + + Example: + { + 'flags': 848, + 'min_width': 387, + 'min_height': 145, + 'max_width': 0, + 'max_height': 0, + 'width_inc': 9, + 'height_inc': 18, + 'min_aspect': ({'num': 0, 'denum': 0}), + 'max_aspect': ({'num': 0, 'denum': 0}), + 'base_width': 66, + 'base_height': 101, + 'win_gravity': 1 + } + """ flags: int min_width: int min_height: int diff --git a/src/ewmhlib/__init__.py b/src/ewmhlib/__init__.py index ec88e64..bf2ab5f 100644 --- a/src/ewmhlib/__init__.py +++ b/src/ewmhlib/__init__.py @@ -1,14 +1,5 @@ -from __future__ import annotations - -import sys -assert sys.platform == "linux" - -from ._ewmhlib import (displaysCount, getDisplaysNames, getDisplaysInfo, getDisplayFromRoot, getDisplayFromWindow, - getProperty, getPropertyValue, changeProperty, sendMessage, _xlibGetAllWindows, - defaultDisplay, defaultScreen, defaultRoot, RootWindow, defaultRootWindow, EwmhWindow - ) -import ewmhlib.Props as Props -import ewmhlib.Structs as Structs +#!/usr/bin/python +# -*- coding: utf-8 -*- __all__ = [ "version", "displaysCount", "getDisplaysNames", "getDisplaysInfo", "getDisplayFromRoot", "getDisplayFromWindow", @@ -17,10 +8,16 @@ "Props", "Structs" ] - __version__ = "0.0.1" def version(numberOnly: bool = True): """Returns the current version of ewmhlib module, in the form ''x.x.xx'' as string""" return ("" if numberOnly else "EWMHlib-")+__version__ + + +from ._main import (displaysCount, getDisplaysNames, getDisplaysInfo, getDisplayFromRoot, getDisplayFromWindow, + getProperty, getPropertyValue, changeProperty, sendMessage, + defaultDisplay, defaultScreen, defaultRoot, RootWindow, defaultRootWindow, EwmhWindow, + Props, Structs + ) diff --git a/src/ewmhlib/_main.py b/src/ewmhlib/_main.py new file mode 100644 index 0000000..0533c10 --- /dev/null +++ b/src/ewmhlib/_main.py @@ -0,0 +1,9 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from ._ewmhlib import (displaysCount, getDisplaysNames, getDisplaysInfo, getDisplayFromRoot, getDisplayFromWindow, + getProperty, getPropertyValue, changeProperty, sendMessage, + defaultDisplay, defaultScreen, defaultRoot, RootWindow, defaultRootWindow, EwmhWindow + ) +import ewmhlib.Props as Props +import ewmhlib.Structs as Structs diff --git a/src/pymonctl/__init__.py b/src/pymonctl/__init__.py index 46c2861..82ca6b9 100644 --- a/src/pymonctl/__init__.py +++ b/src/pymonctl/__init__.py @@ -1,25 +1,16 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -from __future__ import annotations - -import sys -import threading -from abc import abstractmethod, ABC -from collections.abc import Callable -from typing import List, Optional, Union, Tuple - -from pymonctl.structs import DisplayMode, ScreenValue, Size, Point, Box, Rect, Position, Orientation __all__ = [ - "getAllMonitors", "getPrimary", "findMonitor", "findMonitorInfo", "arrangeMonitors", + "getAllMonitors", "getAllMonitorsDict", "getMonitorsCount", "getPrimary", "findMonitor", "findMonitorInfo", + "arrangeMonitors", "getMousePos", "version", "Monitor", "enableUpdateInfo", "disableUpdateInfo", "isUpdateInfoEnabled", "isWatchdogEnabled", "updateWatchdogInterval", "plugListenerRegister", "plugListenerUnregister", "isPlugListenerRegistered", "changeListenerRegister", "changeListenerUnregister", "isChangeListenerRegistered", - "DisplayMode", "ScreenValue", "Size", "Point", "Box", "Rect", "Position", "Orientation", - "getMousePos", "version", "Monitor" + "DisplayMode", "ScreenValue", "Size", "Point", "Box", "Rect", "Position", "Orientation" ] -__version__ = "0.0.12" +__version__ = "0.1" def version(numberOnly: bool = True) -> str: @@ -27,844 +18,10 @@ def version(numberOnly: bool = True) -> str: return ("" if numberOnly else "PyMonCtl-")+__version__ -def _pointInBox(x: int, y: int, left: int, top: int, width: int, height: int) -> bool: - """Returns ``True`` if the ``(x, y)`` point is within the box described - by ``(left, top, width, height)``.""" - return left <= x <= left + width and top <= y <= top + height - - -def getAllMonitors() -> list[Monitor]: - """ - Get the list with all Monitor instances from plugged monitors. - - In case you plan to use this function in a scenario in which it could be invoked quickly and repeatedly, - it's highly recommended to enable update watchdog (see enableUpdate() function). - - :return: list of Monitor instances - """ - global _updateScreens - if _updateScreens is None: - return _getAllMonitors() - else: - return _updateScreens.getMonitors() - - -def getAllMonitorsDict() -> dict[str, ScreenValue]: - """ - Get all monitors info plugged to the system, as a dict. - - In case you plan to use this function in a scenario in which it could be invoked quickly and repeatedly, - it's highly recommended to enable update watchdog (see enableUpdate() function). - - :return: Monitors info as python dictionary - - Output Format: - Key: - Display name (in macOS it is necessary to add handle to avoid duplicates) - - Values: - "system_name": - display name as returned by the system (in macOS, the name can be duplicated!) - "handle": - display handle according to each platform/OS - "is_primary": - ''True'' if monitor is primary (shows clock and notification area, sign in, lock, CTRL+ALT+DELETE screens...) - "position": - Point(x, y) struct containing the display position ((0, 0) for the primary screen) - "size": - Size(width, height) struct containing the display size, in pixels - "workarea": - Rect(left, top, right, bottom) struct with the screen workarea, in pixels - "scale": - Scale ratio, as a tuple of (x, y) scale percentage - "dpi": - Dots per inch, as a tuple of (x, y) dpi values - "orientation": - Display orientation: 0 - Landscape / 1 - Portrait / 2 - Landscape (reversed) / 3 - Portrait (reversed) - "frequency": - Refresh rate of the display, in Hz - "colordepth": - Bits per pixel referred to the display color depth - """ - global _updateScreens - if _updateScreens is None: - return _getAllMonitorsDict() - else: - return _updateScreens.getScreens() - - -def getMonitorsCount() -> int: - """ - Get the number of monitors currently connected to the system. - - :return: number of monitors as integer - """ - return _getMonitorsCount() - - -def getPrimary() -> Monitor: - """ - Get primary monitor instance. This is equivalent to invoking ''Monitor()'', with empty input params. - - :return: Monitor instance or None - """ - return _getPrimary() - - -def findMonitor(x: int, y: int) -> Optional[List[Monitor]]: - """ - Get monitor instance in which given coordinates (x, y) are found. - - :return: Monitor instance or None - """ - return _findMonitor(x, y) - - -def findMonitorInfo(x: int, y: int) -> dict[str, ScreenValue]: - """ - Get monitor info in which given coordinates (x, y) are found. - - :return: monitor info (see getAllMonitorsDict() doc) as dictionary, or empty - """ - info: dict[str, ScreenValue] = {} - monitors = getAllMonitorsDict() - for monitor in monitors.keys(): - pos = monitors[monitor]["position"] - size = monitors[monitor]["size"] - if _pointInBox(x, y, pos.x, pos.y, size.width, size.height): - info[monitor] = monitors[monitor] - break - return info - - -def arrangeMonitors(arrangement: dict[str, dict[str, Union[str, int, Position, Point, Size]]]): - """ - Arrange all monitors in a given shape. - - For that, you must pass a dict with the following structure: - "Monitor name": - monitor name as keys() returned by getAllMonitorsDict() (don't use "system_name" value for this) - "relativePos": - position of this monitor in relation to the monitor provided in ''relativeTo'' - "relativeTo": - monitor name to which ''relativePos'' is referred to (or None if PRIMARY) - - - You MUST pass the position of ALL monitors, and SET ONE of them as PRIMARY. - - HIGHLY RECOMMENDED: When building complex arrangements, start by the primary monitor and then build the rest - taking previous ones as references. - - EXAMPLE for a 3-Monitors setup in which second is at the left and third is on top of primary monitor: - - { - "Display_1": {"relativePos": Position.PRIMARY, "relativeTo": None}, - - "Display_2": {"relativePos": Position.LEFT_TOP, "relativeTo": "Display_1"}, - - "Display_3": {"relativePos": Position.ABOVE_LEFT, "relativeTo": "Display_1"} - } - - :param arrangement: arrangement structure as dict - """ - _arrangeMonitors(arrangement) - - -def getMousePos() -> Point: - """ - Get the current (x, y) coordinates of the mouse pointer on screen, in pixels - - :return: Point struct - """ - return _getMousePos() - - -class BaseMonitor(ABC): - - @property - @abstractmethod - def size(self) -> Optional[Size]: - """ - Get the dimensions of the monitor as a size struct (width, height) - - This property can not be set independently. To do so, choose an allowed mode (from monitor.allModes) - and set the monitor mode property (monitor.mode = selectedMode) - - :return: Size - """ - raise NotImplementedError - - @property - @abstractmethod - def workarea(self) -> Optional[Rect]: - """ - Get dimensions of the "usable by applications" area (screen size minus docks, taskbars and so on), as - a rect struct (x, y, right, bottom) - - This property can not be set. - - :return: Rect - """ - raise NotImplementedError - - @property - @abstractmethod - def position(self) -> Optional[Point]: - """ - Get monitor position coordinates as a point struct (x, y) - - This property can not be set. Use setPosition() method instead. - - :return: Point - """ - raise NotImplementedError - - @abstractmethod - def setPosition(self, relativePos: Union[int, Position], relativeTo: Optional[str]): - """ - Change relative position of the current the monitor in relation to another existing monitor (e.g. primary monitor). - - In general, it is HIGHLY recommendable to use arrangeMonitors() method instead of setPosition(), and most - especially in complex arrangements or setups with more than 2 monitors. - - Important issues: - - - On Windows, primary monitor is mandatory, and it is always placed in (0, 0) coordinates. Besides, the monitors can not overlap. In case the monitor you want to reposition is the primary or the unique one, it will have no effect. To do so, you must switch the primary monitor first, then reposition it. - - - On Linux, primary monitor can be anywhere, monitors can overlap and even there can be no primary monitor - - - On macOS, tests in multi-monitor setups are still required to confirm these behaviors and produce a final version - - :param relativePos: position in relation to another existing monitor (e.g. primary) as per Positions.* - :param relativeTo: monitor in relation to which this monitor must be placed - """ - raise NotImplementedError - - @property - @abstractmethod - def box(self) -> Optional[Box]: - """ - Get monitor dimensions as a box struct (x, y, width, height) - - This property can not be set. - - :return: Box - """ - raise NotImplementedError - - @property - @abstractmethod - def rect(self) -> Optional[Rect]: - """ - Get monitor dimensions as a rect struct (x, y, right, bottom) - - This property can not be set. - - :return: Rect - """ - raise NotImplementedError - - @property - @abstractmethod - def scale(self) -> Optional[Tuple[float, float]]: - """ - Get scale for the monitor - - Note not all scales will be allowed for all monitors and/or modes - """ - raise NotImplementedError - - @abstractmethod - def setScale(self, scale: Tuple[float, float]): - """ - Change scale for the monitor - - Note not all scales will be allowed for all monitors and/or modes - - :param scale: target percentage as float value - """ - raise NotImplementedError - - @property - @abstractmethod - def dpi(self) -> Optional[Tuple[float, float]]: - """ - Get the dpi (dots/pixels per inch) value for the monitor - - This property can not be set - """ - raise NotImplementedError - - @property - @abstractmethod - def orientation(self) -> Optional[Union[int, Orientation]]: - """ - Get current orientation for the monitor identified by name (or primary if empty) - - The available orientations are: - 0 - 0 degrees (normal) - 1 - 90 degrees (right) - 2 - 180 degrees (inverted) - 3 - 270 degrees (left) - """ - raise NotImplementedError - - @abstractmethod - def setOrientation(self, orientation: Optional[Union[int, Orientation]]): - """ - Change orientation for the monitor identified by name (or primary if empty) - - The available orientations are: - 0 - 0 degrees (normal) - 1 - 90 degrees (right) - 2 - 180 degrees (inverted) - 3 - 270 degrees (left) - - :param orientation: orientation as per Orientations.* - """ - raise NotImplementedError - - @property - @abstractmethod - def frequency(self) -> Optional[float]: - """ - Get current refresh rate of monitor. - - This property can not be set independently. To do so, choose an allowed mode (from monitor.allModes) - and set the monitor mode property (monitor.mode = selectedMode) - """ - raise NotImplementedError - - @property - @abstractmethod - def colordepth(self) -> Optional[int]: - """ - Get the colordepth (bits per pixel to describe color) value for the monitor - - This property can not be set - """ - raise NotImplementedError - - @property - @abstractmethod - def brightness(self) -> Optional[int]: - """ - Get the brightness of monitor. The return value is normalized to 0-100 (as a percentage) - - :return: brightness as float - """ - raise NotImplementedError - - @abstractmethod - def setBrightness(self, brightness: Optional[int]): - """ - Change the brightness of monitor. The input parameter must be defined as a percentage (0-100) - """ - raise NotImplementedError - - @property - @abstractmethod - def contrast(self) -> Optional[int]: - """ - Get the contrast of monitor. The return value is normalized to 0-100 (as a percentage) - - WARNING: In Linux and macOS contrast is calculated from Gamma RGB values. - - :return: contrast as float - """ - raise NotImplementedError - - @abstractmethod - def setContrast(self, contrast: Optional[int]): - """ - Change the contrast of monitor. The input parameter must be defined as a percentage (0-100) - - WARNING: In Linux and macOS the change will apply to Gamma homogeneously for all color components (R, G, B). - - Example for Linux: A value of 50.0 (50%), will result in a Gamma of ''0.5:0.5:0.5'' - """ - raise NotImplementedError - - @property - @abstractmethod - def mode(self) -> Optional[DisplayMode]: - """ - Get the current monitor mode (width, height, refresh-rate) for the monitor - - :return: current mode as DisplayMode struct - """ - raise NotImplementedError - - @abstractmethod - def setMode(self, mode: Optional[DisplayMode]): - """ - Change current monitor mode (resolution and/or refresh-rate) for the monitor - - The mode must be one of the allowed modes by the monitor (see allModes property). - - :param mode: target mode as DisplayMode (width, height and frequency) - """ - raise NotImplementedError - - @property - @abstractmethod - def defaultMode(self) -> Optional[DisplayMode]: - """ - Get the preferred mode for the monitor - - :return: DisplayMode struct (width, height, frequency) - """ - raise NotImplementedError - - @abstractmethod - def setDefaultMode(self): - """ - Change current mode to default / preferred mode - """ - raise NotImplementedError - - @property - @abstractmethod - def allModes(self) -> list[DisplayMode]: - """ - Get all allowed modes for the monitor - - :return: list of DisplayMode (width, height, frequency) - """ - raise NotImplementedError - - @property - def isPrimary(self) -> bool: - """ - Check if given monitor is primary. - - :return: ''True'' if given monitor is primary, ''False'' otherwise - """ - raise NotImplementedError - - @abstractmethod - def setPrimary(self): - """ - Set monitor as the primary one. - - WARNING: Notice this can also change the monitor position, altering the whole monitors setup. - To properly handle this, use arrangeMonitors() instead. - """ - raise NotImplementedError - - @abstractmethod - def turnOn(self): - """ - Turn on or wakeup monitor if it was off or suspended (but not if it is detached). - """ - raise NotImplementedError - - @abstractmethod - def turnOff(self): - """ - Turn off monitor - - WARNING: - - Windows: - If monitor has no VCP MCCS support, it can not be addressed separately, so ALL monitors will be turned off. - To address a specific monitor, try using detach() method - - macOS: - Didn't find a way to programmatically turn off a given monitor. Use suspend instead. - """ - raise NotImplementedError - - @abstractmethod - def suspend(self): - """ - Suspend (standby) monitor - - WARNING: - - Windows: - If monitor has no VCP MCCS support, it can not be addressed separately, so ALL monitors will be suspended. - To address a specific monitor, try using detach() method - - Linux: - This method will suspend ALL monitors. - - macOS: - This method will suspend ALL monitors. - """ - raise NotImplementedError - - @property - @abstractmethod - def isOn(self) -> Optional[bool]: - """ - Check if monitor is on - - WARNING: not working in macOS (... yet?) - """ - raise NotImplementedError - - @abstractmethod - def attach(self): - """ - Attach a previously detached monitor to system - - All monitor IDs will change after detaching or attaching a monitor. The module will try to refresh them for - all existing instances - - WARNING: not working in macOS (... yet?) - """ - raise NotImplementedError - - @abstractmethod - def detach(self, permanent: bool = False): - """ - Detach monitor from system. - - Be aware that if you detach a monitor and the script ends, you will have to physically re-attach the monitor. - - All monitor IDs will change after detaching or attaching a monitor. The module will try to refresh them for - all existing instances - - It will not likely work if system has just one monitor plugged. - - WARNING: not working in macOS (... yet?) - """ - raise NotImplementedError - - @property - @abstractmethod - def isAttached(self) -> Optional[bool]: - """ - Check if monitor is attached (not necessarily ON) to system - """ - raise NotImplementedError - - -_updateRequested = False -_plugListeners: List[Callable[[List[str], dict[str, ScreenValue]], None]] = [] -_lockPlug = threading.RLock() -_changeListeners: List[Callable[[List[str], dict[str, ScreenValue]], None]] = [] -_lockChange = threading.RLock() -_kill = threading.Event() - - -class _UpdateScreens(threading.Thread): - - def __init__(self, kill: threading.Event): - threading.Thread.__init__(self) - - self._kill = kill - self._interval = 0.5 - self._screens: dict[str, ScreenValue] = _getAllMonitorsDict() - self._monitors: list[Monitor] = [] - - def run(self): - - # _eventLoop(self._kill, self._interval) - - global _updateRequested - global _plugListeners - global _changeListeners - - while not self._kill.is_set(): - - if _updateRequested or _plugListeners or _changeListeners: - - screens = _getAllMonitorsDict() - newScreens = list(screens.keys()) - currentScreens = list(self._screens.keys()) - - if currentScreens != newScreens: - names = [s for s in newScreens if s not in currentScreens] + \ - [s for s in currentScreens if s not in newScreens] - for listener in _plugListeners: - listener(names, screens) - - if self._screens != screens: - names = [s for s in newScreens if s in currentScreens and screens[s] != self._screens[s]] - self._screens = screens - if names: - for listener in _changeListeners: - listener(names, screens) - - self._monitors = _getAllMonitors() - - self._kill.wait(self._interval) - - def updateInterval(self, interval: float): - self._interval = interval - - def getScreens(self) -> dict[str, ScreenValue]: - return self._screens - - def getMonitors(self) -> list[Monitor]: - return self._monitors - - -_updateScreens: Optional[_UpdateScreens] = None -_lockUpdate = threading.RLock() - - -def enableUpdateInfo(): - """ - Enable this only if you need to keep track of monitor-related events like changing its resolution, position, - or if monitors can be dynamically plugged or unplugged in a multi-monitor setup. This function can also be - useful in scenarios in which monitors list or properties need to be queried quickly and repeatedly, thus keeping - this information updated without impacting main process. - - If enabled, it will activate a separate thread which will periodically update the list of monitors and - their properties (see getAllMonitors() and getAllMonitorsDict() functions). - - If disabled, the information on the monitors connected to the system will be updated right at the moment, - but this might be slow and CPU-consuming, especially if quickly and repeatedly invoked. - """ - global _updateRequested - _updateRequested = True - if _updateScreens is None: - _startUpdateScreens() - - -def disableUpdateInfo(): - """ - The monitors information will be immediately queried after disabling this feature, not taking advantage of - keeping information updated on a separate thread. - - Enable this process again, or invoke getMonitors() function if you need updated info. - """ - global _updateRequested - _updateRequested = False - if not _plugListeners and not _changeListeners and not _updateRequested: - _killUpdateScreens() - - -def plugListenerRegister(monitorCountChanged: Callable[[List[str], dict[str, ScreenValue]], None]): - """ - Use this only if you need to keep track of monitor that can be dynamically plugged or unplugged in a - multi-monitor setup. - - The registered callbacks will be invoked in case the number of connected monitors change. - The information passed to the callbacks is: - - - Names of the screens which have changed (as a list of strings). - - All screens info, as returned by getAllMonitorsDict() function. - - It is possible to access all monitors information by using screen name as dictionary key - - :param monitorCountChanged: callback to be invoked in case the number of monitor connected changes - """ - global _plugListeners - global _lockPlug - with _lockPlug: - if monitorCountChanged not in _plugListeners: - _plugListeners.append(monitorCountChanged) - if _updateScreens is None: - _startUpdateScreens() - - -def plugListenerUnregister(monitorCountChanged: Callable[[List[str], dict[str, ScreenValue]], None]): - """ - Use this function to un-register your custom callback. The callback will not be invoked anymore in case - the number of monitor changes. - - :param monitorCountChanged: callback previously registered - """ - global _plugListeners - global _lockPlug - with _lockPlug: - try: - objIndex = _plugListeners.index(monitorCountChanged) - _plugListeners.pop(objIndex) - except: - pass - if not _plugListeners and not _changeListeners and not _updateRequested: - _killUpdateScreens() - - -def changeListenerRegister(monitorPropsChanged: Callable[[List[str], dict[str, ScreenValue]], None]): - """ - Use this only if you need to keep track of monitor properties changes (position, size, refresh-rate, etc.) in a - multi-monitor setup. - - The registered callbacks will be invoked in case these properties change. - The information passed to the callbacks is: - - - Names of the screens which have changed (as a list of strings). - - All screens info, as returned by getAllMonitorsDict() function. - - It is possible to access all monitor information by using screen name as dictionary key - - :param monitorPropsChanged: callback to be invoked in case the number of monitor properties change - """ - global _changeListeners - global _lockChange - with _lockChange: - if monitorPropsChanged not in _changeListeners: - _changeListeners.append(monitorPropsChanged) - if _updateScreens is None: - _startUpdateScreens() - - -def changeListenerUnregister(monitorPropsChanged: Callable[[List[str], dict[str, ScreenValue]], None]): - """ - Use this function to un-register your custom callback. The callback will not be invoked anymore in case - the monitor properties change. - - :param monitorPropsChanged: callback previously registered - """ - global _changeListeners - global _lockChange - with _lockChange: - try: - objIndex = _plugListeners.index(monitorPropsChanged) - _plugListeners.pop(objIndex) - except: - pass - if not _plugListeners and not _changeListeners and not _updateRequested: - _killUpdateScreens() - - -def _startUpdateScreens(): - global _updateScreens - global _lockUpdate - with _lockUpdate: - if _updateScreens is None: - _kill.clear() - _updateScreens = _UpdateScreens(_kill) - _updateScreens.daemon = True - _updateScreens.start() - - -def _killUpdateScreens(): - global _updateScreens - global _lockUpdate - global _kill - with _lockUpdate: - if _updateScreens is not None: - timer = threading.Timer(_updateScreens._interval * 2, _timerHandler) - timer.start() - try: - _kill.set() - _updateScreens.join(_updateScreens._interval * 3) - except: - pass - _updateScreens = None - timer.cancel() - - -class _TimeOutException(Exception): - pass - - -def _timerHandler(): - global _updateScreens - raise _TimeOutException() - - -def isWatchdogEnabled() -> bool: - """ - Check if the daemon updating screens information and (if applies) invoking callbacks when needed is alive. - - If it is not, just enable update process, or register the callbacks you need. It will be automatically started. - - :return: Return ''True'' is process (thread) is alive - """ - global _updateScreens - return bool(_updateScreens is not None) - - -def isUpdateInfoEnabled() -> bool: - """ - Get monitors watch process status (enabled / disabled). - - :return: Returns ''True'' if enabled. - """ - global _updateRequested - return _updateRequested - - -def isPlugListenerRegistered(monitorCountChanged: Callable[[List[str], dict[str, ScreenValue]], None]): - """ - Check if callback is already registered to be invoked when monitor plugged count change - - :return: Returns ''True'' if registered - """ - global _plugListeners - return monitorCountChanged in _plugListeners - - -def isChangeListenerRegistered(monitorPropsChanged: Callable[[List[str], dict[str, ScreenValue]], None]): - """ - Check if callback is already registered to be invoked when monitor properties change - - :return: Returns ''True'' if registered - """ - global _changeListeners - return monitorPropsChanged in _changeListeners - - -def updateWatchdogInterval(interval: float): - """ - Change the wait interval for the thread loop in seconds (or fractions), Default is 0.50 seconds. - - Higher values will take longer to detect and notify changes. - - Lower values will make it faster, but will consume more CPU. - - Also bear in mind that the OS will take some time to refresh changes, so lowering the update interval - may not necessarily produce better (faster) results. - - :param interval: new interval value in seconds (or fractions), as float. - """ - global _updateScreens - if interval > 0 and _updateScreens is not None: - _updateScreens.updateInterval(interval) - - -def _getRelativePosition(monitor, relativeTo) -> Tuple[int, int]: - relPos = monitor["relativePos"] - if relPos == Position.PRIMARY: - x = y = 0 - elif relPos == Position.LEFT_TOP: - x = relativeTo["position"].x - monitor["size"].width - y = relativeTo["position"].y - elif relPos == Position.LEFT_BOTTOM: - x = relativeTo["position"].x - monitor["size"].width - y = relativeTo["position"].y + relativeTo["size"].height - monitor["size"].height - elif relPos == Position.ABOVE_LEFT: - x = relativeTo["position"].x - y = relativeTo["position"].y - monitor["size"].height - elif relPos == Position.ABOVE_RIGHT: - x = relativeTo["position"].x + relativeTo["size"].width - monitor["size"].width - y = relativeTo["position"].y - monitor["size"].height - elif relPos == Position.RIGHT_TOP: - x = relativeTo["position"].x + relativeTo["size"].width - y = relativeTo["position"].y - elif relPos == Position.RIGHT_BOTTOM: - x = relativeTo["position"].x + relativeTo["size"].width - y = relativeTo["position"].y + relativeTo["size"].height - monitor["size"].height - elif relPos == Position.BELOW_LEFT: - x = relativeTo["position"].x - y = relativeTo["position"].y + relativeTo["size"].height - elif relPos == Position.BELOW_RIGHT: - x = relativeTo["position"].x + relativeTo["size"].width - monitor["size"].width - y = relativeTo["position"].y + relativeTo["size"].height - else: - x = y = monitor["position"] - return x, y - - -if sys.platform == "darwin": - from ._pymonctl_macos import (_getAllMonitors, _getAllMonitorsDict, _getMonitorsCount, _getPrimary, - _findMonitor, _arrangeMonitors, _getMousePos, MacOSMonitor as Monitor - ) -elif sys.platform == "win32": - from ._pymonctl_win import (_getAllMonitors, _getAllMonitorsDict, _getMonitorsCount, _getPrimary, - _findMonitor, _arrangeMonitors, _getMousePos, Win32Monitor as Monitor - ) -elif sys.platform == "linux": - from ._pymonctl_linux import (_getAllMonitors, _getAllMonitorsDict, _getMonitorsCount, _getPrimary, - _findMonitor, _arrangeMonitors, _getMousePos, LinuxMonitor as Monitor - ) -else: - raise NotImplementedError('PyMonCtl currently does not support this platform. If you think you can help, please contribute! https://github.com/Kalmat/PyMonCtl') +from ._main import (getAllMonitors, getAllMonitorsDict, getMonitorsCount, getPrimary, findMonitor, findMonitorInfo, + arrangeMonitors, getMousePos, Monitor, + enableUpdateInfo, disableUpdateInfo, isUpdateInfoEnabled, isWatchdogEnabled, updateWatchdogInterval, + plugListenerRegister, plugListenerUnregister, isPlugListenerRegistered, + changeListenerRegister, changeListenerUnregister, isChangeListenerRegistered, + DisplayMode, ScreenValue, Size, Point, Box, Rect, Position, Orientation + ) diff --git a/src/pymonctl/_main.py b/src/pymonctl/_main.py new file mode 100644 index 0000000..1ddbca0 --- /dev/null +++ b/src/pymonctl/_main.py @@ -0,0 +1,853 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import annotations + +import sys +import threading +from abc import abstractmethod, ABC +from collections.abc import Callable +from typing import List, Optional, Union, Tuple + +from pymonctl._structs import DisplayMode, ScreenValue, Size, Point, Box, Rect, Position, Orientation + + +def _pointInBox(x: int, y: int, left: int, top: int, width: int, height: int) -> bool: + """Returns ``True`` if the ``(x, y)`` point is within the box described + by ``(left, top, width, height)``.""" + return left <= x <= left + width and top <= y <= top + height + + +def getAllMonitors() -> list[Monitor]: + """ + Get the list with all Monitor instances from plugged monitors. + + In case you plan to use this function in a scenario in which it could be invoked quickly and repeatedly, + it's highly recommended to enable update watchdog (see enableUpdate() function). + + :return: list of Monitor instances + """ + global _updateScreens + if _updateScreens is None: + return _getAllMonitors() + else: + return _updateScreens.getMonitors() + + +def getAllMonitorsDict() -> dict[str, ScreenValue]: + """ + Get all monitors info plugged to the system, as a dict. + + In case you plan to use this function in a scenario in which it could be invoked quickly and repeatedly, + it's highly recommended to enable update watchdog (see enableUpdate() function). + + :return: Monitors info as python dictionary + + Output Format: + Key: + Display name (in macOS it is necessary to add handle to avoid duplicates) + + Values: + "system_name": + display name as returned by the system (in macOS, the name can be duplicated!) + "handle": + display handle according to each platform/OS + "is_primary": + ''True'' if monitor is primary (shows clock and notification area, sign in, lock, CTRL+ALT+DELETE screens...) + "position": + Point(x, y) struct containing the display position ((0, 0) for the primary screen) + "size": + Size(width, height) struct containing the display size, in pixels + "workarea": + Rect(left, top, right, bottom) struct with the screen workarea, in pixels + "scale": + Scale ratio, as a tuple of (x, y) scale percentage + "dpi": + Dots per inch, as a tuple of (x, y) dpi values + "orientation": + Display orientation: 0 - Landscape / 1 - Portrait / 2 - Landscape (reversed) / 3 - Portrait (reversed) + "frequency": + Refresh rate of the display, in Hz + "colordepth": + Bits per pixel referred to the display color depth + """ + global _updateScreens + if _updateScreens is None: + return _getAllMonitorsDict() + else: + return _updateScreens.getScreens() + + +def getMonitorsCount() -> int: + """ + Get the number of monitors currently connected to the system. + + :return: number of monitors as integer + """ + return _getMonitorsCount() + + +def getPrimary() -> Monitor: + """ + Get primary monitor instance. This is equivalent to invoking ''Monitor()'', with empty input params. + + :return: Monitor instance or None + """ + return _getPrimary() + + +def findMonitor(x: int, y: int) -> Optional[List[Monitor]]: + """ + Get monitor instance in which given coordinates (x, y) are found. + + :return: Monitor instance or None + """ + return _findMonitor(x, y) + + +def findMonitorInfo(x: int, y: int) -> dict[str, ScreenValue]: + """ + Get monitor info in which given coordinates (x, y) are found. + + :return: monitor info (see getAllMonitorsDict() doc) as dictionary, or empty + """ + info: dict[str, ScreenValue] = {} + monitors = getAllMonitorsDict() + for monitor in monitors.keys(): + pos = monitors[monitor]["position"] + size = monitors[monitor]["size"] + if _pointInBox(x, y, pos.x, pos.y, size.width, size.height): + info[monitor] = monitors[monitor] + return info + + +def arrangeMonitors(arrangement: dict[str, dict[str, Union[str, int, Position, Point, Size]]]): + """ + Arrange all monitors in a given shape. + + For that, you must pass a dict with the following structure: + "Monitor name": + monitor name as keys() returned by getAllMonitorsDict() (don't use "system_name" value for this) + "relativePos": + position of this monitor in relation to the monitor provided in ''relativeTo'' + "relativeTo": + monitor name to which ''relativePos'' is referred to (or None if PRIMARY) + + + You MUST pass the position of ALL monitors, and SET ONE of them as PRIMARY. + + HIGHLY RECOMMENDED: When building complex arrangements, start by the primary monitor and then build the rest + taking previous ones as references. + + EXAMPLE for a 3-Monitors setup in which second is at the left and third is on top of primary monitor: + + { + "Display_1": {"relativePos": Position.PRIMARY, "relativeTo": None}, + + "Display_2": {"relativePos": Position.LEFT_TOP, "relativeTo": "Display_1"}, + + "Display_3": {"relativePos": Position.ABOVE_LEFT, "relativeTo": "Display_1"} + } + + :param arrangement: arrangement structure as dict + """ + _arrangeMonitors(arrangement) + + +def getMousePos() -> Point: + """ + Get the current (x, y) coordinates of the mouse pointer on screen, in pixels + + :return: Point struct + """ + return _getMousePos() + + +class BaseMonitor(ABC): + + @property + @abstractmethod + def size(self) -> Optional[Size]: + """ + Get the dimensions of the monitor as a size struct (width, height) + + This property can not be set independently. To do so, choose an allowed mode (from monitor.allModes) + and set the monitor mode property (monitor.mode = selectedMode) + + :return: Size + """ + raise NotImplementedError + + @property + @abstractmethod + def workarea(self) -> Optional[Rect]: + """ + Get dimensions of the "usable by applications" area (screen size minus docks, taskbars and so on), as + a rect struct (x, y, right, bottom) + + This property can not be set. + + :return: Rect + """ + raise NotImplementedError + + @property + @abstractmethod + def position(self) -> Optional[Point]: + """ + Get monitor position coordinates as a point struct (x, y) + + This property can not be set. Use setPosition() method instead. + + :return: Point + """ + raise NotImplementedError + + @abstractmethod + def setPosition(self, relativePos: Union[int, Position], relativeTo: Optional[str]): + """ + Change relative position of the current the monitor in relation to another existing monitor (e.g. primary monitor). + + In general, it is HIGHLY recommendable to use arrangeMonitors() method instead of setPosition(), and most + especially in complex arrangements or setups with more than 2 monitors. + + Important issues: + + - On Windows, primary monitor is mandatory, and it is always placed at (0, 0) coordinates. Besides, the monitors can not overlap. In case the monitor you want to reposition is the primary or the unique one, it will have no effect. To do so, you must switch the primary monitor first, then reposition it. + + - On Linux, primary monitor can be anywhere, monitors can overlap and even there can be no primary monitor + + - On macOS, primary monitor is mandatory, and it is always placed at (0, 0) coordinates. You will likely have to reposition primary monitor before setting a different monitor as primary. Monitors can overlap. Further tests in multi-monitor setups are still required to confirm these behaviors and produce a final version + + :param relativePos: position in relation to another existing monitor (e.g. primary) as per Positions.* + :param relativeTo: monitor in relation to which this monitor must be placed + """ + raise NotImplementedError + + @property + @abstractmethod + def box(self) -> Optional[Box]: + """ + Get monitor dimensions as a box struct (x, y, width, height) + + This property can not be set. + + :return: Box + """ + raise NotImplementedError + + @property + @abstractmethod + def rect(self) -> Optional[Rect]: + """ + Get monitor dimensions as a rect struct (x, y, right, bottom) + + This property can not be set. + + :return: Rect + """ + raise NotImplementedError + + @property + @abstractmethod + def scale(self) -> Optional[Tuple[float, float]]: + """ + Get scale for the monitor + + Note not all scales will be allowed for all monitors and/or modes + """ + raise NotImplementedError + + @abstractmethod + def setScale(self, scale: Tuple[float, float]): + """ + Change scale for the monitor + + Note not all scales will be allowed for all monitors and/or modes + + :param scale: target percentage as float value + """ + raise NotImplementedError + + @property + @abstractmethod + def dpi(self) -> Optional[Tuple[float, float]]: + """ + Get the dpi (dots/pixels per inch) value for the monitor + + This property can not be set + """ + raise NotImplementedError + + @property + @abstractmethod + def orientation(self) -> Optional[Union[int, Orientation]]: + """ + Get current orientation for the monitor identified by name (or primary if empty) + + The available orientations are: + 0 - 0 degrees (normal) + 1 - 90 degrees (right) + 2 - 180 degrees (inverted) + 3 - 270 degrees (left) + """ + raise NotImplementedError + + @abstractmethod + def setOrientation(self, orientation: Optional[Union[int, Orientation]]): + """ + Change orientation for the monitor identified by name (or primary if empty) + + The available orientations are: + 0 - 0 degrees (normal) + 1 - 90 degrees (right) + 2 - 180 degrees (inverted) + 3 - 270 degrees (left) + + :param orientation: orientation as per Orientations.* + """ + raise NotImplementedError + + @property + @abstractmethod + def frequency(self) -> Optional[float]: + """ + Get current refresh rate of monitor. + + This property can not be set independently. To do so, choose an allowed mode (from monitor.allModes) + and set the monitor mode property (monitor.mode = selectedMode) + """ + raise NotImplementedError + + @property + @abstractmethod + def colordepth(self) -> Optional[int]: + """ + Get the colordepth (bits per pixel to describe color) value for the monitor + + This property can not be set + """ + raise NotImplementedError + + @property + @abstractmethod + def brightness(self) -> Optional[int]: + """ + Get the brightness of monitor. The return value is normalized to 0-100 (as a percentage) + + :return: brightness as float + """ + raise NotImplementedError + + @abstractmethod + def setBrightness(self, brightness: Optional[int]): + """ + Change the brightness of monitor. The input parameter must be defined as a percentage (0-100) + """ + raise NotImplementedError + + @property + @abstractmethod + def contrast(self) -> Optional[int]: + """ + Get the contrast of monitor. The return value is normalized to 0-100 (as a percentage) + + WARNING: In Linux and macOS contrast is calculated from Gamma RGB values. + + :return: contrast as float + """ + raise NotImplementedError + + @abstractmethod + def setContrast(self, contrast: Optional[int]): + """ + Change the contrast of monitor. The input parameter must be defined as a percentage (0-100) + + WARNING: In Linux and macOS the change will apply to Gamma homogeneously for all color components (R, G, B). + + Example for Linux: A value of 50.0 (50%), will result in a Gamma of ''0.5:0.5:0.5'' + """ + raise NotImplementedError + + @property + @abstractmethod + def mode(self) -> Optional[DisplayMode]: + """ + Get the current monitor mode (width, height, refresh-rate) for the monitor + + :return: current mode as DisplayMode struct + """ + raise NotImplementedError + + @abstractmethod + def setMode(self, mode: Optional[DisplayMode]): + """ + Change current monitor mode (resolution and/or refresh-rate) for the monitor + + The mode must be one of the allowed modes by the monitor (see allModes property). + + :param mode: target mode as DisplayMode (width, height and frequency) + """ + raise NotImplementedError + + @property + @abstractmethod + def defaultMode(self) -> Optional[DisplayMode]: + """ + Get the preferred mode for the monitor + + :return: DisplayMode struct (width, height, frequency) + """ + raise NotImplementedError + + @abstractmethod + def setDefaultMode(self): + """ + Change current mode to default / preferred mode + """ + raise NotImplementedError + + @property + @abstractmethod + def allModes(self) -> list[DisplayMode]: + """ + Get all allowed modes for the monitor + + :return: list of DisplayMode (width, height, frequency) + """ + raise NotImplementedError + + @property + def isPrimary(self) -> bool: + """ + Check if given monitor is primary. + + :return: ''True'' if given monitor is primary, ''False'' otherwise + """ + raise NotImplementedError + + @abstractmethod + def setPrimary(self): + """ + Set monitor as the primary one. + + WARNING: Notice this can also change the monitor position, altering the whole monitors setup. + To properly handle this, use arrangeMonitors() instead. + """ + raise NotImplementedError + + @abstractmethod + def turnOn(self): + """ + Turn on or wakeup monitor if it was off or suspended (but not if it is detached). + """ + raise NotImplementedError + + @abstractmethod + def turnOff(self): + """ + Turn off monitor + + WARNING: + + Windows: + If monitor has no VCP MCCS support, it can not be addressed separately, so ALL monitors will be turned off. + To address a specific monitor, try using detach() method + + macOS: + Didn't find a way to programmatically turn off a given monitor. Use suspend instead. + """ + raise NotImplementedError + + @abstractmethod + def suspend(self): + """ + Suspend (standby) monitor + + WARNING: + + Windows: + If monitor has no VCP MCCS support, it can not be addressed separately, so ALL monitors will be suspended. + To address a specific monitor, try using detach() method + + Linux: + This method will suspend ALL monitors. + + macOS: + This method will suspend ALL monitors. + """ + raise NotImplementedError + + @property + @abstractmethod + def isOn(self) -> Optional[bool]: + """ + Check if monitor is on + + WARNING: not working in macOS (... yet?) + """ + raise NotImplementedError + + @abstractmethod + def attach(self): + """ + Attach a previously detached monitor to system + + All monitor IDs will change after detaching or attaching a monitor. The module will try to refresh them for + all existing instances + + WARNING: not working in macOS (... yet?) + """ + raise NotImplementedError + + @abstractmethod + def detach(self, permanent: bool = False): + """ + Detach monitor from system. + + Be aware that if you detach a monitor and the script ends, you will have to physically re-attach the monitor. + + All monitor IDs will change after detaching or attaching a monitor. The module will try to refresh them for + all existing instances + + It will not likely work if system has just one monitor plugged. + + WARNING: not working in macOS (... yet?) + """ + raise NotImplementedError + + @property + @abstractmethod + def isAttached(self) -> Optional[bool]: + """ + Check if monitor is attached (not necessarily ON) to system + """ + raise NotImplementedError + + +_updateRequested = False +_plugListeners: List[Callable[[List[str], dict[str, ScreenValue]], None]] = [] +_lockPlug = threading.RLock() +_changeListeners: List[Callable[[List[str], dict[str, ScreenValue]], None]] = [] +_lockChange = threading.RLock() +_kill = threading.Event() + + +class _UpdateScreens(threading.Thread): + + def __init__(self, kill: threading.Event): + threading.Thread.__init__(self) + + self._kill = kill + self._interval = 0.5 + self._screens: dict[str, ScreenValue] = _getAllMonitorsDict() + self._monitors: list[Monitor] = [] + + def run(self): + + # _eventLoop(self._kill, self._interval) + + global _updateRequested + global _plugListeners + global _changeListeners + + while not self._kill.is_set(): + + if _updateRequested or _plugListeners or _changeListeners: + + screens = _getAllMonitorsDict() + newScreens = list(screens.keys()) + currentScreens = list(self._screens.keys()) + + if currentScreens != newScreens: + names = [s for s in newScreens if s not in currentScreens] + \ + [s for s in currentScreens if s not in newScreens] + for listener in _plugListeners: + listener(names, screens) + + if self._screens != screens: + names = [s for s in newScreens if s in currentScreens and screens[s] != self._screens[s]] + self._screens = screens + if names: + for listener in _changeListeners: + listener(names, screens) + + self._monitors = _getAllMonitors() + + self._kill.wait(self._interval) + + def updateInterval(self, interval: float): + self._interval = interval + + def getScreens(self) -> dict[str, ScreenValue]: + return self._screens + + def getMonitors(self) -> list[Monitor]: + return self._monitors + + +_updateScreens: Optional[_UpdateScreens] = None +_lockUpdate = threading.RLock() + + +def enableUpdateInfo(): + """ + Enable this only if you need to keep track of monitor-related events like changing its resolution, position, + or if monitors can be dynamically plugged or unplugged in a multi-monitor setup. This function can also be + useful in scenarios in which monitors list or properties need to be queried quickly and repeatedly, thus keeping + this information updated without impacting main process. + + If enabled, it will activate a separate thread which will periodically update the list of monitors and + their properties (see getAllMonitors() and getAllMonitorsDict() functions). + + If disabled, the information on the monitors connected to the system will be updated right at the moment, + but this might be slow and CPU-consuming, especially if quickly and repeatedly invoked. + """ + global _updateRequested + _updateRequested = True + if _updateScreens is None: + _startUpdateScreens() + + +def disableUpdateInfo(): + """ + The monitors information will be immediately queried after disabling this feature, not taking advantage of + keeping information updated on a separate thread. + + Enable this process again, or invoke getMonitors() function if you need updated info. + """ + global _updateRequested + _updateRequested = False + if not _plugListeners and not _changeListeners and not _updateRequested: + _killUpdateScreens() + + +def plugListenerRegister(monitorCountChanged: Callable[[List[str], dict[str, ScreenValue]], None]): + """ + Use this only if you need to keep track of monitor that can be dynamically plugged or unplugged in a + multi-monitor setup. + + The registered callbacks will be invoked in case the number of connected monitors change. + The information passed to the callbacks is: + + - Names of the screens which have changed (as a list of strings). + - All screens info, as returned by getAllMonitorsDict() function. + + It is possible to access all monitors information by using screen name as dictionary key + + :param monitorCountChanged: callback to be invoked in case the number of monitor connected changes + """ + global _plugListeners + global _lockPlug + with _lockPlug: + if monitorCountChanged not in _plugListeners: + _plugListeners.append(monitorCountChanged) + if _updateScreens is None: + _startUpdateScreens() + + +def plugListenerUnregister(monitorCountChanged: Callable[[List[str], dict[str, ScreenValue]], None]): + """ + Use this function to un-register your custom callback. The callback will not be invoked anymore in case + the number of monitor changes. + + :param monitorCountChanged: callback previously registered + """ + global _plugListeners + global _lockPlug + with _lockPlug: + try: + objIndex = _plugListeners.index(monitorCountChanged) + _plugListeners.pop(objIndex) + except: + pass + if not _plugListeners and not _changeListeners and not _updateRequested: + _killUpdateScreens() + + +def changeListenerRegister(monitorPropsChanged: Callable[[List[str], dict[str, ScreenValue]], None]): + """ + Use this only if you need to keep track of monitor properties changes (position, size, refresh-rate, etc.) in a + multi-monitor setup. + + The registered callbacks will be invoked in case these properties change. + The information passed to the callbacks is: + + - Names of the screens which have changed (as a list of strings). + - All screens info, as returned by getAllMonitorsDict() function. + + It is possible to access all monitor information by using screen name as dictionary key + + :param monitorPropsChanged: callback to be invoked in case the number of monitor properties change + """ + global _changeListeners + global _lockChange + with _lockChange: + if monitorPropsChanged not in _changeListeners: + _changeListeners.append(monitorPropsChanged) + if _updateScreens is None: + _startUpdateScreens() + + +def changeListenerUnregister(monitorPropsChanged: Callable[[List[str], dict[str, ScreenValue]], None]): + """ + Use this function to un-register your custom callback. The callback will not be invoked anymore in case + the monitor properties change. + + :param monitorPropsChanged: callback previously registered + """ + global _changeListeners + global _lockChange + with _lockChange: + try: + objIndex = _plugListeners.index(monitorPropsChanged) + _plugListeners.pop(objIndex) + except: + pass + if not _plugListeners and not _changeListeners and not _updateRequested: + _killUpdateScreens() + + +def _startUpdateScreens(): + global _updateScreens + global _lockUpdate + with _lockUpdate: + if _updateScreens is None: + _kill.clear() + _updateScreens = _UpdateScreens(_kill) + _updateScreens.daemon = True + _updateScreens.start() + + +def _killUpdateScreens(): + global _updateScreens + global _lockUpdate + global _kill + with _lockUpdate: + if _updateScreens is not None: + timer = threading.Timer(_updateScreens._interval * 2, _timerHandler) + timer.start() + try: + _kill.set() + _updateScreens.join(_updateScreens._interval * 3) + except: + pass + _updateScreens = None + timer.cancel() + + +class _TimeOutException(Exception): + pass + + +def _timerHandler(): + global _updateScreens + raise _TimeOutException() + + +def isWatchdogEnabled() -> bool: + """ + Check if the daemon updating screens information and (if applies) invoking callbacks when needed is alive. + + If it is not, just enable update process, or register the callbacks you need. It will be automatically started. + + :return: Return ''True'' is process (thread) is alive + """ + global _updateScreens + return bool(_updateScreens is not None) + + +def isUpdateInfoEnabled() -> bool: + """ + Get monitors watch process status (enabled / disabled). + + :return: Returns ''True'' if enabled. + """ + global _updateRequested + return _updateRequested + + +def isPlugListenerRegistered(monitorCountChanged: Callable[[List[str], dict[str, ScreenValue]], None]): + """ + Check if callback is already registered to be invoked when monitor plugged count change + + :return: Returns ''True'' if registered + """ + global _plugListeners + return monitorCountChanged in _plugListeners + + +def isChangeListenerRegistered(monitorPropsChanged: Callable[[List[str], dict[str, ScreenValue]], None]): + """ + Check if callback is already registered to be invoked when monitor properties change + + :return: Returns ''True'' if registered + """ + global _changeListeners + return monitorPropsChanged in _changeListeners + + +def updateWatchdogInterval(interval: float): + """ + Change the wait interval for the thread loop in seconds (or fractions), Default is 0.50 seconds. + + Higher values will take longer to detect and notify changes. + + Lower values will make it faster, but will consume more CPU. + + Also bear in mind that the OS will take some time to refresh changes, so lowering the update interval + may not necessarily produce better (faster) results. + + :param interval: new interval value in seconds (or fractions), as float. + """ + global _updateScreens + if interval > 0 and _updateScreens is not None: + _updateScreens.updateInterval(interval) + + +def _getRelativePosition(monitor, relativeTo) -> Tuple[int, int]: + relPos = monitor["relativePos"] + if relPos == Position.PRIMARY: + x = y = 0 + elif relPos == Position.LEFT_TOP: + x = relativeTo["position"].x - monitor["size"].width + y = relativeTo["position"].y + elif relPos == Position.LEFT_BOTTOM: + x = relativeTo["position"].x - monitor["size"].width + y = relativeTo["position"].y + relativeTo["size"].height - monitor["size"].height + elif relPos == Position.ABOVE_LEFT: + x = relativeTo["position"].x + y = relativeTo["position"].y - monitor["size"].height + elif relPos == Position.ABOVE_RIGHT: + x = relativeTo["position"].x + relativeTo["size"].width - monitor["size"].width + y = relativeTo["position"].y - monitor["size"].height + elif relPos == Position.RIGHT_TOP: + x = relativeTo["position"].x + relativeTo["size"].width + y = relativeTo["position"].y + elif relPos == Position.RIGHT_BOTTOM: + x = relativeTo["position"].x + relativeTo["size"].width + y = relativeTo["position"].y + relativeTo["size"].height - monitor["size"].height + elif relPos == Position.BELOW_LEFT: + x = relativeTo["position"].x + y = relativeTo["position"].y + relativeTo["size"].height + elif relPos == Position.BELOW_RIGHT: + x = relativeTo["position"].x + relativeTo["size"].width - monitor["size"].width + y = relativeTo["position"].y + relativeTo["size"].height + else: + x = y = monitor["position"] + return x, y + + +if sys.platform == "darwin": + from ._pymonctl_macos import (_getAllMonitors, _getAllMonitorsDict, _getMonitorsCount, _getPrimary, + _findMonitor, _arrangeMonitors, _getMousePos, MacOSMonitor as Monitor + ) +elif sys.platform == "win32": + from ._pymonctl_win import (_getAllMonitors, _getAllMonitorsDict, _getMonitorsCount, _getPrimary, + _findMonitor, _arrangeMonitors, _getMousePos, Win32Monitor as Monitor + ) +elif sys.platform == "linux": + from ._pymonctl_linux import (_getAllMonitors, _getAllMonitorsDict, _getMonitorsCount, _getPrimary, + _findMonitor, _arrangeMonitors, _getMousePos, LinuxMonitor as Monitor + ) +else: + raise NotImplementedError('PyMonCtl currently does not support this platform. If you think you can help, please contribute! https://github.com/Kalmat/PyMonCtl') diff --git a/src/pymonctl/_pymonctl_linux.py b/src/pymonctl/_pymonctl_linux.py index 9d86778..cd2882d 100644 --- a/src/pymonctl/_pymonctl_linux.py +++ b/src/pymonctl/_pymonctl_linux.py @@ -8,7 +8,6 @@ import subprocess import threading -import time import math from typing import Optional, List, Union, cast, Tuple @@ -19,12 +18,10 @@ import Xlib.xobject from Xlib.ext import randr -from pymonctl import BaseMonitor, _pointInBox, _getRelativePosition, \ - DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation +from ._main import BaseMonitor, _pointInBox, _getRelativePosition, \ + DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation from ewmhlib import displaysCount, getDisplaysNames, defaultDisplay, defaultRoot, defaultScreen, defaultRootWindow, \ - getProperty, getPropertyValue -from ewmhlib.Props import Root - + getProperty, getPropertyValue, Props # Check if randr extension is available if not defaultRootWindow.display.has_extension('RANDR'): @@ -76,10 +73,9 @@ def _XgetRoots(): _roots = _XgetRoots() -def _getAllMonitors(outputs=None) -> list[LinuxMonitor]: +def _getAllMonitors() -> list[LinuxMonitor]: monitors = [] - if not outputs: - outputs = _XgetAllOutputs() + outputs = _XgetAllOutputs() for outputData in outputs: display, screen, root, res, output, outputInfo = outputData if outputInfo.crtc: @@ -105,7 +101,7 @@ def _getAllMonitorsDict() -> dict[str, ScreenValue]: is_primary = monitor.primary == 1 x, y, w, h = monitor.x, monitor.y, monitor.width_in_pixels, monitor.height_in_pixels # https://askubuntu.com/questions/1124149/how-to-get-taskbar-size-and-position-with-python - wa: List[int] = getPropertyValue(getProperty(window=root, prop=Root.WORKAREA, display=display), display=display) + wa: List[int] = getPropertyValue(getProperty(window=root, prop=Props.Root.WORKAREA, display=display), display=display) wx, wy, wr, wb = wa[0], wa[1], wa[2], wa[3] dpiX, dpiY = round((w * 25.4) / (monitor.width_in_millimeters or 1)), round((h * 25.4) / (monitor.height_in_millimeters or 1)) scaleX, scaleY = _scale(monitorName) or (0.0, 0.0) @@ -206,11 +202,14 @@ def _arrangeMonitors(arrangement: dict[str, dict[str, Union[str, int, Position, if y < 0: yOffset += abs(y) newPos[monName] = {"x": x, "y": y} + w, h = targetMonInfo.width_in_pixels, targetMonInfo.height_in_pixels newArrangement[monName] = { "setPrimary": relativePos == Position.PRIMARY, "x": x, - "y": y + "y": y, + "w": w, + "h": h } if newArrangement: @@ -262,7 +261,7 @@ def workarea(self) -> Optional[Rect]: res: Optional[Rect] = None # https://askubuntu.com/questions/1124149/how-to-get-taskbar-size-and-position-with-python wa: List[int] = getPropertyValue( - getProperty(window=self.root, prop=Root.WORKAREA, display=self.display), display=self.display) + getProperty(window=self.root, prop=Props.Root.WORKAREA, display=self.display), display=self.display) if wa: wx, wy, wr, wb = wa[0], wa[1], wa[2], wa[3] res = Rect(wx, wy, wr, wb) @@ -303,15 +302,18 @@ def scale(self) -> Optional[Tuple[float, float]]: return _scale(self.name) def setScale(self, scale: Optional[Tuple[float, float]]): - if scale is not None and self.name and self.name in _XgetAllMonitorsNames(): + if scale is not None: # https://askubuntu.com/questions/1193940/setting-monitor-scaling-to-200-with-xrandr - scaleX, scaleY = round(100/ scale[0], 1), round(100 / scale[1], 1) - # cmd = "xrandr --output %s --scale %sx%s --filter nearest" % (self.name, scaleX, scaleY) - cmd = "xrandr --output %s --scale %sx%s" % (self.name, scaleX, scaleY) - try: - subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) - except: - pass + scaleX, scaleY = round(100 / scale[0], 1), round(100 / scale[1], 1) + if 0 < scaleX <= 1 and 0 < scaleY <= 1: + cmd = "xrandr --output %s --scale %sx%s --filter nearest" % (self.name, scaleX, scaleY) + try: + ret = subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) + if ret and hasattr(ret, "returncode") and ret.returncode != 0: + cmd = "xrandr --output %s --scale %sx%s" % (self.name, scaleX, scaleY) + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) + except: + pass @property def dpi(self) -> Optional[Tuple[float, float]]: @@ -464,42 +466,11 @@ def setMode(self, mode: Optional[DisplayMode]): # Xlib.ext.randr.set_screen_config(defaultRootWindow.root, size_id, 0, 0, round(mode.frequency), 0) # Xlib.ext.randr.change_output_property() if mode is not None: - # allModes = self.allModes - # if mode in allModes: - cmd = " --mode %sx%s -r %s" % (mode.width, mode.height, round(mode.frequency, 2)) - if self.name: - cmd = (" --output %s" % self.name) + cmd - cmd = "xrandr" + cmd - i = 0 - while mode != self.mode and i <= 3: - try: - subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) - except: - pass - i += 1 - time.sleep(0.3) - - def _modeB(self) -> Optional[DisplayMode]: - - outMode: Optional[DisplayMode] = None - allModes = [] - mode = None - - for crtc in _XgetAllCrtcs(self.name): - res = crtc[3] - crtcInfo = crtc[7] - if crtcInfo.mode: - mode = crtcInfo.mode - allModes = res.modes - break - - if mode and allModes: - for m in allModes: - if mode == m.id: - outMode = DisplayMode(m.width, m.height, - round(m.dot_clock / ((m.h_total * m.v_total) or 1), 2)) - break - return outMode + cmd = "xrandr --output %s --mode %sx%s -r %s" % (self.name, mode.width, mode.height, round(mode.frequency, 2)) + try: + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) + except: + pass @property def defaultMode(self) -> Optional[DisplayMode]: @@ -521,20 +492,24 @@ def defaultMode(self) -> Optional[DisplayMode]: def setDefaultMode(self): cmd = "xrandr --output %s --auto" % self.name - _, _ = subprocess.getstatusoutput(cmd) + try: + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) + except: + pass @property def allModes(self) -> list[DisplayMode]: modes: List[DisplayMode] = [] - allModes = [] - for crtcData in _XgetAllCrtcs(self.name): - display, screen, root, res, output, outputInfo, crtc, crtcInfo = crtcData - if crtcInfo.mode: - allModes = res.modes + for outputData in _XgetAllOutputs(self.name): + display, screen, root, res, output, outputInfo = outputData + if self.handle == output: + for outMode in outputInfo.modes: + for resMode in res.modes: + if outMode == resMode.id: + modes.append(DisplayMode(resMode.width, resMode.height, + round(resMode.dot_clock / ((resMode.h_total * resMode.v_total) or 1), 2))) + break - for mode in allModes: - modes.append(DisplayMode(mode.width, mode.height, - round(mode.dot_clock / ((mode.h_total * mode.v_total) or 1), 2))) return modes @property @@ -555,14 +530,10 @@ def turnOn(self): cmdPart = " --right-of %s" % monName break cmd = ("xrandr --output %s" % self.name) + cmdPart + " --auto" - i = 0 - while i <= 3 and not self.isOn: - try: - subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) - except: - pass - i += 1 - time.sleep(0.3) + try: + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, timeout=1) + except: + pass else: cmd = "xset -q | grep ' Monitor is ' | awk '{ print$4 }'" try: @@ -591,7 +562,15 @@ def suspend(self): @property def isOn(self) -> bool: - return self.name in _XgetAllMonitorsNames() + # https://stackoverflow.com/questions/3433203/how-to-determine-if-lcd-monitor-is-turned-on-from-linux-command-line + cmd = "xrandr --listactivemonitors" + try: + err, ret = subprocess.getstatusoutput(cmd) + if err == 0: + return self.name in ret + except: + pass + return False def attach(self): # This produces the same effect, but requires to keep track of last mode used @@ -679,11 +658,12 @@ def _setPosition(relativePos: Position, relativeTo: Optional[str], name: str): w, h = targetMonInfo.width_in_pixels, targetMonInfo.height_in_pixels setPrimary = targetMonInfo.primary == 1 - newPos[monitor] = {"x": x, "y": y} + newPos[monitor] = {"x": x, "y": y, "w": w, "h": h} else: monInfo = monitors[monitor]["monitor"] x, y = monInfo.x, monInfo.y + w, h = monInfo.width_in_pixels, monInfo.height_in_pixels setPrimary = monInfo.primary == 1 if x < 0: xOffset += abs(x) @@ -693,7 +673,9 @@ def _setPosition(relativePos: Position, relativeTo: Optional[str], name: str): arrangement[monitor] = { "setPrimary": setPrimary, "x": x, - "y": y + "y": y, + "w": w, + "h": h } if arrangement: @@ -712,9 +694,9 @@ def _buildCommand(arrangement: dict[str, dict[str, Union[int, bool]]], xOffset: # xrandr won't accept negative values!!!! # https://superuser.com/questions/485120/how-do-i-align-the-bottom-edges-of-two-monitors-with-xrandr cmd += " --pos %sx%s" % (str(int(arrInfo["x"]) + xOffset), str(int(arrInfo["y"]) + yOffset)) + cmd += " --mode %sx%s" % (arrInfo["w"], arrInfo["h"]) if arrInfo["setPrimary"]: cmd += " --primary" - print(cmd) return cmd @@ -748,44 +730,20 @@ def _scale(name: str = "") -> Optional[Tuple[float, float]]: return None -_outputs = [] -_lockOutputs = threading.RLock() -_monitors = [] # type: ignore[var-annotated] -_lockMonitors = threading.RLock() - - def _XgetAllOutputs(name: str = ""): - global _outputs - global _lockOutputs - global _monitors - global _lockMonitors - newMonitors = _XgetAllMonitors() - if _monitors != newMonitors: - with _lockMonitors: - _monitors = newMonitors - outputs = [] - global _roots - for rootData in _roots: - display, screen, root, res = rootData - if res: - for output in res.outputs: - try: - outputInfo = randr.get_output_info(display, output, res.config_timestamp) + outputs = [] + global _roots + for rootData in _roots: + display, screen, root, res = rootData + if res: + for output in res.outputs: + try: + outputInfo = randr.get_output_info(display, output, res.config_timestamp) + if not name or (name and name == outputInfo.name): outputs.append([display, screen, root, res, output, outputInfo]) - except: - pass - with _lockOutputs: - _outputs = outputs - if name: - ret = [] - for outputData in _outputs: - display, screen, root, res, output, outputInfo = outputData - if name == outputInfo.name: - ret.append(outputData) - break - return ret - else: - return _outputs + except: + pass + return outputs def _XgetAllCrtcs(name: str = ""): @@ -853,18 +811,17 @@ def _XgetPrimary(): def _XgetMonitorData(handle: Optional[int] = None): + outputs = _XgetAllOutputs() if handle: - outputs = _XgetAllOutputs() for outputData in outputs: display, screen, root, res, output, outputInfo = outputData if output == handle: return display, screen, root, res, output, outputInfo.name else: - outputs = _XgetAllOutputs() - global _monitors - for monitorData in _monitors: + monitors = _XgetAllMonitors() + for monitorData in monitors: display, root, monitor, monName = monitorData - if monitor.primary == 1 or len(_monitors) == 1: + if monitor.primary == 1 or len(monitors) == 1: for outputData in outputs: display, screen, root, res, output, outputInfo = outputData if monName == outputInfo.name and outputInfo.crtc: diff --git a/src/pymonctl/_pymonctl_macos.py b/src/pymonctl/_pymonctl_macos.py index b8fcf07..6edcd01 100644 --- a/src/pymonctl/_pymonctl_macos.py +++ b/src/pymonctl/_pymonctl_macos.py @@ -20,8 +20,8 @@ import Quartz import Quartz.CoreGraphics as CG -from pymonctl import BaseMonitor, _pointInBox, _getRelativePosition, \ - DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation +from ._main import BaseMonitor, _pointInBox, _getRelativePosition, \ + DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation from ._display_manager_lib import Display @@ -32,6 +32,9 @@ def _getAllMonitors() -> list[MacOSMonitor]: desc = screen.deviceDescription() displayId = desc['NSScreenNumber'] # Quartz.NSScreenNumber seems to be wrong monitors.append(MacOSMonitor(displayId)) + # Alternatives to test: + v, ids, cnt = CG.CGGetOnlineDisplayList(10, None, None) + v, ids, cnt = CG.CGGetActiveDisplayList(10, None, None) return monitors @@ -416,7 +419,7 @@ def setMode(self, mode: Optional[DisplayMode]): try: ret, bestMode = CG.CGDisplayBestModeForParametersAndRefreshRate( self.handle, self.colordepth, mode.width, mode.height, mode.frequency, None) - CG.CGDisplaySwitchToMode(self.handle, bestMode) + CG.CGDisplaySwitchToMode(self.handle, bestMode.get("Mode", 0)) # ret, configRef = Quartz.CGBeginDisplayConfiguration(None) # ret = Quartz.CGConfigureDisplayWithDisplayMode(configRef, self.handle, bestMode, None) # if not ret: @@ -468,14 +471,16 @@ def turnOn(self): # This works, but won't wake up the display despite if the mouse is moving and/or clicking def mouseEvent(eventType, posx, posy): - theEvent = CG.CGEventCreateMouseEvent(None, eventType, CG.CGPointMake(posx, posy), CG.kCGMouseButtonLeft) - CG.CGEventSetType(theEvent, eventType) + ev = CG.CGEventCreateMouseEvent(None, eventType, CG.CGPointMake(posx, posy), CG.kCGMouseButtonLeft) + CG.CGEventSetType(ev, eventType) # or kCGSessionEventTap? - CG.CGEventPost(CG.kCGHIDEventTap, theEvent) - # CG.CFRelease(theEvent) # Produces a Hardware error?!?!?! + CG.CGEventPost(CG.kCGHIDEventTap, ev) + # CG.CFRelease(ev) # Produces a Hardware error?!?!?! def mousemove(posx, posy): mouseEvent(CG.kCGEventMouseMoved, posx, posy) + # Alternative: + CG.CGDisplayMoveCursorToPoint(self.handle, (posx, posy)) def mouseclick(posx, posy): # Not necessary to previously move the mouse to given location diff --git a/src/pymonctl/_pymonctl_win.py b/src/pymonctl/_pymonctl_win.py index 93a2920..343b91a 100644 --- a/src/pymonctl/_pymonctl_win.py +++ b/src/pymonctl/_pymonctl_win.py @@ -17,13 +17,13 @@ import win32evtlog import win32gui -from pymonctl import BaseMonitor, _getRelativePosition, \ - DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation -from pymonctl.structs import (_QDC_ONLY_ACTIVE_PATHS, _DISPLAYCONFIG_PATH_INFO, _DISPLAYCONFIG_MODE_INFO, _LUID, - _DISPLAYCONFIG_SOURCE_DPI_SCALE_GET, _DISPLAYCONFIG_SOURCE_DPI_SCALE_SET, _DPI_VALUES, - _DISPLAYCONFIG_DEVICE_INFO_GET_DPI_SCALE, _DISPLAYCONFIG_DEVICE_INFO_SET_DPI_SCALE, - _DISPLAYCONFIG_SOURCE_DEVICE_NAME, _DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME - ) +from ._main import BaseMonitor, _getRelativePosition, \ + DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation +from pymonctl._structs import (_QDC_ONLY_ACTIVE_PATHS, _DISPLAYCONFIG_PATH_INFO, _DISPLAYCONFIG_MODE_INFO, _LUID, + _DISPLAYCONFIG_SOURCE_DPI_SCALE_GET, _DISPLAYCONFIG_SOURCE_DPI_SCALE_SET, _DPI_VALUES, + _DISPLAYCONFIG_DEVICE_INFO_GET_DPI_SCALE, _DISPLAYCONFIG_DEVICE_INFO_SET_DPI_SCALE, + _DISPLAYCONFIG_SOURCE_DEVICE_NAME, _DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME + ) dpiAware = ctypes.windll.user32.GetAwarenessFromDpiAwarenessContext(ctypes.windll.user32.GetThreadDpiAwarenessContext()) @@ -56,8 +56,8 @@ def _getAllMonitorsDict() -> dict[str, ScreenValue]: pass if dev and dev.StateFlags & win32con.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP: name = monName - x, y, r, b = monitorInfo.get("Monitor", (0, 0, -1, -1)) - wx, wy, wr, wb = monitorInfo.get("Work", (0, 0, -1, -1)) + x, y, r, b = monitorInfo["Monitor"] + wx, wy, wr, wb = monitorInfo["Work"] is_primary = monitorInfo.get("Flags", 0) == win32con.MONITORINFOF_PRIMARY pScale = ctypes.c_uint() ctypes.windll.shcore.GetScaleFactorForMonitor(hMon, ctypes.byref(pScale)) @@ -172,28 +172,28 @@ def __init__(self, handle: Optional[int] = None): @property def size(self) -> Optional[Size]: - if self.handle is not None: - monitorInfo = win32api.GetMonitorInfo(self.handle) - if monitorInfo: - x, y, r, b = monitorInfo.get("Monitor", (0, 0, -1, -1)) + monitorInfo = win32api.GetMonitorInfo(self.handle) + if monitorInfo: + x, y, r, b = monitorInfo["Monitor"] + if x is not None: return Size(abs(r - x), abs(b - y)) return None @property def workarea(self) -> Optional[Rect]: - if self.handle is not None: - monitorInfo = win32api.GetMonitorInfo(self.handle) - if monitorInfo: - wx, wy, wr, wb = monitorInfo.get("Work", (0, 0, -1, -1)) + monitorInfo = win32api.GetMonitorInfo(self.handle) + if monitorInfo: + wx, wy, wr, wb = monitorInfo["Work"] + if wx is not None: return Rect(wx, wy, wr, wb) return None @property def position(self) -> Optional[Point]: - if self.handle is not None: - monitorInfo = win32api.GetMonitorInfo(self.handle) - if monitorInfo: - x, y, r, b = monitorInfo.get("Monitor", (0, 0, -1, -1)) + monitorInfo = win32api.GetMonitorInfo(self.handle) + if monitorInfo: + x, y, r, b = monitorInfo["Monitor"] + if x is not None: return Point(x, y) return None @@ -202,34 +202,32 @@ def setPosition(self, relativePos: Union[int, Position], relativeTo: Optional[st @property def box(self) -> Optional[Box]: - if self.handle is not None: - monitorInfo = win32api.GetMonitorInfo(self.handle) - if monitorInfo: - x, y, r, b = monitorInfo.get("Monitor", (0, 0, -1, -1)) + monitorInfo = win32api.GetMonitorInfo(self.handle) + if monitorInfo: + x, y, r, b = monitorInfo["Monitor"] + if x is not None: return Box(x, y, abs(r - x), abs(b - y)) return None @property def rect(self) -> Optional[Rect]: - if self.handle is not None: - monitorInfo = win32api.GetMonitorInfo(self.handle) - if monitorInfo: - x, y, r, b = monitorInfo.get("Monitor", (0, 0, -1, -1)) + monitorInfo = win32api.GetMonitorInfo(self.handle) + if monitorInfo: + x, y, r, b = monitorInfo["Monitor"] + if x is not None: return Rect(x, y, r, b) return None @property def scale(self) -> Optional[Tuple[float, float]]: - if self.handle is not None: - pScale = ctypes.c_uint() - ctypes.windll.shcore.GetScaleFactorForMonitor(self.handle, ctypes.byref(pScale)) - # import wmi - # obj = wmi.WMI().Win32_PnPEntity(ConfigManagerErrorCode=0) - # displays = [x for x in obj if 'DISPLAY' in str(x)] - # for item in displays: - # print(item) - return float(pScale.value), float(pScale.value) - return None + pScale = ctypes.c_uint() + ctypes.windll.shcore.GetScaleFactorForMonitor(self.handle, ctypes.byref(pScale)) + # import wmi + # obj = wmi.WMI().Win32_PnPEntity(ConfigManagerErrorCode=0) + # displays = [x for x in obj if 'DISPLAY' in str(x)] + # for item in displays: + # print(item) + return float(pScale.value), float(pScale.value) def _getPaths(self) -> Tuple[Optional[_LUID], Optional[int]]: @@ -265,7 +263,7 @@ def _getPaths(self) -> Tuple[Optional[_LUID], Optional[int]]: def setScale(self, scale: Optional[Tuple[float, float]]): - if self.handle is not None and scale is not None: + if scale is not None: # https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.cpp # https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.cpp # HOW to GET adapterId and sourceId values???? -> QueryDisplayConfig @@ -291,9 +289,9 @@ def setScale(self, scale: Optional[Tuple[float, float]]): scaleValue: int = int(scale[0]) targetScale = -1 - if scale < minScale: + if scaleValue < minScale: targetScale = 0 - elif scale > maxScale: + elif scaleValue > maxScale: targetScale = len(_DPI_VALUES) - 1 else: try: @@ -314,12 +312,10 @@ def setScale(self, scale: Optional[Tuple[float, float]]): @property def dpi(self) -> Optional[Tuple[float, float]]: - if self.handle is not None: - dpiX = ctypes.c_uint() - dpiY = ctypes.c_uint() - ctypes.windll.shcore.GetDpiForMonitor(self.handle, 0, ctypes.byref(dpiX), ctypes.byref(dpiY)) - return dpiX.value, dpiY.value - return None + dpiX = ctypes.c_uint() + dpiY = ctypes.c_uint() + ctypes.windll.shcore.GetDpiForMonitor(self.handle, 0, ctypes.byref(dpiX), ctypes.byref(dpiY)) + return dpiX.value, dpiY.value @property def orientation(self) -> Optional[Union[int, Orientation]]: @@ -355,20 +351,20 @@ def colordepth(self) -> Optional[int]: @property def brightness(self) -> Optional[int]: - if self.handle is not None: - minBright = ctypes.c_uint() - currBright = ctypes.c_uint() - maxBright = ctypes.c_uint() - hDevices = _win32getPhysicalMonitorsHandles(self.handle) - for hDevice in hDevices: - ctypes.windll.dxva2.GetMonitorBrightness(hDevice, ctypes.byref(minBright), ctypes.byref(currBright), - ctypes.byref(maxBright)) - _win32destroyPhysicalMonitors(hDevices) - return currBright.value + minBright = ctypes.c_uint() + currBright = ctypes.c_uint() + maxBright = ctypes.c_uint() + hDevices = _win32getPhysicalMonitorsHandles(self.handle) + for hDevice in hDevices: + ctypes.windll.dxva2.GetMonitorBrightness(hDevice, ctypes.byref(minBright), ctypes.byref(currBright), + ctypes.byref(maxBright)) + _win32destroyPhysicalMonitors(hDevices) + return currBright.value return None + def setBrightness(self, brightness: Optional[int]): - if brightness is not None and self.handle is not None: + if brightness is not None: minBright = ctypes.c_uint() currBright = ctypes.c_uint() maxBright = ctypes.c_uint() @@ -388,20 +384,19 @@ def setBrightness(self, brightness: Optional[int]): @property def contrast(self) -> Optional[int]: - if self.handle is not None: - minCont = ctypes.c_uint() - currCont = ctypes.c_uint() - maxCont = ctypes.c_uint() - hDevices = _win32getPhysicalMonitorsHandles(self.handle) - for hDevice in hDevices: - ctypes.windll.dxva2.GetMonitorContrast(hDevice, ctypes.byref(minCont), ctypes.byref(currCont), - ctypes.byref(maxCont)) - _win32destroyPhysicalMonitors(hDevices) - return currCont.value + minCont = ctypes.c_uint() + currCont = ctypes.c_uint() + maxCont = ctypes.c_uint() + hDevices = _win32getPhysicalMonitorsHandles(self.handle) + for hDevice in hDevices: + ctypes.windll.dxva2.GetMonitorContrast(hDevice, ctypes.byref(minCont), ctypes.byref(currCont), + ctypes.byref(maxCont)) + _win32destroyPhysicalMonitors(hDevices) + return currCont.value return None def setContrast(self, contrast: Optional[int]): - if contrast is not None and self.handle is not None: + if contrast is not None: minCont = ctypes.c_uint() currCont = ctypes.c_uint() maxCont = ctypes.c_uint() @@ -466,7 +461,7 @@ def setPrimary(self): def turnOn(self): # https://stackoverflow.com/questions/16402672/control-screen-with-python - if self.handle is not None and self._hasVCPSupport and self._hasVCPPowerSupport: + if self._hasVCPSupport and self._hasVCPPowerSupport: if not self.isOn: hDevices = _win32getPhysicalMonitorsHandles(self.handle) for hDevice in hDevices: @@ -484,7 +479,7 @@ def turnOn(self): def turnOff(self): # https://stackoverflow.com/questions/16402672/control-screen-with-python - if self.handle is not None and self._hasVCPSupport and self._hasVCPPowerSupport: + if self._hasVCPSupport and self._hasVCPPowerSupport: if self.isOn: hDevices = _win32getPhysicalMonitorsHandles(self.handle) for hDevice in hDevices: @@ -497,7 +492,7 @@ def turnOff(self): win32con.SMTO_ABORTIFHUNG, 100) def suspend(self): - if self.handle is not None and self._hasVCPSupport and self._hasVCPPowerSupport: + if self._hasVCPSupport and self._hasVCPPowerSupport: hDevices = _win32getPhysicalMonitorsHandles(self.handle) for hDevice in hDevices: # code and value according to: VESA Monitor Control Command Set (MCCS) standard, version 1.0 and 2.0. @@ -511,26 +506,25 @@ def suspend(self): @property def isOn(self) -> Optional[bool]: ret = None - if self.handle is not None: - if self._hasVCPSupport and self._hasVCPPowerSupport: - hDevices = _win32getPhysicalMonitorsHandles(self.handle) - for hDevice in hDevices: - # code and value according to: VESA Monitor Control Command Set (MCCS) standard, version 1.0 and 2.0. - pvct = ctypes.c_uint() - currValue = ctypes.c_uint() - maxValue = ctypes.c_uint() - ctypes.windll.dxva2.GetVCPFeatureAndVCPFeatureReply(hDevice, 0xD6, ctypes.byref(pvct), - ctypes.byref(currValue), ctypes.byref(maxValue)) - ret = currValue.value == 1 - _win32destroyPhysicalMonitors(hDevices) - else: - # Not working by now (tried with hDevice as well) - # https://stackoverflow.com/questions/203355/is-there-any-way-to-detect-the-monitor-state-in-windows-on-or-off - # https://learn.microsoft.com/en-us/windows/win32/power/power-management-functions - is_working = ctypes.c_uint() - res = ctypes.windll.kernel32.GetDevicePowerState(self.handle, ctypes.byref(is_working)) - if res: - ret = bool(is_working.value == 1) + if self._hasVCPSupport and self._hasVCPPowerSupport: + hDevices = _win32getPhysicalMonitorsHandles(self.handle) + for hDevice in hDevices: + # code and value according to: VESA Monitor Control Command Set (MCCS) standard, version 1.0 and 2.0. + pvct = ctypes.c_uint() + currValue = ctypes.c_uint() + maxValue = ctypes.c_uint() + ctypes.windll.dxva2.GetVCPFeatureAndVCPFeatureReply(hDevice, 0xD6, ctypes.byref(pvct), + ctypes.byref(currValue), ctypes.byref(maxValue)) + ret = currValue.value == 1 + _win32destroyPhysicalMonitors(hDevices) + else: + # Not working by now (tried with hDevice as well) + # https://stackoverflow.com/questions/203355/is-there-any-way-to-detect-the-monitor-state-in-windows-on-or-off + # https://learn.microsoft.com/en-us/windows/win32/power/power-management-functions + is_working = ctypes.c_uint() + res = ctypes.windll.kernel32.GetDevicePowerState(self.handle, ctypes.byref(is_working)) + if res: + ret = bool(is_working.value == 1) return ret def attach(self): @@ -626,14 +620,14 @@ def _setPosition(relativePos: Union[int, Position], relativeTo: Optional[str], n monitors = _win32getAllMonitorsDict() if name in monitors.keys() and relativeTo in monitors.keys(): targetMonInfo = monitors[name]["monitor"] - x, y, r, b = targetMonInfo.get("Monitor", (0, 0, -1, -1)) + x, y, r, b = targetMonInfo["Monitor"] w = abs(r - x) h = abs(b - y) targetMon = {"relativePos": relativePos, "relativeTo": relativeTo, "position": Point(x, y), "size": Size(w, h)} relMonInfo = monitors[relativeTo]["monitor"] - x, y, r, b = relMonInfo.get("Monitor", (0, 0, -1, -1)) + x, y, r, b = relMonInfo["Monitor"] w = abs(r - x) h = abs(b - y) relMon = {"position": Point(x, y), "size": Size(w, h)} @@ -809,6 +803,3 @@ def _eventLogLoop(kill: threading.Event, interval: float): kill.wait(interval) win32evtlog.CloseEventLog(handle) - -# import win32ui -# print(win32ui.GetDeviceCaps(win32gui.GetDC(None), win32con.HORZSIZE), win32ui.GetDeviceCaps(win32gui.GetDC(None), win32con.VERTSIZE)) \ No newline at end of file diff --git a/src/pymonctl/structs.py b/src/pymonctl/_structs.py similarity index 84% rename from src/pymonctl/structs.py rename to src/pymonctl/_structs.py index 5c0546a..ba8fafa 100644 --- a/src/pymonctl/structs.py +++ b/src/pymonctl/_structs.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import annotations -import ctypes import ctypes.wintypes from enum import IntEnum from typing import NamedTuple, Tuple @@ -10,6 +9,7 @@ class Box(NamedTuple): + """Container class to handle Box struct (left, top, width, height)""" left: int top: int width: int @@ -17,6 +17,7 @@ class Box(NamedTuple): class Rect(NamedTuple): + """Container class to handle Rect struct (left, top, right, bottom)""" left: int top: int right: int @@ -24,16 +25,33 @@ class Rect(NamedTuple): class Point(NamedTuple): + """Container class to handle Point struct (x, y)""" x: int y: int class Size(NamedTuple): + """Container class to handle Size struct (right, bottom)""" width: int height: int class ScreenValue(TypedDict): + """ + Container class to handle ScreenValue struct: + + - system_name (str): name of the monitor as known by the system + - id (int): handle/identifier of the monitor + - is_primary (bool): ''True'' if it is the primary monitor + - position (Point): position of the monitor + - size (Size): size of the monitor, in pixels + - workarea (Rect): coordinates of the usable area of the monitor usable by apps/windows (no docks, taskbars, ...) + - scale (Tuple[int, int]): text scale currently applied to monitor + - dpi (Tuple[int, int]): dpi values of current resolution + - orientation (int): rotation value of the monitor as per Orientation values (NORMAL = 0, RIGHT = 1, INVERTED = 2, LEFT = 3) + - frequency (float): refresh rate of the monitor + - colordepth (int): color depth of the monitor + """ system_name: str id: int is_primary: bool @@ -48,6 +66,13 @@ class ScreenValue(TypedDict): class DisplayMode(NamedTuple): + """ + Container class to handle DisplayMode struct: + + - width (int): width, in pixels + - height (int): height, in pixels + - frequency (float): refresh rate + """ width: int height: int frequency: float diff --git a/tests/test_pymonctl.py b/tests/test_pymonctl.py index 1ec7d0e..fc0dbf2 100644 --- a/tests/test_pymonctl.py +++ b/tests/test_pymonctl.py @@ -6,7 +6,7 @@ from typing import Union import pymonctl as pmc -from pymonctl.structs import * +from pymonctl._structs import * def countChanged(names, screensInfo):