From 4bebde5a9cc3b63eaf537f933fe882c5b3b7daae Mon Sep 17 00:00:00 2001 From: qwcan Date: Sat, 29 Nov 2025 00:22:06 -0600 Subject: [PATCH] Initial work for fixing the offscreen window --- umalauncher/carrotjuicer.py | 16 ++++++++++-- umalauncher/util.py | 49 +++++++++++++++++++++++++++++++++++++ umalauncher/version.py | 2 +- umalauncher/windowmover.py | 23 ++++++++++++++++- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/umalauncher/carrotjuicer.py b/umalauncher/carrotjuicer.py index 89a4f59..fd57e12 100644 --- a/umalauncher/carrotjuicer.py +++ b/umalauncher/carrotjuicer.py @@ -167,7 +167,7 @@ def open_helper(self): def get_browser_reset_position(self): if self.threader.windowmover.window is None: - return None + return [0, 0, 720, 480] game_rect, _ = self.threader.windowmover.window.get_rect() workspace_rect = self.threader.windowmover.window.get_workspace_rect() left_side = abs(workspace_rect[0] - game_rect[0]) @@ -178,7 +178,18 @@ def get_browser_reset_position(self): else: left_x = game_rect[2] + 5 width = right_side - return [left_x, workspace_rect[1], width, workspace_rect[3] - workspace_rect[1] + 6] + height = workspace_rect[3] - workspace_rect[1] + 6 + # Enforce a 100-pixel minimum width and height + if width < 100: + width = 100 + if height < 100: + height = 100 + rect = [left_x, workspace_rect[1], width, height] + if util.rect_is_onscreen( rect ): + return rect + else: + return util.move_rect_to_screen(rect) + def close_browser(self): @@ -190,6 +201,7 @@ def close_browser(self): def save_rect(self, rect_var, setting): if rect_var: + #TODO: actually check window dimensions if (rect_var['x'] == -32000 and rect_var['y'] == -32000): logger.warning(f"Browser minimized, cannot save position for {setting}: {rect_var}") rect_var = None diff --git a/umalauncher/util.py b/umalauncher/util.py index c023641..5ca60f4 100644 --- a/umalauncher/util.py +++ b/umalauncher/util.py @@ -442,6 +442,55 @@ def monitor_from_window(*args, **kwargs): except pywinerror: return None +def get_monitor_from_point(*args, **kwargs): + try: + return win32api.MonitorFromPoint(*args, **kwargs) + except pywinerror: + return None + +def rect_is_onscreen(rect): + top_left = (rect[0], rect[1]) + top_right = (rect[0] + rect[2], rect[1]) + bottom_left = (rect[0], rect[1] + rect[3]) + bottom_right = (rect[0] + rect[2], rect[1] + rect[3]) + return get_monitor_from_point(top_left) is not None \ + and get_monitor_from_point(top_right) is not None \ + and get_monitor_from_point(bottom_left) is not None \ + and get_monitor_from_point(bottom_right) + +def move_rect_to_screen(rect): + if rect_is_onscreen(rect): + return rect + + #TODO: this is nasty AI-generated code that I haven't looked at too closely + + # Get closest monitor for top-left corner + monitor = get_monitor_from_point((rect[0], rect[1])) + if monitor is None: + # Try top-right corner + monitor = get_monitor_from_point((rect[0] + rect[2], rect[1])) + + if monitor is None: + # Both top corners are offscreen, move them onscreen + monitor = get_monitor_from_point((rect[0], rect[1]), win32con.MONITOR_DEFAULTTONEAREST) + monitor_info = get_monitor_info(monitor) + if not monitor_info: + logger.error("Cannot get monitor info.") + # raise Exception("Cannot get monitor info.") + monitor_info = {"Work": (0, 0, 1920, 1080)} + work_area = monitor_info.get("Work") + + # Calculate new position while preserving dimensions + new_x = max(work_area[0], min(work_area[2] - rect[2], rect[0])) + new_y = max(work_area[1], min(work_area[3] - rect[3], rect[1])) + + return [new_x, new_y, rect[2], rect[3]] + else: + # Both top corners are onscreen, should be visible + return rect + + + def get_monitor_info(*args, **kwargs): try: return win32api.GetMonitorInfo(*args, **kwargs) diff --git a/umalauncher/version.py b/umalauncher/version.py index f4e7587..4118490 100644 --- a/umalauncher/version.py +++ b/umalauncher/version.py @@ -11,7 +11,7 @@ import gui import glob -VERSION = "1.17.13" +VERSION = "1.17.14" def parse_version(version_string: str): """Convert version string to tuple.""" diff --git a/umalauncher/windowmover.py b/umalauncher/windowmover.py index 4ca94f7..968ae8a 100644 --- a/umalauncher/windowmover.py +++ b/umalauncher/windowmover.py @@ -1,5 +1,7 @@ import os import time + +import win32con from loguru import logger import util import win32gui @@ -45,8 +47,27 @@ def set_pos(self, pos, is_portrait): return success + def is_window_onscreen(self): + return util.monitor_from_window(self.handle, win32con.MONITOR_DEFAULTTONULL) is not None + + def move_window_onscreen(self): + if not self.is_window_onscreen(): + nearest_monitor = util.monitor_from_window(self.handle, win32con.MONITOR_DEFAULTTONEAREST) + if not nearest_monitor: + logger.error("Cannot determine monitor nearest to game window.") + return False + monitor_info = util.get_monitor_info(nearest_monitor) + if not monitor_info: + logger.error("Cannot get monitor info.") + # raise Exception("Cannot get monitor info.") + return False + monitor_rect = monitor_info.get("Work") + prev_rect = util.get_window_rect(self.handle) + util.move_window(self.handle, monitor_rect[0], monitor_rect[1], prev_rect[2], prev_rect[3], True ) + return True + def get_workspace_rect(self): - monitor = util.monitor_from_window(self.handle) + monitor = util.monitor_from_window(self.handle, win32con.MONITOR_DEFAULTTOPRIMARY) if not monitor: logger.error("Cannot determine monitor used by game window.") # raise Exception("Cannot determine monitor used by game window.")