diff --git a/CHANGES.txt b/CHANGES.txt index cc189c1..1f917bc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/README.md b/README.md index 4d8bd39..b634915 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ The watchdog will automatically stop when window doesn't exist anymore or progra | start | | updateCallbacks | | updateInterval | +| setTryToFind | | isAlive | | stop | @@ -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 @@ -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 diff --git a/dist/PyWinCtl-0.0.34-py3-none-any.whl b/dist/PyWinCtl-0.0.34-py3-none-any.whl index 961ab08..428a63e 100644 Binary files a/dist/PyWinCtl-0.0.34-py3-none-any.whl and b/dist/PyWinCtl-0.0.34-py3-none-any.whl differ diff --git a/src/pywinctl/__init__.py b/src/pywinctl/__init__.py index e204127..63df614 100644 --- a/src/pywinctl/__init__.py +++ b/src/pywinctl/__init__.py @@ -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") @@ -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 @@ -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 @@ -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() diff --git a/src/pywinctl/_pywinctl_linux.py b/src/pywinctl/_pywinctl_linux.py index 34c1103..e30f38f 100644 --- a/src/pywinctl/_pywinctl_linux.py +++ b/src/pywinctl/_pywinctl_linux.py @@ -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(): @@ -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 @@ -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": @@ -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 diff --git a/src/pywinctl/_pywinctl_macos.py b/src/pywinctl/_pywinctl_macos.py index 6d558c6..9a056c7 100644 --- a/src/pywinctl/_pywinctl_macos.py +++ b/src/pywinctl/_pywinctl_macos.py @@ -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: @@ -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) @@ -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. @@ -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: @@ -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 @@ -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 | @@ -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 @@ -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": @@ -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, @@ -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) diff --git a/src/pywinctl/_pywinctl_win.py b/src/pywinctl/_pywinctl_win.py index a5758fe..1bdf4f8 100644 --- a/src/pywinctl/_pywinctl_win.py +++ b/src/pywinctl/_pywinctl_win.py @@ -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 @@ -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": @@ -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,