Skip to content

Commit

Permalink
New approach based on Monitor() class to access all properties and fu…
Browse files Browse the repository at this point in the history
…nctionalities. macOS is still experimental and not tested on multi-monitor setups.
  • Loading branch information
Kalmat committed Jun 16, 2023
1 parent 68f8702 commit 363f14d
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 212 deletions.
73 changes: 40 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Functions to get monitor instances, get info and manage monitors plugged to the
| getMonitorsCount |
| getPrimary |
| findMonitor |
| findMonitorInfo |
| arrangeMonitors |
| getMousePos |

Expand All @@ -30,42 +31,48 @@ 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.

| Methods | getter | setter | action | Windows | Linux | macOS |
|:--------------:|:------:|:------:|:------:|:-------:|:-----:|:-----:|
| size | X | | | X | X | X |
| workarea | X | | | X | X | X |
| position | X | | | X | X | X |
| setPosition | | X | | X | X | X |
| box | X | | | X | X | X |
| rect | X | | | X | X | X |
| scale | X | | | X | X | X |
| dpi | X | | | X | X | X |
| orientation | X | X | | X | X | X |
| frequency | X | | | X | X | X |
| colordepth | X | | | X | X | X |
| brightness | X | X | | X | X | X |
| contrast | X | X | | X | X | X |
| mode | X | X | | X | X | X |
| defaultMode | X | | | X | X | X |
| setDefaultMode | | X | | X | X | X |
| allModes | X | | | X | X | X |
| isPrimary | X | | | X | X | X |
| setPrimary | | X | | X | X | X |
| turnOn | | | X | X | X | X |
| turnOff | | | X | X (1) | X | |
| suspend | | | X | X (1) | X (2) | X (2) |
| isOn | X | | | X | X | |
| attach | | | X | X | | |
| detach | | | X | X | | |
| isAttached | X | | | X | X | X |

(1) 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 | |
| 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 | X |
| turnOff | X (2) | X | |
| suspend | X (2) | X (3) | X (3) |
| isOn | X | X | |
| attach | X | | |
| detach | 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,
so ALL monitors will be turned off / suspended.
To address a specific monitor, try using detach() method.
(2) It will suspend ALL monitors
To address a specific monitor, try using detach() / attach() methods.
(3) It will suspend ALL monitors.


#### WARNING: Most of these getters may return ''None'' in case the value can not be obtained
#### WARNING: Most of these properties may return ''None'' in case the value can not be obtained

## Keep Monitors info updated

Expand Down
Binary file modified dist/PyMonCtl-0.0.9-py3-none-any.whl
Binary file not shown.
68 changes: 44 additions & 24 deletions src/ewmhlib/_ewmhlib.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import ctypes
import sys
assert sys.platform == "linux"

Expand Down Expand Up @@ -154,23 +155,22 @@ def getDisplayFromRoot(rootId: int) -> Tuple[Xlib.display.Display, Struct, XWind


def getProperty(window: XWindow, prop: Union[str, int, Root, Window],
prop_type: int = Xlib.X.AnyPropertyType, sizehint: int = 10,
prop_type: int = Xlib.X.AnyPropertyType,
display: Xlib.display.Display = defaultDisplay) -> Optional[Xlib.protocol.request.GetProperty]:
"""
Get given window/root property
:param window: window from which get the property
:param prop: property to retrieve as int or str (will be translated to int)
:param prop_type: property type (e.g. Xlib.X.AnyPropertyType or Xlib.Xatom.ATOM)
:param sizehint: Expected data length hint (defaults to 10)
:param display: display to which window belongs to (defaults to default display)
:return: Xlib.protocol.request.GetProperty struct or None (property couldn't be obtained)
"""
if isinstance(prop, str):
prop = display.get_atom(prop)

if isinstance(prop, int) and prop != 0:
return window.get_full_property(prop, prop_type, sizehint)
return window.get_full_property(prop, prop_type, 10)
return None


Expand All @@ -193,11 +193,11 @@ def changeProperty(window: XWindow, prop: Union[str, int, Root, Window], data: U
if isinstance(prop, int) and prop != 0:
# I think (to be confirmed) that 16 is not used in Python (no difference between short and long int)
if isinstance(data, str):
dataFormat: int = 8
dataFormat: int = DataFormat.STR
data = data.encode(encoding="utf-8")
else:
data = (data + [0] * (5 - len(data)))[:5]
dataFormat = 32
dataFormat = DataFormat.INT

window.change_property(prop, prop_type, dataFormat, data, propMode)
display.flush()
Expand All @@ -220,10 +220,10 @@ def sendMessage(winId: int, prop: Union[str, int, Root, Window], data: Union[Lis
if isinstance(prop, int) and prop != 0:
# I think (to be confirmed) that 16 is not used in Python (no difference between short and long int)
if isinstance(data, str):
dataFormat: int = 8
dataFormat: int = DataFormat.STR
else:
data = (data + [0] * (5 - len(data)))[:5]
dataFormat = 32
dataFormat = DataFormat.INT

ev: Xlib.protocol.event.ClientMessage = Xlib.protocol.event.ClientMessage(window=winId, client_type=prop,
data=(dataFormat, data))
Expand Down Expand Up @@ -302,19 +302,18 @@ def __init__(self, root: Optional[XWindow] = None):
self.wmProtocols = self._WmProtocols(self.display, self.root)

def getProperty(self, prop: Union[str, int, Root, Window],
prop_type: int = Xlib.X.AnyPropertyType, sizehint: int = 10) \
prop_type: int = Xlib.X.AnyPropertyType) \
-> Optional[Xlib.protocol.request.GetProperty]:
"""
Retrieves given property from root
:param prop: Property to query (int or str format)
:param prop_type: Property type (e.g. X.AnyPropertyType or Xatom.STRING)
:param sizehint: Expected data length
:return: List of int, List of str or None (nothing obtained)
"""
if isinstance(prop, str):
prop = self.display.get_atom(prop)
return getProperty(self.root, prop, prop_type, sizehint)
return getProperty(self.root, prop, prop_type, self.display)

def setProperty(self, prop: Union[str, int, Root, Window], data: Union[List[int], str]):
"""
Expand Down Expand Up @@ -602,7 +601,7 @@ def getDesktopLayout(self) -> Optional[List[int]]:
res = cast(List[int], res)
return res

def setDesktopLayout(self, orientation: int, columns: int, rows: int, starting_corner: int):
def setDesktopLayout(self, orientation: Union[int, DesktopLayout], columns: int, rows: int, starting_corner: Union[int, DesktopLayout]):
"""
Values (as per RootWindow.DesktopLayout):
_NET_WM_ORIENTATION_HORZ 0
Expand Down Expand Up @@ -1019,19 +1018,18 @@ def __init__(self, winId: int, root: XWindow = defaultRoot):

self._currDesktop = os.environ['XDG_CURRENT_DESKTOP'].lower()

def getProperty(self, prop: Union[str, int, Root, Window], prop_type: int = Xlib.X.AnyPropertyType, sizehint: int = 10) \
def getProperty(self, prop: Union[str, int, Root, Window], prop_type: int = Xlib.X.AnyPropertyType) \
-> Optional[Xlib.protocol.request.GetProperty]:
"""
Retrieves given property data from given window
:param prop: Property to query (int or str format)
:param prop_type: Property type (e.g. X.AnyPropertyType or Xatom.STRING)
:param sizehint: Expected data length
:return: List of int, List of str or None (nothing obtained)
"""
if isinstance(prop, str):
prop = self.display.get_atom(prop)
return getProperty(self.xWindow, prop, prop_type, sizehint)
return getProperty(self.xWindow, prop, prop_type, self.display)

def sendMessage(self, prop: Union[str, int, Root, Window], data: Union[List[int], str]):
"""
Expand Down Expand Up @@ -1233,7 +1231,7 @@ def getWmWindowType(self, text: bool = False) -> Optional[Union[List[int], List[
"""
return getPropertyValue(self.getProperty(Window.WM_WINDOW_TYPE), text, self.display)

def setWmWindowType(self, winType: WindowType):
def setWmWindowType(self, winType: Union[str, WindowType]):
"""
Changes the type of current window.
Expand Down Expand Up @@ -1325,7 +1323,7 @@ def getWmState(self, text: bool = False) -> Optional[Union[List[int], List[str]]
"""
return getPropertyValue(self.getProperty(Window.WM_STATE), text, self.display)

def changeWmState(self, action: StateAction, state: State, state2: State = State.NULL, userAction: bool = True):
def changeWmState(self, action: StateAction, state: Union[str, State], state2: Union[str, State] = State.NULL, userAction: bool = True):
"""
Sets the window states values of current window.
Expand Down Expand Up @@ -1367,28 +1365,30 @@ def setMaximized(self, maxHorz: bool, maxVert: bool):
if State.MAXIMIZED_VERT not in states:
state2 = State.MAXIMIZED_VERT
if state1 or state2:
self.changeWmState(StateAction.ADD, state1 if state1 != NULL else state2, state2 if state1 != NULL else NULL)
self.changeWmState(StateAction.ADD, cast(State, state1) if state1 != NULL else cast(State, state2),
cast(State, state2) if state1 != NULL else cast(State, NULL))
elif maxHorz:
if State.MAXIMIZED_HORZ not in states:
state = State.MAXIMIZED_HORZ
self.changeWmState(StateAction.ADD, state, NULL)
self.changeWmState(StateAction.ADD, cast(State, state), cast(State, NULL))
if State.MAXIMIZED_VERT in states:
state = State.MAXIMIZED_VERT
self.changeWmState(StateAction.REMOVE, state, NULL)
self.changeWmState(StateAction.REMOVE, cast(State, state), cast(State, NULL))
elif maxVert:
if State.MAXIMIZED_HORZ in states:
state = State.MAXIMIZED_HORZ
self.changeWmState(StateAction.REMOVE, state, NULL)
self.changeWmState(StateAction.REMOVE, cast(State, state), cast(State, NULL))
if State.MAXIMIZED_VERT not in states:
state = State.MAXIMIZED_VERT
self.changeWmState(StateAction.ADD, state, NULL)
self.changeWmState(StateAction.ADD, cast(State, state), cast(State, NULL))
else:
if State.MAXIMIZED_HORZ in states:
state1 = State.MAXIMIZED_HORZ
if State.MAXIMIZED_VERT in states:
state2 = State.MAXIMIZED_VERT
if state1 or state2:
self.changeWmState(StateAction.REMOVE, state1 if state1 != NULL else state2, state2 if state1 != NULL else NULL)
self.changeWmState(StateAction.REMOVE, cast(State, state1) if state1 != NULL else cast(State, state2),
cast(State, state2) if state1 != NULL else cast(State, NULL))

def setMinimized(self):
"""
Expand Down Expand Up @@ -1841,7 +1841,7 @@ def setMoveResize(self, gravity: int = 0, x: Optional[int] = None, y: Optional[i
gravity_flags = gravity_flags | (1 << 13)
self.sendMessage(Root.MOVERESIZE, [gravity_flags, x, y, width, height])

def setWmMoveResize(self, x_root: int, y_root: int, orientation: int, button: int, userAction: bool = True):
def setWmMoveResize(self, x_root: int, y_root: int, orientation: Union[int, MoveResize], button: int, userAction: bool = True):
"""
This message allows Clients to initiate window movement or resizing. They can define their own move and size
"grips", whilst letting the Window Manager control the actual operation. This means that all moves/resizes
Expand Down Expand Up @@ -2431,6 +2431,7 @@ def __init__(self, winId: int, display: Xlib.display.Display, root: XWindow):
self._stopRequested: bool = False
self._checkThread: Optional[threading.Thread] = None
self._threadStarted: bool = False
self._interval = 0.1

# self._isCinnamon = "cinnamon" in os.environ['XDG_CURRENT_DESKTOP'].lower()

Expand All @@ -2451,7 +2452,7 @@ def _checkDisplayEvents(self):
self._callback(event)
break
i -= 1
time.sleep(0.1)
time.sleep(self._interval)

# Is this necessary to somehow "free" the events catching???
self._root.change_attributes(event_mask=Xlib.X.NoEventMask)
Expand Down Expand Up @@ -2555,11 +2556,30 @@ def stop(self):
Start a new watchdog using start() again.
"""
if self._threadStarted and self._checkThread is not None:
timer = threading.Timer(self._interval * 2, self._forceStop)
timer.start()
self._threadStarted = False
self._stopRequested = True
self._keep.set()
self._checkThread.join()
self._checkThread = None
timer.cancel()

def _getTid(self):
if self._checkThread and self._checkThread.is_alive():
if hasattr(self._checkThread, '_thread_id'):
return self._checkThread._thread_id
for id, thread in threading._active.items():
if thread is self._checkThread:
return id
return None

def _forceStop(self):
thread_id = self._getTid()
if thread_id is not None:
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit))
if res > 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)


def _getWindowParent(win: XWindow, rootId: int) -> int:
Expand Down
Loading

0 comments on commit 363f14d

Please sign in to comment.