From 4aade851df55a685223da671350b564e672113e6 Mon Sep 17 00:00:00 2001 From: Kalmat Date: Wed, 23 Aug 2023 22:16:49 +0200 Subject: [PATCH] MACOS: Added brightness(), setBrightnes(), setOrientation() thru display_manager_lib (thanks to University of Utah - Marriott Library - Apple Infrastructure) WIN32: Fixed setScale() --- AUTHORS.txt | 1 + CHANGES.txt | 5 + DISPLAY_MANAGER_LICENSE.txt | 14 + LICENSE.txt | 4 +- README.md | 81 +- TODO.txt | 1 - ...y.whl => PyMonCtl-0.0.11-py3-none-any.whl} | Bin 117142 -> 117373 bytes src/pymonctl/__init__.py | 2 +- src/pymonctl/_display_manager_lib.py | 747 ++++++++++++++++++ src/pymonctl/_pymonctl_macos.py | 21 +- 10 files changed, 822 insertions(+), 54 deletions(-) create mode 100644 DISPLAY_MANAGER_LICENSE.txt rename dist/{PyMonCtl-0.0.10-py3-none-any.whl => PyMonCtl-0.0.11-py3-none-any.whl} (82%) create mode 100644 src/pymonctl/_display_manager_lib.py diff --git a/AUTHORS.txt b/AUTHORS.txt index d9c02aa..264c2c7 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,3 +1,4 @@ PyMonCtl authors, contributors and maintainers: Kalmat https://github.com/Kalmat +University of Utah - Marriott Library - Apple Infrastructure https://github.com/univ-of-utah-marriott-library-apple \ No newline at end of file diff --git a/CHANGES.txt b/CHANGES.txt index 1c34714..5703034 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,2 +1,7 @@ +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 + LINUX: Added attach()/detach(), fixed setPosition() and arrangeMonitors() + WIN32: Fixed and improved many issues (scale still pending) 0.0.9, 2023/07/19 -- New approach based on Monitor() class to access all properties and functionalities (macOS pending) 0.0.8, 2023/05/12 -- Pre-release tested OK in Linux/X11 (not Wayland) and win32 (macOS pending) diff --git a/DISPLAY_MANAGER_LICENSE.txt b/DISPLAY_MANAGER_LICENSE.txt new file mode 100644 index 0000000..25dade2 --- /dev/null +++ b/DISPLAY_MANAGER_LICENSE.txt @@ -0,0 +1,14 @@ +######################################################################## +# Copyright (c) 2018 University of Utah Student Computing Labs. # +# All Rights Reserved. # +# # +# Permission to use, copy, modify, and distribute this software and # +# its documentation for any purpose and without fee is hereby granted, # +# provided that the above copyright notice appears in all copies and # +# that both that copyright notice and this permission notice appear # +# in supporting documentation, and that the name of The University # +# of Utah not be used in advertising or publicity pertaining to # +# distribution of the software without specific, written prior # +# permission. This software is supplied as is without expressed or # +# implied warranties of any kind. # +######################################################################## diff --git a/LICENSE.txt b/LICENSE.txt index 92eaf79..816d7e7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -24,4 +24,6 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + diff --git a/README.md b/README.md index 45ee683..368cec3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ 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 ## General Functions @@ -31,48 +32,50 @@ getPrimary() or findMonitor(x, y). To instantiate it, you need to pass the monitor handle (OS-dependent). It can raise ValueError exception in case the provided handle is not valid. -| | Windows | Linux | macOS | -|:--------------:|:-------:|:-----:|:-----:| -| size | X | X | X | -| workarea | X | X | X | -| position | X | X | X | -| setPosition | X | X | X | -| box | X | X | X | -| rect | X | X | X | -| scale | X | X | X | -| setScale | X | X | X | -| dpi | X | X | X | -| orientation | X | X | X | -| setOrientation | X | X | | -| frequency | X | X | X | -| colordepth | X | X | X | -| brightness | X (1) | X | | -| setBrightness | X (1) | X | | -| contrast | X (1) | X | | -| setContrast | X (1) | X | | -| mode | X | X | X | -| setMode | X | X | X | -| defaultMode | X | X | X | -| setDefaultMode | X | X | X | -| allModes | X | X | X | -| isPrimary | X | X | X | -| setPrimary | X | X | X | -| turnOn | X | X | | -| turnOff | X (2) | X | | -| suspend | X (2) | X (3) | X (3) | -| isOn | X | X | | -| attach | X | X | | -| detach | X | X | | -| isAttached | X | X | X | - - -(1) If monitor has no VCP MCCS support, these methods won't likely work. - -(2) If monitor has no VCP MCCS support, it can not be addressed separately, +| | Windows | Linux | macOS | +|:--------------:|:-------:|:------:|:-----:| +| size | X | X | X | +| workarea | X | X | X | +| position | X | X | X | +| setPosition | X | X | X | +| box | X | X | X | +| rect | X | X | X | +| scale | X | X | X | +| setScale | X | X | X | +| dpi | X | X | X | +| orientation | X | X | X | +| setOrientation | X | X | X (1) | +| frequency | X | X | X | +| colordepth | X | X | X | +| brightness | X (2) | X | X (1) | +| setBrightness | X (2) | X | X (1) | +| contrast | X (2) | X | | +| setContrast | X (2) | X | | +| mode | X | X | X | +| setMode | X | X | X | +| defaultMode | X | X | X | +| setDefaultMode | X | X | X | +| allModes | X | X | X | +| isPrimary | X | X | X | +| setPrimary | X | X | X | +| turnOn | X | X | | +| turnOff | X (3) | X | | +| suspend | X (3) | X (4) | X (4) | +| isOn | X | X | | +| attach | X | X | | +| detach | X | X | | +| isAttached | X | X | X | + + +(1) Thru display_manager_lib from University of Utah - Marriott Library - Apple Infrastructure (thank you, guys!) + +(2) If monitor has no VCP MCCS support, these methods won't likely work. + +(3) If monitor has no VCP MCCS support, it can not be addressed separately, so ALL monitors will be turned off / suspended. To address a specific monitor, try using detach() / attach() methods. -(3) It will suspend ALL monitors. +(4) It will suspend ALL monitors. #### WARNING: Most of these properties may return ''None'' in case the value can not be obtained diff --git a/TODO.txt b/TODO.txt index 699e2d1..4924d69 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,7 +8,6 @@ WINDOWS - Find a solution for changing scale MACOS -- Contact display-manager-lib team to check alternatives or find another solution (for setOrientation, brightness) - Test everything in an actual macOS installation with a multi-monitor setup - Check returned coordinates (multi-monitor, flipped, ...) - Find a way to turn monitor OFF/ON/SUSPEND/WAKEUP/DETACH/ATTACH diff --git a/dist/PyMonCtl-0.0.10-py3-none-any.whl b/dist/PyMonCtl-0.0.11-py3-none-any.whl similarity index 82% rename from dist/PyMonCtl-0.0.10-py3-none-any.whl rename to dist/PyMonCtl-0.0.11-py3-none-any.whl index b365968f2418b92c22652870e5698570e6370292..a36c427e830171a20b2537f8d6cedd036e5c61c2 100644 GIT binary patch delta 18321 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ಽwR$Es?Oa5fg2^&Xt4!&^6fW$T!ELL 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 H0#yM3OE1ok diff --git a/src/pymonctl/__init__.py b/src/pymonctl/__init__.py index 1ccb854..45f7551 100644 --- a/src/pymonctl/__init__.py +++ b/src/pymonctl/__init__.py @@ -19,7 +19,7 @@ "getMousePos", "version", "Monitor" ] -__version__ = "0.0.10" +__version__ = "0.0.11" def version(numberOnly: bool = True) -> str: diff --git a/src/pymonctl/_display_manager_lib.py b/src/pymonctl/_display_manager_lib.py new file mode 100644 index 0000000..f6c823d --- /dev/null +++ b/src/pymonctl/_display_manager_lib.py @@ -0,0 +1,747 @@ +#!/usr/bin/python3 + +######################################################################## +# Copyright (c) 2018 University of Utah Student Computing Labs. # +# All Rights Reserved. # +# # +# Permission to use, copy, modify, and distribute this software and # +# its documentation for any purpose and without fee is hereby granted, # +# provided that the above copyright notice appears in all copies and # +# that both that copyright notice and this permission notice appear # +# in supporting documentation, and that the name of The University # +# of Utah not be used in advertising or publicity pertaining to # +# distribution of the software without specific, written prior # +# permission. This software is supplied as is without expressed or # +# implied warranties of any kind. # +######################################################################## + +# Display Manager, version 1.0.2 +# Python Library + +# Programmatically manages Mac displays. +# Can set screen resolution, refresh rate, rotation, brightness, underscan, and screen mirroring. + +import sys # make decisions based on system configuration +import warnings # control warning settings for +from abc import abstractmethod, ABCMeta # allows use of abstract classes +from typing import Optional, Dict, Callable + +import objc # type: ignore[import] # access Objective-C functions and variables +import CoreFoundation # type: ignore[import] #work with Objective-C data types +import Quartz # work with system graphics + + +# Configured for global usage; otherwise, must be re-instantiated each time it is called +iokit: Optional[Dict[str, Callable]] = None # type: ignore[type-arg] + + +class DisplayError(Exception): + """ + Raised if a display cannot perform the requested operation (or access the requested property) + (e.g. does not have a matching display mode, display cannot modify this setting, etc.) + """ + pass + + +class AbstractDisplay(object): + """ + Abstract representation which display_manager_lib.Display will inherit from. + + Included for unit testing purposes + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def __init__(self, displayID): + self.displayID = displayID + + # "Magic" methods + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.displayID == other.displayID + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, self.__class__): + return self.displayID != other.displayID + else: + return NotImplemented + + def __lt__(self, other): + return self.displayID < other.displayID + + def __gt__(self, other): + return self.displayID > other.displayID + + def __hash__(self): + # Actually just returns self.displayID, as self.displayID is int; + # hash() is called for consistency and compatibility + return hash(self.displayID) + + # General properties + + @property + @abstractmethod + def isMain(self): + pass + + @property + @abstractmethod + def tag(self): + pass + + # Mode properties and methods + + @property + @abstractmethod + def currentMode(self): + pass + + @property + @abstractmethod + def allModes(self): + pass + + @abstractmethod + def highestMode(self, hidpi): + pass + + @abstractmethod + def closestMode(self, width, height, refresh, hidpi): + pass + + @abstractmethod + def setMode(self, mode): + pass + + # Rotation properties and methods + + @property + @abstractmethod + def rotation(self): + pass + + @abstractmethod + def setRotate(self, angle): + pass + + # Brightness + + @property + @abstractmethod + def brightness(self): + pass + + @abstractmethod + def setBrightness(self, brightness): + pass + + # Underscan + + @property + @abstractmethod + def underscan(self): + pass + + @abstractmethod + def setUnderscan(self, underscan): + pass + + # Mirroring + + @property + @abstractmethod + def mirrorSource(self): + pass + + @abstractmethod + def setMirrorSource(self, mirrorDisplay): + pass + + +class Display(AbstractDisplay): + """ + Virtual representation of a physical display. + + Contains properties regarding display information for a given physical display, along with a few + useful helper functions to configure the display. + """ + + def __init__(self, displayID): + """ + :param displayID: The DisplayID of the display to manipulate + """ + # Make sure displayID is actually a display + (error, allDisplayIDs, count) = Quartz.CGGetOnlineDisplayList(32, None, None) # max 32 displays + if displayID not in allDisplayIDs or error: + raise DisplayError("Display with ID \"{}\" not found".format(displayID)) + + # Sets self.displayID to displayID + super(Display, self).__init__(displayID) + + # iokit is required for several Display methods + getIOKit() + + # General properties + + @property + def tag(self): + """ + :return: The display tag for this Display + """ + if self.isMain: + return "main" + # is external display + else: + # Get all the external displays (in order) + externals = sorted(getAllDisplays()) + for display in externals: + if display.isMain: + externals.remove(display) + break + + for i in range(len(externals)): + if self == externals[i]: + return "ext" + str(i) + + @property + def isMain(self): + """ + :return: Boolean for whether this Display is the main display + """ + return Quartz.CGDisplayIsMain(self.displayID) + + @property + def isHidpi(self): + """ + :return: Whether this display can be set to HiDPI resolutions + """ + # Check if self.allModes has any HiDPI modes + for mode in self.allModes: + if mode.hidpi: + return True + # None of self.allModes were HiDPI + return False + + # Helper methods, properties + + @property + def __servicePort(self): + """ + :return: The integer representing this display's service port. + """ + return Quartz.CGDisplayIOServicePort(self.displayID) + + @staticmethod + def __rightHidpi(mode, hidpi): + """ + Evaluates whether the mode fits the user's HiDPI specification. + + :param mode: The mode to be evaluated. + :param hidpi: HiDPI code. 0 returns everything, 1 returns only non-HiDPI, and 2 returns only HiDPI. + :return: Whether the mode fits the HiDPI description specified by the user. + """ + if ( + (hidpi == 0) # fits HiDPI or non-HiDPI (default) + or (hidpi == 1 and not mode.hidpi) # fits only non-HiDPI + or (hidpi == 2 and mode.hidpi) # fits only HiDPI + ): + return True + else: + return False + + # Mode properties and methods + + @property + def currentMode(self): + """ + :return: The current Quartz "DisplayMode" interface for this display. + """ + return DisplayMode(Quartz.CGDisplayCopyDisplayMode(self.displayID)) + + @property + def defaultMode(self): + for mode in self.allModes: + if mode.isDefault: + return mode + # No default mode was found + return None + + @property + def allModes(self): + """ + :return: All possible Quartz "DisplayMode" interfaces for this display. + """ + # TO-DO: This needs to be revisited + modes = [] + # options forces Quartz to show HiDPI modes + options = {Quartz.kCGDisplayShowDuplicateLowResolutionModes: True} + for modeRef in Quartz.CGDisplayCopyAllDisplayModes(self.displayID, options): + modes.append(DisplayMode(modeRef)) + + # Eliminate all duplicate modes, including any modes that duplicate "default" + uniqueModes = set(modes) + defaultMode = None + # Find default mode + for mode in uniqueModes: + if mode.isDefault: + defaultMode = mode + if defaultMode: + # If there are any duplicates of defaultMode, remove them (and not defaultMode) + for mode in modes: + if all([ + defaultMode.width == mode.width, + defaultMode.height == mode.height, + defaultMode.refresh == mode.refresh, + defaultMode.hidpi == mode.hidpi, + not mode.isDefault, + ]): + try: + uniqueModes.remove(mode) + except KeyError: + pass + + return list(uniqueModes) + + def highestMode(self, hidpi=0): + """ + :param hidpi: HiDPI code. 0 returns everything, 1 returns only non-HiDPI, and 2 returns only HiDPI. + :return: The Quartz "DisplayMode" interface with the highest display resolution for this display. + """ + highest = None + for mode in self.allModes: + if highest: + if mode > highest and self.__rightHidpi(mode, hidpi): + highest = mode + else: # highest hasn't been set yet, so anything is the highest + highest = mode + + if highest: + return highest + else: + if hidpi == 1: + raise DisplayError( + "Display \"{}\" cannot be set to any non-HiDPI resolutions".format(self.tag)) + elif hidpi == 2: + raise DisplayError( + "Display \"{}\" cannot be set to any HiDPI resolutions".format(self.tag)) + else: + raise DisplayError( + "Display \"{}\"\'s resolution cannot be set".format(self.tag)) + + def closestMode(self, width, height, refresh=0, hidpi=0): + """ + :param width: Desired width + :param height: Desired height + :param refresh: Desired refresh rate + :param hidpi: HiDPI code. 0 returns everything, 1 returns only non-HiDPI, and 2 returns only HiDPI + :return: The closest Quartz "DisplayMode" interface possible for this display. + """ + # Which criteria does it match (in addition to width and height)? + both = [] # matches HiDPI and refresh + onlyHidpi = [] # matches HiDPI + onlyRefresh = [] # matches refresh + + for mode in self.allModes: + if mode.width == width and mode.height == height: + if self.__rightHidpi(mode, hidpi) and mode.refresh == refresh: + both.append(mode) + elif self.__rightHidpi(mode, hidpi): + onlyHidpi.append(mode) + elif mode.refresh == refresh: + onlyRefresh.append(mode) + + # Return the nearest match, with HiDPI matches preferred over refresh matches + for modes in [both, onlyHidpi, onlyRefresh]: + if modes: + return modes[0] + + raise DisplayError( + "Display \"{}\" cannot be set to {}x{}".format(self.tag, width, height) + ) + + def setMode(self, mode): + """ + :param mode: The Quartz "DisplayMode" interface to set this display to. + """ + (error, configRef) = Quartz.CGBeginDisplayConfiguration(None) + if error: + raise DisplayError( + "Display \"{}\"\'s resolution cannot be set to {}x{} at {} Hz".format( + self.tag, mode.width, mode.height, mode.refresh)) + + error = Quartz.CGConfigureDisplayWithDisplayMode(configRef, self.displayID, mode.raw, None) + if error: + Quartz.CGCancelDisplayConfiguration(configRef) + raise DisplayError( + "Display \"{}\"\'s resolution cannot be set to {}x{} at {} Hz".format( + self.tag, mode.width, mode.height, mode.refresh)) + + Quartz.CGCompleteDisplayConfiguration(configRef, Quartz.kCGConfigurePermanently) + + # Rotation properties and methods + + @property + def rotation(self): + """ + :return: Rotation of this display, in degrees. + """ + return int(Quartz.CGDisplayRotation(self.displayID)) + + def setRotate(self, angle): + """ + :param angle: The angle of rotation. + """ + # see: https://opensource.apple.com/source/IOGraphics/IOGraphics-406/IOGraphicsFamily/IOKit/graphics/ + # IOGraphicsTypes.h for angle codes (kIOScaleRotate{0, 90, 180, 270}). + # Likewise, see .../IOKit/graphics/IOGraphicsTypesPrivate.h for rotateCode (kIOFBSetTransform) + swapAxes = 0x10 + invertX = 0x20 + invertY = 0x40 + angleCodes = { + 0: 0, + 90: (swapAxes | invertX) << 16, + 180: (invertX | invertY) << 16, + 270: (swapAxes | invertY) << 16, + } + rotateCode = 0x400 + + # If user enters inappropriate angle, we should quit + if angle % 90 != 0: + raise ValueError("Can only rotate by multiples of 90 degrees.") + options = rotateCode | angleCodes[angle % 360] + + # Actually rotate the screen + global iokit + if iokit: + error = iokit["IOServiceRequestProbe"](self.__servicePort, options) + if error: + raise DisplayError("Cannot manage rotation on display \"{}\"".format(self.tag)) + + # Brightness properties and methods + + @property + def brightness(self): + """ + :return: Brightness of this display, from 0 to 1. + """ + global iokit + if iokit: + service = self.__servicePort + (error, brightness) = iokit["IODisplayGetFloatParameter"](service, 0, iokit["kDisplayBrightness"], None) + if error: + return None + else: + return brightness + + def setBrightness(self, brightness): + """ + :param brightness: The desired brightness, from 0 to 1. + """ + global iokit + if iokit: + error = iokit["IODisplaySetFloatParameter"](self.__servicePort, 0, iokit["kDisplayBrightness"], brightness) + if error: + if self.isMain: + raise DisplayError("Cannot manage brightness on display \"{}\"".format(self.tag)) + else: + raise DisplayError( + "Display \"{}\"\'s brightness cannot be set.\n" + "External displays may not be compatible with Display Manager. " + "Try setting manually on device hardware.".format(self.tag)) + + # Underscan properties and methods + + @property + def underscan(self): + """ + :return: Display's active underscan setting, from 1 (0%) to 0 (100%). + (Yes, it doesn't really make sense to have 1 -> 0 and 0 -> 100, but it's how IOKit reports it.) + """ + global iokit + if iokit: + (error, underscan) = iokit["IODisplayGetFloatParameter"]( + self.__servicePort, 0, iokit["kDisplayUnderscan"], None) + if error: + return None + else: + # IOKit handles underscan values as the opposite of what makes sense, so I switch it here. + # e.g. 0 -> maximum (100%), 1 -> 0% (default) + return float(abs(underscan - 1)) + + def setUnderscan(self, underscan): + """ + :param underscan: Underscan value, from 0 (no underscan) to 1 (maximum underscan). + """ + # IOKit handles underscan values as the opposite of what makes sense, so I switch it here. + # e.g. 0 -> maximum (100%), 1 -> 0% (default) + underscan = float(abs(underscan - 1)) + + global iokit + if iokit: + error = iokit["IODisplaySetFloatParameter"](self.__servicePort, 0, iokit["kDisplayUnderscan"], underscan) + if error: + raise DisplayError("Cannot manage underscan on display \"{}\"".format(self.tag)) + + # Mirroring properties and methods + + @property + def mirrorSource(self): + """ + Checks whether self is mirroring another display + :return: The Display that self is mirroring; if self is not mirroring + any display, returns None + """ + # The display which self is mirroring + masterDisplayID = Quartz.CGDisplayMirrorsDisplay(self.displayID) + if masterDisplayID == Quartz.kCGNullDirectDisplay: + # self is not mirroring any display + return None + else: + return Display(masterDisplayID) + + def setMirrorSource(self, mirrorDisplay): + """ + :param mirrorDisplay: The Display which this Display will mirror. + Input a NoneType to stop mirroring. + """ + (error, configRef) = Quartz.CGBeginDisplayConfiguration(None) + if error: + raise DisplayError( + "Display \"{}\" cannot be set to mirror display \"{}\"".format(self.tag, mirrorDisplay.tag)) + + # Will be passed a None mirrorDisplay to disable mirroring. Cannot mirror self. + if mirrorDisplay is None or mirrorDisplay.displayID == self.displayID: + Quartz.CGConfigureDisplayMirrorOfDisplay(configRef, self.displayID, Quartz.kCGNullDirectDisplay) + else: + Quartz.CGConfigureDisplayMirrorOfDisplay(configRef, self.displayID, mirrorDisplay.displayID) + + Quartz.CGCompleteDisplayConfiguration(configRef, Quartz.kCGConfigurePermanently) + + +class AbstractDisplayMode(object): + """ + Abstract representation which display_manager_lib.DisplayMode will inherit from. + + Included for unit testing purposes + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def __init__(self, mode): + self.raw = mode + + # "Magic" methods + + def __eq__(self, other): + if isinstance(other, self.__class__): + return all([ + self.width == other.width, + self.height == other.height, + self.refresh == other.refresh, + self.hidpi == other.hidpi, + self.isDefault == other.isDefault, + ]) + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, self.__class__): + return not self.__eq__(other) + else: + return NotImplemented + + def __lt__(self, other): + return self.width * self.height < other.width * other.height + + def __gt__(self, other): + return self.width * self.height > other.width * other.height + + def __hash__(self): + return hash((self.width, self.height, self.refresh, self.hidpi, self.isDefault)) + + # General properties + + @property + @abstractmethod + def width(self): + pass + + @property + @abstractmethod + def height(self): + pass + + @property + @abstractmethod + def refresh(self): + pass + + @property + @abstractmethod + def hidpi(self): + pass + + @property + @abstractmethod + def isDefault(self): + pass + + +class DisplayMode(AbstractDisplayMode): + """ + Represents a DisplayMode as implemented in Quartz.CoreGraphics + """ + + def __init__(self, mode): + if not isinstance(mode, Quartz.CGDisplayModeRef): + raise DisplayError("\"{}\" is not a valid Quartz.CGDisplayModeRef".format(mode)) + # sets self.raw to mode + super(DisplayMode, self).__init__(mode) + + self.__width = int(Quartz.CGDisplayModeGetWidth(mode)) + self.__height = int(Quartz.CGDisplayModeGetHeight(mode)) + self.__refresh = int(Quartz.CGDisplayModeGetRefreshRate(mode)) + + maxWidth = Quartz.CGDisplayModeGetPixelWidth(mode) # the maximum display width for this display + maxHeight = Quartz.CGDisplayModeGetPixelHeight(mode) # the maximum display width for this display + self.__hidpi = (maxWidth != self.width and maxHeight != self.height) # if they're the same, mode is not HiDPI + + # General properties + + @property + def littleString(self): + return "resolution: {width}x{height}, refresh rate: {refresh}, HiDPI: {hidpi}".format(**{ + "width": self.width, + "height": self.height, + "refresh": self.refresh, + "hidpi": self.hidpi, + }) + + @property + def bigString(self): + return "\n".join([ + "resolution: {}x{}".format(self.width, self.height), + "refresh rate: {}".format(self.refresh), + "HiDPI: {}".format(self.hidpi), + ]) + + @property + def width(self): + return self.__width + + @property + def height(self): + return self.__height + + @property + def refresh(self): + return self.__refresh + + @property + def hidpi(self): + return self.__hidpi + + @property + def isDefault(self): + """ + :return: Whether this DisplayMode is the display's default mode + """ + # CGDisplayModeGetIOFlags returns a hexadecimal number representing the DisplayMode's flags + # the "default" flag is 0x4, which means that said number's binary representation must have + # a '1' in the third-to-last position for it to be the default + return bin(Quartz.CGDisplayModeGetIOFlags(self.raw))[-3] == '1' + + +def getMainDisplay(): + """ + :return: The main Display. + """ + return Display(Quartz.CGMainDisplayID()) + + +def getAllDisplays(): + """ + :return: A list containing all currently-online displays. + """ + (error, displayIDs, count) = Quartz.CGGetOnlineDisplayList(32, None, None) # max 32 displays + if error: + raise DisplayError("Could not retrieve displays list") + + displays = [] + for displayID in displayIDs: + displays.append(Display(displayID)) + return sorted(displays) + + +def getIOKit(): + """ + This handles the importing of specific functions and variables from the + IOKit framework. IOKit is not natively bridged in PyObjC, so the methods + must be found and encoded manually to gain their functionality in Python. + + :return: A dictionary containing several IOKit functions and variables. + """ + global iokit + + # IOKit may have already been instantiated, in which case, nothing needs to be done + if not iokit: + # PyObjC sometimes raises compatibility warnings in macOS 10.14 relating to parts of IOKit that + # Display Manager doesn't use. Thus, such warnings will be temporarily ignored + warnings.simplefilter("ignore") + + # The dictionary which will contain all of the necessary functions and variables from IOKit + iokit = {} + + # Retrieve the IOKit framework + iokitBundle = objc.initFrameworkWrapper( + "IOKit", + frameworkIdentifier="com.apple.iokit", + frameworkPath=objc.pathForFramework("/System/Library/Frameworks/IOKit.framework"), + globals=globals() + ) + + # The IOKit functions to be retrieved + functions = [ + ("IOServiceGetMatchingServices", b"iI@o^I"), + ("IODisplayCreateInfoDictionary", b"@II"), + ("IODisplayGetFloatParameter", b"iII@o^f"), + ("IODisplaySetFloatParameter", b"iII@f"), + ("IOServiceRequestProbe", b"iII"), + ("IOIteratorNext", b"II"), + ] + + # The IOKit variables to be retrieved + # The IOKit variables to be retrieved + variables = [ + ("kIODisplayNoProductName", b"I"), + ("kIOMasterPortDefault", b"I"), + ("kIODisplayOverscanKey", b"*"), + ("kDisplayVendorID", b"*"), + ("kDisplayProductID", b"*"), + ("kDisplaySerialNumber", b"*"), + ] + + # Load functions from IOKit.framework into our iokit + objc.loadBundleFunctions(iokitBundle, iokit, functions) + # Bridge won't put straight into iokit, so globals() + objc.loadBundleVariables(iokitBundle, globals(), variables) + # Move only the desired variables into iokit + for var in variables: + key = "{}".format(var[0]) + if key in globals(): + iokit[key] = globals()[key] + + # A few IOKit variables that have been deprecated, but whose values + # still work as intended in IOKit functions + iokit["kDisplayBrightness"] = CoreFoundation.CFSTR("brightness") + iokit["kDisplayUnderscan"] = CoreFoundation.CFSTR("pscn") + + # Stop ignoring warnings now that we've finished with PyObjC + warnings.simplefilter("default") + + return iokit diff --git a/src/pymonctl/_pymonctl_macos.py b/src/pymonctl/_pymonctl_macos.py index 4ce958e..a2642ce 100644 --- a/src/pymonctl/_pymonctl_macos.py +++ b/src/pymonctl/_pymonctl_macos.py @@ -20,7 +20,7 @@ from pymonctl import BaseMonitor, _pointInBox, _getRelativePosition, \ DisplayMode, ScreenValue, Box, Rect, Point, Size, Position, Orientation -# from ._display_manager_lib import Display +from ._display_manager_lib import Display def _getAllMonitors() -> list[MacOSMonitor]: @@ -196,7 +196,7 @@ def __init__(self, handle: Optional[int] = None): except: # In older macOS, screen doesn't have localizedName() method self.name = "Display" + "_" + str(self.handle) - # self._dm = Display(self.handle) + self._dm = Display(self.handle) else: raise ValueError @@ -308,9 +308,8 @@ def orientation(self) -> Optional[Union[int, Orientation]]: return None def setOrientation(self, orientation: Optional[Union[int, Orientation]]): - # if orientation in (NORMAL, INVERTED, LEFT, RIGHT): - # self._dm.setRotate(orientation * 90) - pass + if orientation in (Orientation.NORMAL, Orientation.INVERTED, Orientation.LEFT, Orientation.RIGHT): + self._dm.setRotate(orientation * 90) @property def frequency(self) -> Optional[float]: @@ -324,8 +323,7 @@ def colordepth(self) -> Optional[int]: @property def brightness(self) -> Optional[int]: - # return self._dm.brightness - return None + return self._dm.brightness # https://stackoverflow.com/questions/46885603/is-there-a-programmatic-way-to-check-if-brightness-is-at-max-or-min-value-on-osx # value = None # cmd = """nvram backlight-level | awk '{print $2}'""" @@ -335,11 +333,10 @@ def brightness(self) -> Optional[int]: # return value def setBrightness(self, brightness: Optional[int]): - # try: - # self._dm.setBrightness(brightness) - # except: - # pass - pass + try: + self._dm.setBrightness(brightness) + except: + pass # https://github.com/thevickypedia/pybrightness/blob/main/pybrightness/controller.py # https://eastmanreference.com/complete-list-of-applescript-key-codes # for _ in range(32):