Closed super-ibby closed 1 year ago
Please let me know if this works on your end. It seems to work fine when called from another module or a Jupyter notebook. However, on my end, running the
_pywinctl_macos.py
file as a script yields the following error:Traceback (most recent call last): File "/Users/ibby/Developer/PyWinCtl/src/pywinctl/_pywinctl_macos.py", line 2672, in <module> main() File "/Users/ibby/Developer/PyWinCtl/src/pywinctl/_pywinctl_macos.py", line 2662, in main npw = getActiveWindow() File "/Users/ibby/Developer/PyWinCtl/src/pywinctl/_pywinctl_macos.py", line 104, in getActiveWindow return MacOSWindow(active_app, title=active_window_info['kCGWindowName'], bounds=bounds) TypeError: Can't instantiate abstract class MacOSWindow with abstract method isAlerting
I'm not sure why this is happening on your side. Most likely it's my fault since I messed with dev
branch (I am a total newbie with git, github, branches, ... sorry). If you check current master version (or dev), isAlerting() method is commented everywhere, so it shouldn't complain because it is present or not. I guess the solution can be to install pywinctl-0.043 and download the proper release (v0.0.43)... not sure (hopefuly yes!). Apart from this, I suggest you use dev branch so we can better manage all pre-release changes, if you agree, of course.
What the hell?!?! I'm sure I tested your WindowDelegate
solution in Catalina. It was pending to test in other versions, so I have started to test it in Yosemite and got this error. I then tested again in Catalina... but I get this very same error!
Traceback (most recent call last):
File "/Volumes/Proyectos/PycharmProjects/PyWinCtl-dev/src/pywinctl/_pywinctl_macos.py", line 488, in <module>
class WindowDelegate(AppKit.NSObject): # Cannot put into a closure as subsequent calls will cause a re-registration error due to subclassing NSObject.
objc.error: WindowDelegate is overriding existing Objective-C class
Don't know what is happening, to be honest.
EDIT:
Mistery solved. If you run _pwinctl_macos.py
script directly, it loads pywinctl module twice, so it complains because WindowDelegate
is already registered. This doesn't happen if you run it as a module. Most likely, when I first tested in Catalina, I didn't update the new module version, but just ran _pwinctl_macos.py
directly. Since the existing pywinctl
module version had no reference of WindowDelegate
, it didn't fail. The question now is how to solve this... I will check!
EDIT 2:
Doing this, it seems to work running the script directly as python3 _pywinctl_macos.py
and within a separate script as import pywinctl
. I have included two calls to getClientFrame()
in test_MacOSNSWindow.py
, and it doesn't fail, but you have a comment suggesting it's not the right way, correct?:
def getClientFrame(self):
"""
Get the client area of window including scroll, menu and status bars, as a Rect (x, y, right, bottom)
Notice that this method won't match non-standard window decoration style sizes
:return: Rect struct
WARNING: it will fail if not called within main thread
"""
class WindowDelegate(AppKit.NSObject): # Cannot put into a closure as subsequent calls will cause a re-registration error due to subclassing NSObject.
"""Helps run window operations on the main thread."""
print("IN", registered)
results: Dict[bytes, Any] = {} # Store results here. Not ideal, but may be better than using a global.
registered = True
print("IN", registered)
@staticmethod
def run_on_main_thread(selector: bytes, obj: Optional[Any] = None, wait: Optional[bool] = True) -> Any:
"""Runs a method of this object on the main thread."""
WindowDelegate.alloc().performSelectorOnMainThread_withObject_waitUntilDone_(selector, obj, wait)
return WindowDelegate.results.get(selector)
def getTitleBarHeightAndBorderWidth(self) -> None:
"""Updates results with title bar height and border width."""
frame_width = 100
window = AppKit.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
((0, 0), (frame_width, 100)),
AppKit.NSTitledWindowMask,
AppKit.NSBackingStoreBuffered,
False,
)
titlebar_height = int(window.titlebarHeight())
# print(titlebar_height)
content_rect = window.contentRectForFrameRect_(window.frame())
x1 = AppKit.NSMinX(content_rect)
x2 = AppKit.NSMaxX(content_rect)
border_width = int(frame_width - (x2 - x1))
# print(border_width)
result = titlebar_height, border_width
WindowDelegate.results[b'getTitleBarHeightAndBorderWidth'] = result
# https://www.macscripter.net/viewtopic.php?id=46336 --> Won't allow access to NSWindow objects, but interesting
# Didn't find a way to get menu bar height using Apple Script
# titleHeight, borderWidth = _getBorderSizes()
titleHeight, borderWidth = WindowDelegate.run_on_main_thread(b'getTitleBarHeightAndBorderWidth')
res = Rect(int(self.left + borderWidth), int(self.top + titleHeight), int(self.right - borderWidth), int(self.bottom - borderWidth))
return res
Using Quartz was my first approach, with no AppleScript at all. The problems started when I began to test in Catalina and above... Look at this
Without the title, I found no option to handle windows (move, resize, ...). In addition to the window title (not present in most cases), kCGWindowNumber doesn't match any other value you can get using AppleScript or AppKit or whatever. Furthermore, again after Catalina, window id (which could be another possible potential stable, unique key to identify a window) was removed from AppleScript and returning nothing...
Besides, the objects retrieved using Quartz take a "lot" of time to be refreshed (perhaps it's because I'm using a VM... not sure).
Kind of a hell. I had to refactor the whole module and start using AppleScript, and the window title as "handle"... not the best solution in deed.
In short, and sorry for the long comment, Quartz came out not to be a proper solution (or at least MY Quartz wasn't).
Regarding the change you suggest, I'm affraid that kCGWindowName will not be available in most cases, making it unusable...
Finally, and just for information, using this:
on_screen_window_info_list = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID)
returns a lot of "system" windows which are not the usual target of the module. In addition to that, windows should be filtered using
kCGWindowLayer == 0
This is very useful background. Thank you.
What the hell?!?! I'm sure I tested your
WindowDelegate
solution in Catalina. It was pending to test in other versions, so I have started to test it in Yosemite and got this error. I then tested again in Catalina... but I get this very same error!Traceback (most recent call last): File "/Volumes/Proyectos/PycharmProjects/PyWinCtl-dev/src/pywinctl/_pywinctl_macos.py", line 488, in <module> class WindowDelegate(AppKit.NSObject): # Cannot put into a closure as subsequent calls will cause a re-registration error due to subclassing NSObject. objc.error: WindowDelegate is overriding existing Objective-C class
Don't know what is happening, to be honest.
EDIT: Mistery solved. If you run
_pwinctl_macos.py
script directly, it loads pywinctl module twice, so it complains becauseWindowDelegate
is already registered. This doesn't happen if you run it as a module. Most likely, when I first tested in Catalina, I didn't update the new module version, but just ran_pwinctl_macos.py
directly. Since the existingpywinctl
module version had no reference ofWindowDelegate
, it didn't fail. The question now is how to solve this... I will check!EDIT 2: Doing this, it seems to work running the script directly as
python3 _pywinctl_macos.py
and within a separate script asimport pywinctl
. I have included two calls togetClientFrame()
intest_MacOSNSWindow.py
, and it doesn't fail, but you have a comment suggesting it's not the right way, correct?:def getClientFrame(self): """ Get the client area of window including scroll, menu and status bars, as a Rect (x, y, right, bottom) Notice that this method won't match non-standard window decoration style sizes :return: Rect struct WARNING: it will fail if not called within main thread """ class WindowDelegate(AppKit.NSObject): # Cannot put into a closure as subsequent calls will cause a re-registration error due to subclassing NSObject. """Helps run window operations on the main thread.""" print("IN", registered) results: Dict[bytes, Any] = {} # Store results here. Not ideal, but may be better than using a global. registered = True print("IN", registered) @staticmethod def run_on_main_thread(selector: bytes, obj: Optional[Any] = None, wait: Optional[bool] = True) -> Any: """Runs a method of this object on the main thread.""" WindowDelegate.alloc().performSelectorOnMainThread_withObject_waitUntilDone_(selector, obj, wait) return WindowDelegate.results.get(selector) def getTitleBarHeightAndBorderWidth(self) -> None: """Updates results with title bar height and border width.""" frame_width = 100 window = AppKit.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( ((0, 0), (frame_width, 100)), AppKit.NSTitledWindowMask, AppKit.NSBackingStoreBuffered, False, ) titlebar_height = int(window.titlebarHeight()) # print(titlebar_height) content_rect = window.contentRectForFrameRect_(window.frame()) x1 = AppKit.NSMinX(content_rect) x2 = AppKit.NSMaxX(content_rect) border_width = int(frame_width - (x2 - x1)) # print(border_width) result = titlebar_height, border_width WindowDelegate.results[b'getTitleBarHeightAndBorderWidth'] = result # https://www.macscripter.net/viewtopic.php?id=46336 --> Won't allow access to NSWindow objects, but interesting # Didn't find a way to get menu bar height using Apple Script # titleHeight, borderWidth = _getBorderSizes() titleHeight, borderWidth = WindowDelegate.run_on_main_thread(b'getTitleBarHeightAndBorderWidth') res = Rect(int(self.left + borderWidth), int(self.top + titleHeight), int(self.right - borderWidth), int(self.bottom - borderWidth)) return res
Using a closure doesn't work for me. For example, consider a file named test.py
containing the following lines of code:
from typing import Any, Dict, Optional
import AppKit
def run():
class WindowDelegate(AppKit.NSObject): # Cannot put into a closure as subsequent calls will cause a re-registration error due to subclassing NSObject.
"""Helps run window operations on the main thread."""
results: Dict[bytes, Any] = {} # Store results here. Not ideal, but may be better than using a global.
@staticmethod
def run_on_main_thread(selector: bytes, obj: Optional[Any]=None, wait: Optional[bool]=True) -> Any:
"""Runs a method of this object on the main thread."""
WindowDelegate.alloc().performSelectorOnMainThread_withObject_waitUntilDone_(selector, obj, wait)
return WindowDelegate.results.get(selector)
def getTitleBarHeightAndBorderWidth(self) -> None:
"""Updates results with title bar height and border width."""
frame_width = 100
window = AppKit.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
((0, 0), (frame_width, 100)),
AppKit.NSTitledWindowMask,
AppKit.NSBackingStoreBuffered,
False,
)
titlebar_height = int(window.titlebarHeight())
# print(titlebar_height)
content_rect = window.contentRectForFrameRect_(window.frame())
x1 = AppKit.NSMinX(content_rect)
x2 = AppKit.NSMaxX(content_rect)
border_width = int(frame_width - (x2 - x1))
# print(border_width)
result = titlebar_height, border_width
WindowDelegate.results[b'getTitleBarHeightAndBorderWidth'] = result
results = WindowDelegate.run_on_main_thread(b'getTitleBarHeightAndBorderWidth')
return results
if __name__ == '__main__':
print(run())
Running the file works fine for me. Now if I treat it as a module and import the run()
, it fails:
import sys
sys.path.insert(0, r'/Users/ibby/Developer/PyWinCtl/src')
from pywinctl._test import run
run()
The error message is the same as before:
Actually, I get the same error if I do not wrap WindowDelegate
into a closure. But a simple (and ugly) solution seems to be using a try-except block:
rom typing import Any, Dict, Optional
import AppKit
try:
WindowDelegate = AppKit.WindowDelegate
except BaseException:
class WindowDelegate(AppKit.NSObject): # Cannot put into a closure as subsequent calls will cause a re-registration error due to subclassing NSObject.
"""Helps run window operations on the main thread."""
results: Dict[bytes, Any] = {} # Store results here. Not ideal, but may be better than using a global.
@staticmethod
def run_on_main_thread(selector: bytes, obj: Optional[Any]=None, wait: Optional[bool]=True) -> Any:
"""Runs a method of this object on the main thread."""
WindowDelegate.alloc().performSelectorOnMainThread_withObject_waitUntilDone_(selector, obj, wait)
return WindowDelegate.results.get(selector)
def getTitleBarHeightAndBorderWidth(self) -> None:
"""Updates results with title bar height and border width."""
frame_width = 100
window = AppKit.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
((0, 0), (frame_width, 100)),
AppKit.NSTitledWindowMask,
AppKit.NSBackingStoreBuffered,
False,
)
titlebar_height = int(window.titlebarHeight())
# print(titlebar_height)
content_rect = window.contentRectForFrameRect_(window.frame())
x1 = AppKit.NSMinX(content_rect)
x2 = AppKit.NSMaxX(content_rect)
border_width = int(frame_width - (x2 - x1))
# print(border_width)
result = titlebar_height, border_width
WindowDelegate.results[b'getTitleBarHeightAndBorderWidth'] = result
if __name__ == '__main__':
results = WindowDelegate.run_on_main_thread(b'getTitleBarHeightAndBorderWidth')
print(results)
The option of the closure, importing it as a module, works fine on my side, but not when invoking run() twice.
Though I don't think try-except block is a so ugly solution, I think I found a safer, smarter solution. Let me know what you think:
if not hasattr(AppKit, "WindowDelegate"):
... import/declare WindowDelegate class ...
else:
WindowDelegate = AppKit.WindowDelegate
results = WindowDelegate.run_on_main_thread(b'getTitleBarHeightAndBorderWidth')
Thank you for your interest and help. I do really appreciate it!
EDIT: Good news are that your solution also works in Yosemite!!! (the previous one based on AppleScript, didn't)
Excellent @Kalmat !
Hi, ibby! I'm (sadly) closing this PR if you agree. Please, PR your getClientFrame() solution to master branch so we can merge it with all other changes and prepare a new release. Thank you!
Hi again! Please, PR your solution for getClientFrame() to master branch. I hope to be able to create a new module version (v0.1) in very next days / weeks, and I would like that part of the code to be fully attributed to your wonderful contribution!
Thank you!
Hi @Kalmat , do you still need me to do this? Apologies, I've been away from GitHub for some time.
Hi! No apologies. This is not an obligation at all! Of course I would like you to PR your solution to master branch, so your work is fully attributed to you!
Thank you!
Please let me know if this works on your end. It seems to work fine when called from another module or a Jupyter notebook. However, on my end, running the
_pywinctl_macos.py
file as a script yields the following error:Note that line 110 in the old code seems to be setting the
Rect
bounds incorrectly. The AppleScript returns size as{(x, y), (width, height)}
, and the old code sets the bounds to these numbers. Theright
andbottom
bounds are therefore off by-x
and-y
, respectively. Please let me know if you disagree.