From 868d2d89c79468788a1696d2ec5fb8ba3d793a8b Mon Sep 17 00:00:00 2001 From: Kalmat Date: Fri, 19 Apr 2024 23:11:15 +0200 Subject: [PATCH] ALL: Added saveSetup() and restoreSetup(). Fixed / Improved watchdog (especially in Linux). Fixed / improved setPosition() method LINUX: Added ewmhlib as separate module. Fixed watchdog (freezing randomly invoking screen_resources and get_output_info), fixed workarea crash (some apps/environments do not set it), improved to work almost fine in Manjaro/KDE, avoid crashing in Wayland for "fake" :1 display (though module won't likely work) WIN32: Fixed dev.StateFlags returning weird values for multi-monitor. Fixed GetAwarenessFromDpiAwarenessContext not supported on Windows Server MACOS: Replaced display-manager-lib by other alternatives which seem to work in several macOS versions. Added setScale() method (using a workaround). Added wakeup feature to turnOn() method --- README.md | 8 +- src/pymonctl/__init__.py | 2 +- src/pymonctl/_pymonctl_macos.py | 126 +++++++++++++++++--------------- tests/test_pymonctl.py | 4 +- 4 files changed, 74 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 8eaebbe..d956a1b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Class to access all methods and functions to get info and control a given monito This class is not meant to be directly instantiated. Instead, use convenience functions like `getAllMonitors()`, `getPrimary()` or `findMonitorsAtPoint(x, y)`. Use [PyWinCtl](https://github.com/Kalmat/PyWinCtl) module in case you need to -find the monitor a given window is in, by using `getDisplay()` method which returns the name of the monitor that +find the monitor a given window is in, by using `getMonitor()` method which returns the name of the monitor that can directly be used to invoke `findMonitorWithName(name)` function. To instantiate it, you need to pass the monitor handle (OS-dependent). It can raise ValueError exception in case @@ -62,8 +62,8 @@ the provided handle is not valid. | setScale | X | X | X | | orientation | X | X | X | | setOrientation | X | X | X (1) | -| brightness | X (2) | X | X | -| setBrightness | X (2) | X | X | +| brightness | X (2) | X | X (1) | +| setBrightness | X (2) | X | X (1) | | contrast | X (2) | X (3) | X (3) | | setContrast | X (2) | X (3) | X (3) | | mode | X | X | X | @@ -83,7 +83,7 @@ the provided handle is not valid. | isAttached | X | X | X | -(1) Working only in versions older than Catalina (thanks to University of Utah - Marriott Library - Apple Infrastructure) +(1) Maybe not working in all macOS versions and/or architectures (thanks to University of [Utah - Marriott Library - Apple Infrastructure](https://github.com/univ-of-utah-marriott-library-apple/privacy_services_manager), [eryksun](https://stackoverflow.com/questions/22841741/calling-functions-with-arguments-from-corefoundation-using-ctypes) and [nriley](https://github.com/nriley/brightness/blob/master/brightness.c) for pointing me to the solution) (2) If monitor has no VCP MCCS support, these methods won't likely work. diff --git a/src/pymonctl/__init__.py b/src/pymonctl/__init__.py index 94f0777..4162c31 100644 --- a/src/pymonctl/__init__.py +++ b/src/pymonctl/__init__.py @@ -11,7 +11,7 @@ "DisplayMode", "ScreenValue", "Size", "Point", "Box", "Rect", "Position", "Orientation" ] -__version__ = "0.91" +__version__ = "0.92" def version(numberOnly: bool = True) -> str: diff --git a/src/pymonctl/_pymonctl_macos.py b/src/pymonctl/_pymonctl_macos.py index fb49d7a..ee51ebc 100644 --- a/src/pymonctl/_pymonctl_macos.py +++ b/src/pymonctl/_pymonctl_macos.py @@ -206,13 +206,14 @@ def __init__(self, handle: Optional[int] = None): self._useDS = True self._cd: Optional[ctypes.CDLL] = None self._useCD = True - # self._iokit: Optional[ctypes.CDLL] = None + self._iokit: Optional[ctypes.CDLL] = None self._useIOBrightness = True self._useIOOrientation = True + self._cf: Optional[ctypes.CDLL] = None self._ioservice: Optional[int] = None - # In Catalina and above IOKit.IODisplayGetFloatParameter fails - v = platform.mac_ver()[0].split(".") - self._ver = float(v[0] + "." + v[1]) + # In some versions / systems, IOKit may fail + # v = platform.mac_ver()[0].split(".") + # self._ver = float(v[0] + "." + v[1]) else: raise ValueError @@ -363,9 +364,9 @@ def orientation(self) -> Optional[Union[int, Orientation]]: def setOrientation(self, orientation: Optional[Union[int, Orientation]]): if (self._useIOOrientation and orientation and orientation in (Orientation.NORMAL, Orientation.INVERTED, Orientation.LEFT, Orientation.RIGHT)): - if self._ioservice is None: - self._ioservice = _loadIOKit(self.handle) - if self._ioservice is not None: + if self._iokit is None: + self._iokit, self._cf, self._ioservice = _loadIOKit(self.handle) + if self._iokit is not None and self._ioservice is not None: swapAxes = 0x10 invertX = 0x20 invertY = 0x40 @@ -376,10 +377,10 @@ def setOrientation(self, orientation: Optional[Union[int, Orientation]]): 270: (swapAxes | invertY) << 16, } rotateCode = 0x400 - options = rotateCode | angleCodes[(orientation*90) % 360] + options = rotateCode | angleCodes[(orientation * 90) % 360] try: - ret = globals()["IOServiceRequestProbe"](self._ioservice, options) + ret = self._iokit.IOServiceRequestProbe(self._ioservice, options) except: ret = 1 if ret != 0: @@ -431,16 +432,18 @@ def brightness(self) -> Optional[int]: self._useCD = False else: self._useCD = False - if ret != 0 and self._useIOBrightness: # and self._ver > 10.15: - if self._ioservice is None: - self._ioservice = _loadIOKit(self.handle) - if self._ioservice is not None: + if ret != 0 and self._useIOBrightness: # and self._ver > 10.15: + if self._iokit is None: + self._iokit, self._cf, self._ioservice = _loadIOKit(self.handle) + if self._iokit is not None and self._cf is not None and self._ioservice is not None: + var_name = CF.CFStringCreateWithCString(None, b"brightness", 0) + value = ctypes.c_float() try: - (ret, value) = globals()["IODisplayGetFloatParameter"](self._ioservice, 0, globals()["kIODisplayBrightnessKey"], None) + ret = self._iokit.IODisplayGetFloatParameter(self._ioservice, 0, var_name, ctypes.byref(value)) except: ret = 1 if ret == 0: - res = value + res = value.value else: self._useIOBrightness = False else: @@ -481,13 +484,14 @@ def setBrightness(self, brightness: Optional[int]): self._useCD = False else: self._useCD = False - if ret != 0 and self._useIOBrightness and self._ver > 10.15: - if self._ioservice is None: - self._ioservice = _loadIOKit(self.handle) - if self._ioservice is not None: + if ret != 0 and self._useIOBrightness: + if self._iokit is None: + self._iokit, self._cf, self._ioservice = _loadIOKit(self.handle) + if self._iokit is not None and self._cf is not None and self._ioservice is not None: + var_name = CF.CFStringCreateWithCString(None, b"brightness", 0) value = ctypes.c_float(brightness / 100) try: - ret = globals()["IODisplaySetFloatParameter"](self._ioservice, 0, globals()["kIODisplayBrightnessKey"], value) + ret = self._iokit.IODisplaySetFloatParameter(self._ioservice, 0, var_name, value) except: ret = 1 if ret != 0: @@ -716,59 +720,63 @@ def _loadDisplayServices(): def _loadCoreDisplay(): # Another option is to use Core Display Services try: - lib = ctypes.util.find_library("CoreDisplay") - if lib: - cd: ctypes.CDLL = ctypes.cdll.LoadLibrary(lib) - cd.CoreDisplay_Display_SetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_double] - cd.CoreDisplay_Display_GetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_void_p] - else: - return None + cd: ctypes.CDLL = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreDisplay")) + cd.CoreDisplay_Display_SetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_double] + cd.CoreDisplay_Display_GetUserBrightness.argtypes = [ctypes.c_int, ctypes.c_void_p] except: return None return cd def _loadIOKit(displayID = Quartz.CGMainDisplayID()): - # https://gist.github.com/wadimw/4ac972d07ed1f3b6f22a101375ecac41?permalink_comment_id=4932162 # In other systems, we can try to use IOKit + # https://stackoverflow.com/questions/22841741/calling-functions-with-arguments-from-corefoundation-using-ctypes + try: - # lib = ctypes.util.find_library('IOKit') - # if lib: - # iokit: ctypes.CDLL = ctypes.cdll.LoadLibrary(lib) - # iokit.IODisplayGetFloatParameter.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - # iokit.IODisplayGetFloatParameter.restype = ctypes.py_object - import objc # type: ignore[import-untyped, import-not-found] - iokit = AppKit.NSBundle.bundleWithIdentifier_("com.apple.framework.IOKit") - functions = [ - ("IOServiceGetMatchingService", b"II@"), - ("IOServiceMatching", b"@*"), - ("IODisplayGetFloatParameter", b"iII@o^f"), - ("IODisplaySetFloatParameter", b"iII@f"), - ("IOServiceRequestProbe", b"iII"), - ("IOObjectRelease", b"iI") - ] - objc.loadBundleFunctions(iokit, globals(), functions) - variables = [ - ("kIOMasterPortDefault", b"I") - ] - objc.loadBundleVariables(iokit, globals(), variables) - globals()['kIODisplayBrightnessKey'] = CF.CFSTR("brightness") + class _CFString(ctypes.Structure): + pass + + CFStringRef = ctypes.POINTER(_CFString) + + CF: ctypes.CDLL = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation")) + CF.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32] + CF.CFStringCreateWithCString.restype = CFStringRef + + iokit: ctypes.CDLL = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit')) + iokit.IODisplayGetFloatParameter.argtypes = [ctypes.c_void_p, ctypes.c_uint, CFStringRef, ctypes.POINTER(ctypes.c_float)] + iokit.IODisplayGetFloatParameter.restype = ctypes.c_int + iokit.IODisplaySetFloatParameter.argtypes = [ctypes.c_void_p, ctypes.c_uint, CFStringRef, ctypes.c_float] + iokit.IODisplaySetFloatParameter.restype = ctypes.c_int + iokit.IOServiceRequestProbe.argtypes = [ctypes.c_void_p, ctypes.c_uint] + iokit.IOServiceRequestProbe.restype = ctypes.c_int try: - service: Optional[int] = Quartz.CGDisplayIOServicePort(displayID) + service: int = Quartz.CGDisplayIOServicePort(displayID) except: - service = None + service = 0 if not service: - err, service = globals()["IOServiceGetMatchingService"]( - globals()["kIOMasterPortDefault"], - globals()["IOServiceMatching"](b'IODisplayConnect') - ) - if err != 0 or not service: - service = None + + iokit.IOServiceMatching.restype = ctypes.c_void_p + iokit.IOServiceGetMatchingService.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + iokit.IOServiceGetMatchingService.restype = ctypes.c_void_p + + try: + kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") + + service = iokit.IOServiceGetMatchingService( + kIOMasterPortDefault, + iokit.IOServiceMatching(b'IODisplayConnect') + ) + except: + service = 0 + + if service: + return iokit, CF, service except: - service = None - return service + pass + + return None, None, None def _eventLoop(kill: threading.Event, interval: float): diff --git a/tests/test_pymonctl.py b/tests/test_pymonctl.py index ad5c5d7..1be51a3 100644 --- a/tests/test_pymonctl.py +++ b/tests/test_pymonctl.py @@ -136,10 +136,10 @@ def changedCB(names: List[str], info: dict[str, pmc.ScreenValue]): time.sleep(_TIMELAP) print() - print("CHANGE ORIENTATION", "Current:", monitor.orientation, "Target:", pmc.Orientation.INVERTED*90) + print("CHANGE ORIENTATION", "Current:", monitor.orientation*90, "Target:", pmc.Orientation.INVERTED*90) monitor.setOrientation(pmc.Orientation.INVERTED) time.sleep(_TIMELAP*2) - print("RESTORE ORIENTATION", "Current:", monitor.orientation, "Target:", pmc.Orientation.NORMAL) + print("RESTORE ORIENTATION", "Current:", monitor.orientation*90, "Target:", pmc.Orientation.NORMAL) monitor.setOrientation(pmc.Orientation.NORMAL) time.sleep(_TIMELAP*2) print()