Files
Modernization-Tool/setup_tool_responsive.py

264 lines
9.2 KiB
Python

import json
import os
import tkinter as tk
from tkinter import messagebox, ttk
import dxvk_fps
import setup_tool_dynamic
class ResponsiveModernWowSetupTool(setup_tool_dynamic.ModernWowSetupTool):
"""Production entry point with display-scaling-safe UI and DXVK FPS control."""
def __init__(self, root):
# Define these before the parent constructor: WowSetupTool.__init__ calls
# overridden UI/settings methods while it is still initializing.
self.detected_refresh_rate = dxvk_fps.detect_max_refresh_rate()
self.limit_dxvk_fps = tk.BooleanVar(master=root, value=True)
self.dxvk_fps_limit = tk.IntVar(
master=root,
value=self.detected_refresh_rate,
)
self.dxvk_fps_checkbox = None
self.dxvk_fps_entry = None
self.dxvk_fps_unit_label = None
super().__init__(root)
# Keep the FPS controls synchronized when the renderer radio selection
# changes or when saved settings restore a different renderer.
self._rendering_mode_trace = self.rendering_mode.trace_add(
"write",
lambda *_args: self._update_dxvk_fps_state(),
)
self._update_dxvk_fps_state()
# Preserve all existing installer behavior and only change how the
# top-level Tk window allocates and exposes its controls.
root.resizable(True, True)
# Reserve the bottom action button before the expandable notebook so
# Windows DPI/text scaling cannot push Apply outside the visible area.
notebook = next(
(child for child in root.winfo_children() if isinstance(child, ttk.Notebook)),
None,
)
apply_button = getattr(self, "apply_button", None)
if notebook is not None and apply_button is not None:
notebook.pack_forget()
apply_button.pack_forget()
apply_button.pack(side="bottom", pady=10, fill="x", padx=20)
notebook.pack(fill="both", expand=True, padx=10, pady=(5, 10))
root.update_idletasks()
# Prefer the widgets' requested size when the monitor has room, while
# keeping a margin so the window remains reachable on smaller screens.
screen_width = max(1, root.winfo_screenwidth())
screen_height = max(1, root.winfo_screenheight())
max_width = max(640, screen_width - 80)
max_height = max(520, screen_height - 80)
requested_width = max(680, root.winfo_reqwidth())
requested_height = max(660, root.winfo_reqheight())
width = min(requested_width, max_width)
height = min(requested_height, max_height)
root.geometry(f"{width}x{height}")
root.minsize(min(640, max_width), min(520, max_height))
def build_main_tab(self, parent):
super().build_main_tab(parent)
optional_label = None
for child in parent.winfo_children():
try:
if child.cget("text") == "Optional Mods:":
optional_label = child
break
except tk.TclError:
continue
fps_frame = ttk.Frame(parent)
pack_options = {
"fill": "x",
"padx": 20,
"pady": (8, 2),
}
if optional_label is not None:
pack_options["before"] = optional_label
fps_frame.pack(**pack_options)
controls = ttk.Frame(fps_frame)
controls.pack(anchor="w")
self.dxvk_fps_checkbox = ttk.Checkbutton(
controls,
text="Limit DXVK FPS",
variable=self.limit_dxvk_fps,
command=self._update_dxvk_fps_state,
)
self.dxvk_fps_checkbox.pack(side="left")
self.dxvk_fps_entry = ttk.Entry(
controls,
textvariable=self.dxvk_fps_limit,
width=7,
)
self.dxvk_fps_entry.pack(side="left", padx=(10, 4))
self.dxvk_fps_unit_label = ttk.Label(controls, text="FPS")
self.dxvk_fps_unit_label.pack(side="left")
help_label = ttk.Label(
fps_frame,
text=(
"Auto-detected from the fastest active display at startup. "
"You can change it manually. DXVK only."
),
font=("Segoe UI", 8, "italic"),
)
help_label.pack(anchor="w", padx=(22, 0), pady=(1, 0))
tooltip = (
"Limits DXVK with d3d9.maxFrameRate. The initial value uses the "
f"highest active display refresh rate detected at startup "
f"({self.detected_refresh_rate} Hz). You can enter another positive "
"integer. DirectX 9 is never modified by this option."
)
setup_tool_dynamic.ToolTip(self.dxvk_fps_checkbox, tooltip)
setup_tool_dynamic.ToolTip(self.dxvk_fps_entry, tooltip)
self._update_dxvk_fps_state()
def _update_dxvk_fps_state(self):
is_dxvk = getattr(self, "rendering_mode", None) is not None and (
self.rendering_mode.get() == "dxvk"
)
enabled = bool(self.limit_dxvk_fps.get())
checkbox = getattr(self, "dxvk_fps_checkbox", None)
entry = getattr(self, "dxvk_fps_entry", None)
unit_label = getattr(self, "dxvk_fps_unit_label", None)
if checkbox is not None:
checkbox.configure(state="normal" if is_dxvk else "disabled")
if entry is not None:
entry.configure(state="normal" if is_dxvk and enabled else "disabled")
if unit_label is not None:
if is_dxvk and enabled:
unit_label.state(["!disabled"])
else:
unit_label.state(["disabled"])
def _collect_settings(self):
settings = super()._collect_settings()
try:
fps_value = int(self.dxvk_fps_limit.get())
except (tk.TclError, TypeError, ValueError):
fps_value = int(self.detected_refresh_rate)
settings["dxvk_fps_limit"] = {
"enabled": bool(self.limit_dxvk_fps.get()),
"value": fps_value,
}
return settings
def _apply_settings_dict(self, saved):
super()._apply_settings_dict(saved)
fps_settings = saved.get("dxvk_fps_limit") if isinstance(saved, dict) else None
if isinstance(fps_settings, dict):
enabled = fps_settings.get("enabled")
value = fps_settings.get("value")
if isinstance(enabled, bool):
self.limit_dxvk_fps.set(enabled)
if (
isinstance(value, int)
and not isinstance(value, bool)
and value > 0
):
self.dxvk_fps_limit.set(value)
self._update_dxvk_fps_state()
def load_settings(self, target_dir):
# Existing v2.3 installations predate this option. Preserve their
# current DXVK limit state instead of silently enabling a new cap.
has_saved_fps_setting = False
settings_path = self._settings_path(target_dir)
try:
with open(settings_path, "r", encoding="utf-8") as handle:
saved = json.load(handle)
has_saved_fps_setting = isinstance(
saved.get("dxvk_fps_limit") if isinstance(saved, dict) else None,
dict,
)
except (OSError, json.JSONDecodeError, ValueError, TypeError):
pass
loaded = super().load_settings(target_dir)
if (
not has_saved_fps_setting
and self._looks_like_managed_install(target_dir)
and self.rendering_mode.get() == "dxvk"
):
existing = dxvk_fps.read_dxvk_fps_limit(
os.path.join(target_dir, "dxvk.conf")
)
if existing is None:
self.limit_dxvk_fps.set(False)
else:
enabled, value = existing
self.limit_dxvk_fps.set(enabled)
self.dxvk_fps_limit.set(value)
self._update_dxvk_fps_state()
return loaded
def validate_limits(self):
if not super().validate_limits():
return False
if self.rendering_mode.get() != "dxvk" or not self.limit_dxvk_fps.get():
return True
try:
value = int(self.dxvk_fps_limit.get())
except (tk.TclError, TypeError, ValueError):
messagebox.showerror(
"Input Error",
"DXVK FPS Limit must contain a positive whole number.",
)
return False
if value <= 0:
messagebox.showerror(
"Input Error",
"DXVK FPS Limit must be greater than 0.",
)
return False
return True
def configure_dxvk(self, target):
super().configure_dxvk(target)
# DirectX 9 follows the existing renderer cleanup path above. Never
# create or modify dxvk.conf when DXVK is not selected.
if self.rendering_mode.get() != "dxvk":
return
dxvk_fps.apply_dxvk_fps_limit(
os.path.join(target, "dxvk.conf"),
bool(self.limit_dxvk_fps.get()),
int(self.dxvk_fps_limit.get()),
)
if __name__ == "__main__":
root = tk.Tk()
app = ResponsiveModernWowSetupTool(root)
root.mainloop()