264 lines
9.2 KiB
Python
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()
|