Skip to content

Commit

Permalink
macOS Apple Script version: Fixed getWindowsWithTitle()
Browse files Browse the repository at this point in the history
  • Loading branch information
Kalmat committed Apr 18, 2022
1 parent 50416a9 commit 3fcab57
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 37 deletions.
2 changes: 1 addition & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
0.0.34, 2022/04/06 -- Added DIFFRATIO (difflib similarity ratio) to getWindowsWithTitle() and getAppsWithName(). Linux: fixed getAllScreens() for LXDE. macOS (Apple Script version): Added updatedTitle property, improved watchdog to detect title changes and fixed isAlive property
0.0.34, 2022/04/06 -- Added DIFFRATIO (difflib similarity ratio) to getWindowsWithTitle() and getAppsWithName(). Linux: fixed getAllScreens() for LXDE. macOS (Apple Script version): Fixed getWindowsWithTitle() and added updatedTitle property, improved watchdog to detect title changes and fixed isAlive property
0.0.33, 2022/04/04 -- Added getAppsWithName() function with regex-like options to search app names. Added param to getWindowsWithTitle() used to define app names in which search window titles
0.0.32, 2022/03/29 -- Added WinWatchDog class to hook to some window changes notifications. Added regex-like search options in getWindowsWithTitle() function. Fixed getMenu() method for menus with 5+ levels.
0.0.31, 2022/03/27 -- Added getExtraFrame(), getClientFrame() methods and isAlive property. Fixed isVisible and getAllScreens() for older macOS
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ The watchdog will automatically stop when window doesn't exist anymore or progra
| start |
| updateCallbacks |
| updateInterval |
| setTryToFind |
| isAlive |
| stop |

Expand All @@ -120,6 +121,7 @@ Example:

npw = pwc.getActiveWindow()
npw.watchdog.start(isActiveCB=activeCB)
npw.watchdog.setTryToFind(True)
print("toggle focus and move active window")
print("Press Ctl-C to Quit")
i = 0
Expand All @@ -129,6 +131,7 @@ Example:
npw.watchdog.updateCallbacks(isActiveCB=activeCB, movedCB=movedCB)
if i == 100:
npw.watchdog.updateInterval(0.1)
npw.setTryToFind(False)
time.sleep(0.1)
except KeyboardInterrupt:
break
Expand Down
Binary file modified dist/PyWinCtl-0.0.34-py3-none-any.whl
Binary file not shown.
36 changes: 16 additions & 20 deletions src/pywinctl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
# PyWinCtl
# A cross-platform module to get info on and control windows on screen

# pywin32 on Windows
# pyobjc (AppKit and Quartz) on macOS
# Xlib and ewmh on Linux

#!/usr/bin/python
# -*- coding: utf-8 -*-

__version__ = "0.0.34"

import collections
import difflib

import numpy as np
import re
import sys
import threading
from typing import Tuple, List

import numpy as np
import pyrect

Rect = collections.namedtuple("Rect", "left top right bottom")
Expand Down Expand Up @@ -496,6 +490,7 @@ def __init__(self, win: BaseWindow, isAliveCB=None, isActiveCB=None, isVisibleCB
threading.Thread.__init__(self)
self._win = win
self._interval = interval
self._tryToFind = False
self._kill = threading.Event()

self._isAliveCB = isAliveCB
Expand Down Expand Up @@ -552,24 +547,21 @@ def run(self):
self._kill.wait(self._interval)

try:
if self._isAliveCB or type(self._win).__name__ == MacOSWindow.__name__:
if self._isAliveCB or self._tryToFind:
# In macOS AppScript version, if title changes, it will consider window is not alive anymore
if not self._win.isAlive:
if self._isAliveCB:
self._isAliveCB(False)
if type(self._win).__name__ == MacOSWindow.__name__:
if self._tryToFind:
title = self._win.title
if self._title != title:
try:
title = self._win.updatedTitle
except NotImplementedError:
pass
if self._changedTitleCB is not None:
# In macOS AppScript version, watchdog will try to find a similar window title within same app
# and will pass it to the callback. However, the watchdog will stop!
title = self._win.updatedTitle
self._title = title
if self._changedTitleCB:
self._changedTitleCB(title)
self.kill()
break
if not self._tryToFind or (self._tryToFind and not self._title):
self.kill()
break

if self._isActiveCB:
active = self._win.isActive
Expand Down Expand Up @@ -641,6 +633,10 @@ def updateCallbacks(self, isAliveCB=None, isActiveCB=None, isVisibleCB=None, isM
def updateInterval(self, interval=0.3):
self._interval = interval

def setTryToFind(self, tryToFind):
if type(self._win).__name__ == MacOSWindow.__name__:
self._tryToFind = tryToFind

def kill(self):
self._kill.set()

Expand Down
22 changes: 18 additions & 4 deletions src/pywinctl/_pywinctl_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def getWindowsWithTitle(title, app=(), condition=Re.IS, flags=0):
title = re.compile(title, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO) and (not isinstance(flags, int) or not (0 < flags <= 100)):
flags = 90
elif flags == re.IGNORECASE:
elif flags == Re.IGNORECASE:
lower = True
title = title.lower()
for win in getAllWindows():
Expand Down Expand Up @@ -1000,6 +1000,21 @@ def updateInterval(self, interval=0.3):
else:
self._watchdog = None

def setTryToFind(self, tryToFind: bool):
"""
In macOS Apple Script version, if set to ''True'' and in case title changes, watchdog will try to find
a similar title within same application to continue monitoring it. It will stop if set to ''False'' or
similar title not found.
IMPORTANT:
- It will have no effect in other platforms (Windows and Linux) and classes (MacOSNSWindow)
- This behavior is deactivated by default, so you need to explicitly activate it
:param tryToFind: set to ''True'' to try to find a similar title. Set to ''False'' to deactivate this behavior
"""
pass

def stop(self):
"""
Stop the entire WatchDog and all its hooks
Expand Down Expand Up @@ -1045,7 +1060,7 @@ def getAllScreens():
"workarea":
Rect(left, top, right, bottom) struct with the screen workarea, in pixels
"scale":
Scale ratio, as a percentage
Scale ratio, as a tuple of (x, y) scale percentage
"dpi":
Dots per inch, as a tuple of (x, y) dpi values
"orientation":
Expand Down Expand Up @@ -1081,9 +1096,8 @@ def getAllScreens():
x, y, w, h = crtc.x, crtc.y, crtc.width, crtc.height
wx, wy, wr, wb = x + wa[0], y + wa[1], x + w - (screen.width_in_pixels - wa[2] - wa[0]), y + h - (screen.height_in_pixels - wa[3] - wa[1])
# check all these values with physical monitors using dpi, mms or other possible values or props
# dpiX, dpiY = round(crtc.width * 25.4 / params.mm_width), round(crtc.height * 25.4 / params.mm_height)
dpiX, dpiY = round(w * 25.4 / screen.width_in_mms), round(h * 25.4 / screen.height_in_mms)
# 'dpi' = (round(SCREEN.width_in_pixels * 25.4 / SCREEN.width_in_mms), round(SCREEN.height_in_pixels * 25.4 / SCREEN.height_in_mms)),
# 'dpi' = (round(crtc.width * 25.4 / params.mm_width), round(crtc.height * 25.4 / params.mm_height)),
scaleX, scaleY = round(dpiX / 96 * 100), round(dpiY / 96 * 100)
rot = int(math.log(crtc.rotation, 2))
freq = 0
Expand Down
61 changes: 51 additions & 10 deletions src/pywinctl/_pywinctl_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ def getWindowsWithTitle(title, app=(), condition=Re.IS, flags=0):
title = re.compile(title, flags)
elif condition in (Re.EDITDISTANCE, Re.DIFFRATIO) and (not isinstance(flags, int) or not (0 < flags <= 100)):
flags = 90
elif flags == re.IGNORECASE:
elif flags == Re.IGNORECASE:
lower = True
title = title.lower()
if not app:
if not app or (app and isinstance(app, tuple)):
activeApps = _getAllApps()
titleList = _getWindowTitles()
for item in titleList:
Expand All @@ -172,8 +172,8 @@ def getWindowsWithTitle(title, app=(), condition=Re.IS, flags=0):
x, y, w, h = int(item[2][0]), int(item[2][1]), int(item[3][0]), int(item[3][1])
rect = Rect(x, y, x + w, y + h)
for a in activeApps:
if a.processIdentifier() == pID and (not app or (app and app.localizedName() in app)):
matches.append(MacOSWindow(app, item[1], rect))
if (not app and a.processIdentifier() == pID) or a.localizedName() in app:
matches.append(MacOSWindow(a, item[1], rect))
break
else:
windows = getAllWindows(app)
Expand Down Expand Up @@ -1057,7 +1057,7 @@ def title(self) -> Union[str, None]:
@property
def updatedTitle(self) -> str:
"""
Get and update title by finding a similar window title within same application.
Get and updated title by finding a similar window title within same application.
It uses a similarity check to find the best match in case title changes (no way to effectively detect it).
This can be useful since this class uses window title to identify the target window.
If watchdog is activated, it will stop in case title changes.
Expand All @@ -1067,7 +1067,7 @@ def updatedTitle(self) -> str:
- New title may not belong to the original target window, it is just similar within same application
- If original title or a similar one is not found, window may still exist
:return: possible new title or same title if it didn't change, as a string
:return: possible new title, empty if no similar title found or same title if it didn't change, as a string
"""
titles = _getAppWindowsTitles(self._appName)
if self._winTitle not in titles:
Expand Down Expand Up @@ -1225,6 +1225,24 @@ def updateInterval(self, interval=0.3):
else:
self._watchdog = None

def setTryToFind(self, tryToFind: bool):
"""
In macOS Apple Script version, if set to ''True'' and in case title changes, watchdog will try to find
a similar title within same application to continue monitoring it. It will stop if set to ''False'' or
similar title not found.
IMPORTANT:
- It will have no effect in other platforms (Windows and Linux) and classes (MacOSNSWindow)
- This behavior is deactivated by default, so you need to explicitly activate it
:param tryToFind: set to ''True'' to try to find a similar title. Set to ''False'' to deactivate this behavior
"""
if self._watchdog and self.isAlive():
self._watchdog.setTryToFind(tryToFind)
else:
self._watchdog = None

def stop(self):
"""
Stop the entire WatchDog and all its hooks
Expand Down Expand Up @@ -2066,8 +2084,8 @@ def sendBehind(self, sb: bool = True) -> bool:
if sb:
ret1 = self._hWnd.setLevel_(Quartz.kCGDesktopWindowLevel - 1)
ret2 = self._hWnd.setCollectionBehavior_(Quartz.NSWindowCollectionBehaviorCanJoinAllSpaces |
Quartz.NSWindowCollectionBehaviorStationary |
Quartz.NSWindowCollectionBehaviorIgnoresCycle)
Quartz.NSWindowCollectionBehaviorStationary |
Quartz.NSWindowCollectionBehaviorIgnoresCycle)
else:
ret1 = self._hWnd.setLevel_(Quartz.kCGNormalWindowLevel)
ret2 = self._hWnd.setCollectionBehavior_(Quartz.NSWindowCollectionBehaviorDefault |
Expand Down Expand Up @@ -2314,6 +2332,21 @@ def updateInterval(self, interval=0.3):
else:
self._watchdog = None

def setTryToFind(self, tryToFind: bool):
"""
In macOS Apple Script version, if set to ''True'' and in case title changes, watchdog will try to find
a similar title within same application to continue monitoring it. It will stop if set to ''False'' or
similar title not found.
IMPORTANT:
- It will have no effect in other platforms (Windows and Linux) and classes (MacOSNSWindow)
- This behavior is deactivated by default, so you need to explicitly activate it
:param tryToFind: set to ''True'' to try to find a similar title. Set to ''False'' to deactivate this behavior
"""
pass

def stop(self):
"""
Stop the entire WatchDog and all its hooks
Expand Down Expand Up @@ -2379,7 +2412,7 @@ def getAllScreens():
"workarea":
Rect(left, top, right, bottom) struct with the screen workarea, in pixels
"scale":
Scale ratio, as a percentage
Scale ratio, as a tuple of (x, y) scale percentage
"dpi":
Dots per inch, as a tuple of (x, y) dpi values
"orientation":
Expand Down Expand Up @@ -2417,7 +2450,7 @@ def getAllScreens():
'pos': Point(x, y),
'size': Size(w, h),
'workarea': Rect(wx, wy, wr, wb),
'scale': scale,
'scale': (scale, scale),
'dpi': (dpiX, dpiY),
'orientation': rot,
'frequency': freq,
Expand Down Expand Up @@ -2493,6 +2526,14 @@ def displayWindowsUnderMouse(xOffset: int = 0, yOffset: int = 0) -> None:
sys.stdout.flush()


def isAliveCB(alive):
print("ALIVE", alive)


def changedTitleCB(title):
print("TITLE", title)


def main():
"""Run this script from command-line to get windows under mouse pointer"""
print("PLATFORM:", sys.platform)
Expand Down
19 changes: 17 additions & 2 deletions src/pywinctl/_pywinctl_win.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,21 @@ def updateInterval(self, interval=0.3):
else:
self._watchdog = None

def setTryToFind(self, tryToFind: bool):
"""
In macOS Apple Script version, if set to ''True'' and in case title changes, watchdog will try to find
a similar title within same application to continue monitoring it. It will stop if set to ''False'' or
similar title not found.
IMPORTANT:
- It will have no effect in other platforms (Windows and Linux) and classes (MacOSNSWindow)
- This behavior is deactivated by default, so you need to explicitly activate it
:param tryToFind: set to ''True'' to try to find a similar title. Set to ''False'' to deactivate this behavior
"""
pass

def stop(self):
"""
Stop the entire WatchDog and all its hooks
Expand Down Expand Up @@ -1152,7 +1167,7 @@ def getAllScreens() -> dict:
"workarea":
Rect(left, top, right, bottom) struct with the screen workarea, in pixels
"scale":
Scale ratio, as a percentage
Scale ratio, as a tuple of (x, y) scale percentage
"dpi":
Dots per inch, as a tuple of (x, y) dpi values
"orientation":
Expand Down Expand Up @@ -1211,7 +1226,7 @@ def getAllScreens() -> dict:
"pos": Point(x, y),
"size": Size(r - x, b - y),
"workarea": Rect(wx, wy, wr, wb),
"scale": scale,
"scale": (scale, scale),
"dpi": (dpiX.value, dpiY.value),
"orientation": rot,
"frequency": freq,
Expand Down

0 comments on commit 3fcab57

Please sign in to comment.