Move window trackers into the class, get rid of more globals

This commit is contained in:
AuroraWright
2025-05-05 06:57:07 +02:00
parent eaca5bf006
commit a06e2f5659
2 changed files with 91 additions and 124 deletions

View File

@@ -4,6 +4,14 @@ import argparse
import textwrap
import urllib.request
def str2bool(value):
if value.lower() == 'true':
return True
elif value.lower() == 'false':
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
parser = argparse.ArgumentParser(prog='owocr', description=textwrap.dedent('''\
Runs OCR in the background.
It can read images copied to the system clipboard or placed in a directory, images sent via a websocket or a Unix domain socket, or directly capture a screen (or a portion of it) or a window.
@@ -36,7 +44,7 @@ parser.add_argument('-sa', '--screen_capture_area', type=str, default=argparse.S
help='Area to target when reading with screen capture. Can be either empty (automatic selector), a set of coordinates (x,y,width,height), "screen_N" (captures a whole screen, where N is the screen number starting from 1) or a window name (the first matching window title will be used).')
parser.add_argument('-sd', '--screen_capture_delay_secs', type=float, default=argparse.SUPPRESS,
help='Delay (in seconds) between screenshots when reading with screen capture.')
parser.add_argument('-sw', '--screen_capture_only_active_windows', action='store_true', default=argparse.SUPPRESS,
parser.add_argument('-sw', '--screen_capture_only_active_windows', type=str2bool, default=argparse.SUPPRESS,
help="When reading with screen capture and screen_capture_area is a window name, only target the window while it's active.")
parser.add_argument('-sc', '--screen_capture_combo', type=str, default=argparse.SUPPRESS,
help='When reading with screen capture, combo to wait on for taking a screenshot instead of using the delay. As an example: "<ctrl>+<shift>+s". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key')

View File

@@ -296,92 +296,6 @@ class RequestHandler(socketserver.BaseRequestHandler):
conn.sendall(b'False')
class MacOSWindowTracker(threading.Thread):
def __init__(self, window_id):
super().__init__(daemon=True)
self.stop = False
self.window_id = window_id
self.window_active = screencapture_window_active
def run(self):
found = True
while found and not self.stop:
found = False
is_active = False
with objc.autorelease_pool():
window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID)
for i, window in enumerate(window_list):
if found and window.get(kCGWindowName, '') == 'Fullscreen Backdrop':
is_active = True
break
if self.window_id == window['kCGWindowNumber']:
found = True
if i == 0 or window_list[i-1].get(kCGWindowName, '') in ('Dock', 'Color Enforcer Window'):
is_active = True
break
if not found:
window_list = CGWindowListCreateDescriptionFromArray([self.window_id])
if len(window_list) > 0:
found = True
if found and self.window_active != is_active:
on_window_activated(is_active)
self.window_active = is_active
time.sleep(0.2)
if not found:
on_window_closed(False)
class WindowsWindowTracker(threading.Thread):
def __init__(self, window_handle, only_active):
super().__init__(daemon=True)
self.stop = False
self.window_handle = window_handle
self.only_active = only_active
self.window_active = screencapture_window_active
self.window_minimized = not screencapture_window_visible
def run(self):
found = True
while not self.stop:
found = win32gui.IsWindow(self.window_handle)
if not found:
break
if self.only_active:
is_active = self.window_handle == win32gui.GetForegroundWindow()
if self.window_active != is_active:
on_window_activated(is_active)
self.window_active = is_active
else:
is_minimized = win32gui.IsIconic(self.window_handle)
if self.window_minimized != is_minimized:
on_window_minimized(is_minimized)
self.window_minimized = is_minimized
time.sleep(0.2)
if not found:
on_window_closed(False)
def get_windows_window_handle(window_title):
def callback(hwnd, window_title_part):
window_title = win32gui.GetWindowText(hwnd)
if window_title_part in window_title:
handles.append((hwnd, window_title))
return True
handle = win32gui.FindWindow(None, window_title)
if handle:
return (handle, window_title)
handles = []
win32gui.EnumWindows(callback, window_title)
for handle in handles:
_, pid = win32process.GetWindowThreadProcessId(handle[0])
if psutil.Process(pid).name().lower() not in ('cmd.exe', 'powershell.exe', 'windowsterminal.exe'):
return handle
return (None, None)
class TextFiltering:
accurate_filtering = False
@@ -453,8 +367,10 @@ class TextFiltering:
class ScreenshotClass:
def __init__(self):
screen_capture_area = config.get_general('screen_capture_area')
self.macos_window_tracker = None
self.windows_window_tracker = None
self.macos_window_tracker_instance = None
self.windows_window_tracker_instance = None
self.screencapture_window_active = True
self.screencapture_window_visible = True
if screen_capture_area == '':
self.screencapture_mode = 0
elif screen_capture_area.startswith('screen_'):
@@ -501,15 +417,14 @@ class ScreenshotClass:
self.sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height}
logger.opt(ansi=True).info(f'Selected coordinates: {coord_left},{coord_top},{coord_width},{coord_height}')
else:
screen_capture_only_active_windows = config.get_general('screen_capture_only_active_windows')
self.screen_capture_only_active_windows = config.get_general('screen_capture_only_active_windows')
area_invalid_error = '"screen_capture_area" must be empty, "screen_N" where N is a screen number starting from 1, a valid set of coordinates, or a valid window name'
if sys.platform == 'darwin':
if config.get_general('screen_capture_old_macos_api') or int(platform.mac_ver()[0].split('.')[0]) < 14:
self.old_macos_screenshot_api = True
else:
self.old_macos_screenshot_api = False
global screencapturekit_queue
screencapturekit_queue = queue.Queue()
self.screencapturekit_queue = queue.Queue()
CGMainDisplayID()
window_list = CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements, kCGNullWindowID)
window_titles = []
@@ -535,36 +450,68 @@ class ScreenshotClass:
self.window_id = window_ids[window_index]
window_title = window_titles[window_index]
if screen_capture_only_active_windows:
self.macos_window_tracker = MacOSWindowTracker(self.window_id)
self.macos_window_tracker.start()
if self.screen_capture_only_active_windows:
self.macos_window_tracker_instance = threading.Thread(target=self.macos_window_tracker)
self.macos_window_tracker_instance.start()
logger.opt(ansi=True).info(f'Selected window: {window_title}')
elif sys.platform == 'win32':
self.window_handle, window_title = get_windows_window_handle(screen_capture_area)
self.window_handle, window_title = self.get_windows_window_handle(screen_capture_area)
if not self.window_handle:
raise ValueError(area_invalid_error)
ctypes.windll.shcore.SetProcessDpiAwareness(1)
self.windows_window_tracker = WindowsWindowTracker(self.window_handle, screen_capture_only_active_windows)
self.windows_window_tracker.start()
self.windows_window_tracker_instance = threading.Thread(target=self.windows_window_tracker)
self.windows_window_tracker_instance.start()
logger.opt(ansi=True).info(f'Selected window: {window_title}')
else:
raise ValueError('Window capture is only currently supported on Windows and macOS')
def __del__(self):
if self.macos_window_tracker:
self.macos_window_tracker.stop = True
self.macos_window_tracker.join()
elif self.windows_window_tracker:
self.windows_window_tracker.stop = True
self.windows_window_tracker.join()
if self.macos_window_tracker_instance:
self.macos_window_tracker_instance.join()
elif self.windows_window_tracker_instance:
self.windows_window_tracker_instance.join()
def get_windows_window_handle(self, window_title):
def callback(hwnd, window_title_part):
window_title = win32gui.GetWindowText(hwnd)
if window_title_part in window_title:
handles.append((hwnd, window_title))
return True
handle = win32gui.FindWindow(None, window_title)
if handle:
return (handle, window_title)
handles = []
win32gui.EnumWindows(callback, window_title)
for handle in handles:
_, pid = win32process.GetWindowThreadProcessId(handle[0])
if psutil.Process(pid).name().lower() not in ('cmd.exe', 'powershell.exe', 'windowsterminal.exe'):
return handle
return (None, None)
def windows_window_tracker(self):
found = True
while not terminated:
found = win32gui.IsWindow(self.window_handle)
if not found:
break
if self.screen_capture_only_active_windows:
self.screencapture_window_active = self.window_handle == win32gui.GetForegroundWindow()
else:
self.screencapture_window_visible = not win32gui.IsIconic(self.window_handle)
time.sleep(0.2)
if not found:
on_window_closed(False)
def capture_macos_window_screenshot(self, window_id):
def shareable_content_completion_handler(shareable_content, error):
if error:
screencapturekit_queue.put(None)
self.screencapturekit_queue.put(None)
return
target_window = None
@@ -574,7 +521,7 @@ class ScreenshotClass:
break
if not target_window:
screencapturekit_queue.put(None)
self.screencapturekit_queue.put(None)
return
with objc.autorelease_pool():
@@ -598,15 +545,41 @@ class ScreenshotClass:
def capture_image_completion_handler(image, error):
if error:
screencapturekit_queue.put(None)
self.screencapturekit_queue.put(None)
return
screencapturekit_queue.put(image)
self.screencapturekit_queue.put(image)
SCShareableContent.getShareableContentWithCompletionHandler_(
shareable_content_completion_handler
)
def macos_window_tracker(self):
found = True
while found and not terminated:
found = False
is_active = False
with objc.autorelease_pool():
window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID)
for i, window in enumerate(window_list):
if found and window.get(kCGWindowName, '') == 'Fullscreen Backdrop':
is_active = True
break
if self.window_id == window['kCGWindowNumber']:
found = True
if i == 0 or window_list[i-1].get(kCGWindowName, '') in ('Dock', 'Color Enforcer Window'):
is_active = True
break
if not found:
window_list = CGWindowListCreateDescriptionFromArray([self.window_id])
if len(window_list) > 0:
found = True
if found:
self.screencapture_window_active = is_active
time.sleep(0.2)
if not found:
on_window_closed(False)
def __call__(self):
if self.screencapture_mode == 2:
if sys.platform == 'darwin':
@@ -616,7 +589,7 @@ class ScreenshotClass:
else:
self.capture_macos_window_screenshot(self.window_id)
try:
cg_image = screencapturekit_queue.get(timeout=0.5)
cg_image = self.screencapturekit_queue.get(timeout=0.5)
except queue.Empty:
cg_image = None
if not cg_image:
@@ -783,16 +756,6 @@ def on_window_closed(alive):
terminated = True
def on_window_activated(active):
global screencapture_window_active
screencapture_window_active = active
def on_window_minimized(minimized):
global screencapture_window_visible
screencapture_window_visible = not minimized
def on_screenshot_combo():
if not paused:
img = take_screenshot()
@@ -917,11 +880,7 @@ def run():
websocket_server_thread = WebsocketServerThread('websocket' in (read_from, read_from_secondary))
websocket_server_thread.start()
if 'screencapture' in (read_from, read_from_secondary):
global screencapture_window_active
global screencapture_window_visible
global take_screenshot
screencapture_window_active = False
screencapture_window_visible = True
screen_capture_delay_secs = config.get_general('screen_capture_delay_secs')
screen_capture_combo = config.get_general('screen_capture_combo')
last_screenshot_time = 0
@@ -992,7 +951,7 @@ def run():
pass
if (not img) and process_screenshots:
if (not paused) and screencapture_window_active and screencapture_window_visible and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
if (not paused) and take_screenshot.screencapture_window_active and take_screenshot.screencapture_window_visible and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
img = take_screenshot()
filter_img = True
notify = False