diff --git a/LivescriptV2.01.py b/LivescriptV2.01.py index 72f86e1..0d513af 100644 --- a/LivescriptV2.01.py +++ b/LivescriptV2.01.py @@ -29,6 +29,12 @@ DEVELOPER_URL = "https://github.com/sally4d" LinkedIn_URL = "https://www.linkedin.com/in/oscurprof/" SUPPORT_URL = "https://oscurprofundo.gumroad.com/l/dmkbes" +BASE_SCREEN_DPI = 96 +BASE_TK_SCALING = BASE_SCREEN_DPI / 72 +MIN_UI_SCALING = 0.75 +MAX_UI_SCALING = 3.0 +UI_SCALING_CHECK_INTERVAL_MS = 3000 +MAX_DIALOG_SCREEN_FRACTION = 0.9 # --- Language Model Definitions --- LANGUAGE_MODELS = { @@ -306,13 +312,139 @@ def save_settings(self, settings): print(f"Error saving settings: {e}") +def clamp_ui_scaling(value): + return max(MIN_UI_SCALING, min(MAX_UI_SCALING, value)) + + +def parse_positive_float(value): + try: + parsed_value = float(value) + except (TypeError, ValueError): + return None + return parsed_value if parsed_value > 0 else None + + +def get_environment_ui_scaling(): + for variable_name in ("QT_SCALE_FACTOR", "GDK_SCALE", "ELM_SCALE", "CLUTTER_SCALE"): + scaling = parse_positive_float(os.environ.get(variable_name)) + if scaling and scaling != 1: + return scaling, variable_name + + scaling = parse_positive_float(os.environ.get("GDK_DPI_SCALE")) + if scaling and scaling != 1 and "GDK_SCALE" not in os.environ: + return scaling, "GDK_DPI_SCALE" + + return None, None + + +def get_xrdb_ui_scaling(): + try: + result = subprocess.run(["xrdb", "-query"], capture_output=True, text=True, timeout=0.5, check=False) + except (FileNotFoundError, subprocess.SubprocessError, OSError): + return None, None + + for line in result.stdout.splitlines(): + if line.lower().startswith("xft.dpi"): + _, _, raw_dpi = line.partition(":") + dpi = parse_positive_float(raw_dpi.strip()) + if dpi: + return dpi / BASE_SCREEN_DPI, "Xft.dpi" + + return None, None + + +def get_tk_ui_scaling(root): + try: + root.update_idletasks() + dpi = root.winfo_fpixels("1i") + except tk.TclError: + return None, None + + return dpi / BASE_SCREEN_DPI, "Tk DPI" + + +def detect_system_ui_scaling(root): + fallback_scaling = 1.0 + fallback_source = "default" + + for detector in (get_environment_ui_scaling, get_xrdb_ui_scaling, lambda: get_tk_ui_scaling(root)): + scaling, source = detector() + if scaling: + scaling = clamp_ui_scaling(scaling) + if abs(scaling - 1.0) >= 0.05: + return scaling, source + fallback_scaling = scaling + fallback_source = source + + return fallback_scaling, fallback_source + + +def apply_system_ui_scaling(root, log=True): + scaling, source = detect_system_ui_scaling(root) + + try: + root.tk.call("tk", "scaling", BASE_TK_SCALING * scaling) + except tk.TclError: + pass + + ctk.set_widget_scaling(scaling) + ctk.set_window_scaling(scaling) + if log: + print(f"Using UI scaling: {scaling:.2f}x ({source})") + return scaling + + +def get_scaled_dialog_geometry(window, width, height): + scaling, _ = detect_system_ui_scaling(window) + screen_width = window.winfo_screenwidth() + screen_height = window.winfo_screenheight() + max_width = screen_width * MAX_DIALOG_SCREEN_FRACTION + max_height = screen_height * MAX_DIALOG_SCREEN_FRACTION + + scaled_width = min(width * scaling, max_width) + scaled_height = min(height * scaling, max_height) + geometry_width = max(1, round(scaled_width / scaling)) + geometry_height = max(1, round(scaled_height / scaling)) + x = max(0, round((screen_width - scaled_width) / 2)) + y = max(0, round((screen_height - scaled_height) / 2)) + + return f"{geometry_width}x{geometry_height}+{x}+{y}" + + +def monitor_system_ui_scaling(root, caption_window, last_scaling): + try: + if not root.winfo_exists(): + return + + scaling, source = detect_system_ui_scaling(root) + if abs(scaling - last_scaling) >= 0.05: + try: + root.tk.call("tk", "scaling", BASE_TK_SCALING * scaling) + except tk.TclError: + pass + + ctk.set_widget_scaling(scaling) + ctk.set_window_scaling(scaling) + caption_window.apply_settings(caption_window.settings) + for child in root.winfo_children(): + if isinstance(child, (AboutWindow, SettingsWindow)): + child.center_window() + print(f"Updated UI scaling: {scaling:.2f}x ({source})") + last_scaling = scaling + + root.after(UI_SCALING_CHECK_INTERVAL_MS, monitor_system_ui_scaling, root, caption_window, last_scaling) + except tk.TclError: + pass + + class AboutWindow(ctk.CTkToplevel): """The 'About' window providing app information and links.""" + WINDOW_WIDTH = 550 + WINDOW_HEIGHT = 450 def __init__(self, parent): super().__init__(parent) self.title("About Live Captions") - self.geometry("550x450") self.transient(parent) self.center_window() self.grid_columnconfigure(0, weight=1) @@ -367,20 +499,17 @@ def __init__(self, parent): def center_window(self): self.update_idletasks() - screen_width = self.winfo_screenwidth() - screen_height = self.winfo_screenheight() - x = (screen_width - 550) // 2 - y = (screen_height - 450) // 2 - self.geometry(f"550x450+{x}+{y}") + self.geometry(get_scaled_dialog_geometry(self, self.WINDOW_WIDTH, self.WINDOW_HEIGHT)) class SettingsWindow(ctk.CTkToplevel): """The settings window GUI, built with customtkinter.""" + WINDOW_WIDTH = 540 + WINDOW_HEIGHT = 850 def __init__(self, parent, settings_manager, caption_window, restart_callback): super().__init__(parent) self.title("Settings") - self.geometry("540x850") self.transient(parent) self.center_window() @@ -402,11 +531,7 @@ def __init__(self, parent, settings_manager, caption_window, restart_callback): def center_window(self): self.update_idletasks() - screen_width = self.winfo_screenwidth() - screen_height = self.winfo_screenheight() - x = (screen_width - 540) // 2 - y = (screen_height - 850) // 2 - self.geometry(f"540x850+{x}+{y}") + self.geometry(get_scaled_dialog_geometry(self, self.WINDOW_WIDTH, self.WINDOW_HEIGHT)) def setup_ui(self): self.grid_rowconfigure(0, weight=1) @@ -1050,6 +1175,7 @@ def main(): temp_root = ctk.CTk() temp_root.withdraw() + apply_system_ui_scaling(temp_root, log=False) if not os.path.isdir(settings['model_path']): messagebox.showerror("Model Not Found", @@ -1058,6 +1184,7 @@ def main(): temp_root.destroy() root = tk.Tk() + current_ui_scaling = apply_system_ui_scaling(root) def restart_application(): print("Restarting application...") @@ -1090,6 +1217,7 @@ def restart_application(): recognizer_thread.start() app = CaptionWindow(root, settings_manager, restart_application) + root.after(UI_SCALING_CHECK_INTERVAL_MS, monitor_system_ui_scaling, root, app, current_ui_scaling) if is_first_run or settings.get('show_about_on_startup', True): root.after(100, app.open_about_window) @@ -1107,4 +1235,4 @@ def restart_application(): if __name__ == "__main__": - main() \ No newline at end of file + main()