Added resolution settings and updater modal with rollback functionality for launcher versions.
This commit is contained in:
@@ -74,6 +74,7 @@ class LauncherUpdateCheck:
|
||||
reason: str
|
||||
manifest_url: str
|
||||
manifest: UpdateManifest
|
||||
checked_at: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -110,6 +111,26 @@ class LauncherRollbackResult:
|
||||
restart_required: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LauncherInstalledRelease:
|
||||
"""One validated release retained in the launcher's version store."""
|
||||
|
||||
version: str
|
||||
installed_at: str
|
||||
directory: str
|
||||
active: bool
|
||||
previous: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LauncherUpdateHistory:
|
||||
"""Validated launcher releases available for inspection or rollback."""
|
||||
|
||||
current_version: str | None
|
||||
previous_version: str | None
|
||||
releases: tuple[LauncherInstalledRelease, ...]
|
||||
|
||||
|
||||
def default_public_key_path() -> Path:
|
||||
return Path(__file__).with_name(PUBLIC_KEY_FILENAME)
|
||||
|
||||
@@ -157,7 +178,12 @@ def check_for_launcher_update(
|
||||
)
|
||||
_verify_manifest_signature(manifest_bytes, signature_bytes, key)
|
||||
manifest = parse_update_manifest(manifest_bytes)
|
||||
return _evaluate_launcher_update(manifest, current_version, url)
|
||||
return _evaluate_launcher_update(
|
||||
manifest,
|
||||
current_version,
|
||||
url,
|
||||
checked_at=datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
)
|
||||
|
||||
|
||||
def read_cached_launcher_update_check(
|
||||
@@ -198,13 +224,21 @@ def read_cached_launcher_update_check(
|
||||
manifest_value = value.get("manifest")
|
||||
if not isinstance(manifest_value, dict):
|
||||
return None
|
||||
checked_at = _cached_timestamp(value.get("checked_at"))
|
||||
if checked_at is None:
|
||||
return None
|
||||
try:
|
||||
manifest = parse_update_manifest(
|
||||
(json.dumps(manifest_value, sort_keys=True) + "\n").encode("utf-8")
|
||||
)
|
||||
except (LauncherUpdateError, TypeError, ValueError):
|
||||
return None
|
||||
return _evaluate_launcher_update(manifest, current_version, url)
|
||||
return _evaluate_launcher_update(
|
||||
manifest,
|
||||
current_version,
|
||||
url,
|
||||
checked_at=checked_at,
|
||||
)
|
||||
|
||||
|
||||
def cache_launcher_update_check(
|
||||
@@ -223,7 +257,8 @@ def cache_launcher_update_check(
|
||||
cache_path,
|
||||
{
|
||||
"schema": UPDATE_CHECK_CACHE_SCHEMA,
|
||||
"checked_at": datetime.now().astimezone().isoformat(),
|
||||
"checked_at": check.checked_at
|
||||
or datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"current_version": check.current_version,
|
||||
"manifest_url": check.manifest_url,
|
||||
"public_key_sha256": key_sha256,
|
||||
@@ -239,6 +274,8 @@ def _evaluate_launcher_update(
|
||||
manifest: UpdateManifest,
|
||||
current_version: str,
|
||||
manifest_url: str,
|
||||
*,
|
||||
checked_at: str | None = None,
|
||||
) -> LauncherUpdateCheck:
|
||||
current = _parse_version(current_version, "current launcher version")
|
||||
latest = _parse_version(manifest.version, "manifest version")
|
||||
@@ -269,6 +306,7 @@ def _evaluate_launcher_update(
|
||||
reason=reason,
|
||||
manifest_url=manifest_url,
|
||||
manifest=manifest,
|
||||
checked_at=checked_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -408,12 +446,22 @@ def rollback_launcher_update(paths: AppPaths) -> LauncherRollbackResult:
|
||||
raise LauncherUpdateError("the current launcher release link is missing")
|
||||
if previous is None:
|
||||
raise LauncherUpdateError("no previous launcher release is available")
|
||||
current_version = _validate_release_directory(current)
|
||||
previous_version = _validate_release_directory(previous)
|
||||
if current == previous:
|
||||
current_release = current
|
||||
previous_release = previous
|
||||
current_version = _validate_release_directory(current_release)
|
||||
previous_version = _validate_release_directory(previous_release)
|
||||
if current_release == previous_release:
|
||||
raise LauncherUpdateError("current and previous launcher releases are identical")
|
||||
_replace_release_link(paths.launcher_previous, current, paths.launcher_releases)
|
||||
_replace_release_link(paths.launcher_current, previous, paths.launcher_releases)
|
||||
_replace_release_link(
|
||||
paths.launcher_previous,
|
||||
current_release,
|
||||
paths.launcher_releases,
|
||||
)
|
||||
_replace_release_link(
|
||||
paths.launcher_current,
|
||||
previous_release,
|
||||
paths.launcher_releases,
|
||||
)
|
||||
_fsync_directory(paths.app_home)
|
||||
_write_json(
|
||||
paths.launcher_update_state,
|
||||
@@ -422,19 +470,64 @@ def rollback_launcher_update(paths: AppPaths) -> LauncherRollbackResult:
|
||||
"rolled_back_at": datetime.now().astimezone().isoformat(),
|
||||
"from_version": current_version,
|
||||
"to_version": previous_version,
|
||||
"release_directory": str(previous),
|
||||
"previous_release": str(current),
|
||||
"release_directory": str(previous_release),
|
||||
"previous_release": str(current_release),
|
||||
},
|
||||
)
|
||||
return LauncherRollbackResult(
|
||||
from_version=current_version,
|
||||
to_version=previous_version,
|
||||
current_release=str(previous),
|
||||
previous_release=str(current),
|
||||
current_release=str(previous_release),
|
||||
previous_release=str(current_release),
|
||||
restart_required=True,
|
||||
)
|
||||
|
||||
|
||||
def inspect_launcher_update_history(paths: AppPaths) -> LauncherUpdateHistory:
|
||||
"""Return validated retained releases without trusting directory names alone."""
|
||||
|
||||
current = _release_from_link(paths.launcher_current, paths.launcher_releases)
|
||||
previous = _release_from_link(paths.launcher_previous, paths.launcher_releases)
|
||||
try:
|
||||
candidates = tuple(paths.launcher_releases.iterdir())
|
||||
except FileNotFoundError:
|
||||
candidates = ()
|
||||
except OSError as exc:
|
||||
raise LauncherUpdateError(f"cannot inspect installed launcher releases: {exc}") from exc
|
||||
releases: list[LauncherInstalledRelease] = []
|
||||
for candidate in candidates:
|
||||
if _VERSION_PATTERN.fullmatch(candidate.name) is None:
|
||||
continue
|
||||
try:
|
||||
version = _validate_release_directory(candidate)
|
||||
installed_at = datetime.fromtimestamp(
|
||||
candidate.stat().st_mtime
|
||||
).astimezone().isoformat(timespec="seconds")
|
||||
except (LauncherUpdateError, OSError, ValueError, OverflowError):
|
||||
continue
|
||||
if version != candidate.name:
|
||||
continue
|
||||
resolved = candidate.resolve()
|
||||
releases.append(
|
||||
LauncherInstalledRelease(
|
||||
version=version,
|
||||
installed_at=installed_at,
|
||||
directory=str(resolved),
|
||||
active=resolved == current,
|
||||
previous=resolved == previous,
|
||||
)
|
||||
)
|
||||
releases.sort(
|
||||
key=lambda release: _parse_version(release.version, "launcher release version"),
|
||||
reverse=True,
|
||||
)
|
||||
return LauncherUpdateHistory(
|
||||
current_version=_release_version(releases, active=True),
|
||||
previous_version=_release_version(releases, previous=True),
|
||||
releases=tuple(releases),
|
||||
)
|
||||
|
||||
|
||||
def parse_update_manifest(data: bytes) -> UpdateManifest:
|
||||
"""Strictly parse one already-authenticated manifest."""
|
||||
|
||||
@@ -857,9 +950,15 @@ def _activate_release(paths: AppPaths, release: Path) -> Path | None:
|
||||
raise LauncherUpdateError(
|
||||
f"launcher previous path is not a symbolic link: {paths.launcher_previous}"
|
||||
)
|
||||
if current is not None and current != release:
|
||||
_validate_release_directory(current)
|
||||
_replace_release_link(paths.launcher_previous, current, paths.launcher_releases)
|
||||
if current is not None:
|
||||
current_release = current
|
||||
if current_release != release:
|
||||
_validate_release_directory(current_release)
|
||||
_replace_release_link(
|
||||
paths.launcher_previous,
|
||||
current_release,
|
||||
paths.launcher_releases,
|
||||
)
|
||||
_replace_release_link(paths.launcher_current, release, paths.launcher_releases)
|
||||
_fsync_directory(paths.app_home)
|
||||
return current
|
||||
@@ -980,6 +1079,30 @@ def _parse_published_at(value: str) -> datetime:
|
||||
return parsed
|
||||
|
||||
|
||||
def _cached_timestamp(value: object) -> str | None:
|
||||
if not isinstance(value, str) or not value:
|
||||
return None
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _release_version(
|
||||
releases: list[LauncherInstalledRelease],
|
||||
*,
|
||||
active: bool = False,
|
||||
previous: bool = False,
|
||||
) -> str | None:
|
||||
for release in releases:
|
||||
if (active and release.active) or (previous and release.previous):
|
||||
return release.version
|
||||
return None
|
||||
|
||||
|
||||
def _required_string(value: dict[str, object], key: str) -> str:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, str) or not item:
|
||||
|
||||
@@ -16,21 +16,27 @@ from .paths import AppPaths
|
||||
|
||||
SETTINGS_SCHEMA = 1
|
||||
_TITLE_FRAME_DEFAULT_MIGRATION_KEY = "title_frame_default_migrated"
|
||||
UI_SCALE_CHOICES = frozenset({"auto", "100", "125", "150", "175", "200"})
|
||||
_RETIRED_DISPLAY_SETTINGS = frozenset(
|
||||
{
|
||||
"width",
|
||||
"height",
|
||||
"output_width",
|
||||
"output_height",
|
||||
"refresh_rate",
|
||||
"fullscreen",
|
||||
"scaler",
|
||||
"filter",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
realm: str = "Babel"
|
||||
width: int = 1600
|
||||
height: int = 900
|
||||
output_width: int = 1600
|
||||
output_height: int = 900
|
||||
refresh_rate: int = 60
|
||||
fullscreen: bool = False
|
||||
ui_scale: str = "auto"
|
||||
borderless: bool = False
|
||||
suppress_unresponsive_warnings: bool = True
|
||||
scaler: str = "fit"
|
||||
filter: str = "linear"
|
||||
runtime: str = "auto"
|
||||
proton_sync: str = "auto"
|
||||
proton_path: str | None = None
|
||||
@@ -136,20 +142,13 @@ def _read_settings_object(path: Path) -> dict[str, object] | None:
|
||||
def _settings_from_object(value: dict[str, object]) -> Settings:
|
||||
settings = Settings(
|
||||
realm=_string(value, "realm", "Babel").strip(),
|
||||
width=_integer(value, "width", 1600),
|
||||
height=_integer(value, "height", 900),
|
||||
output_width=_integer(value, "output_width", 1600),
|
||||
output_height=_integer(value, "output_height", 900),
|
||||
refresh_rate=_integer(value, "refresh_rate", 60),
|
||||
fullscreen=_boolean(value, "fullscreen", False),
|
||||
ui_scale=_string(value, "ui_scale", "auto"),
|
||||
borderless=_migrated_borderless(value),
|
||||
suppress_unresponsive_warnings=_boolean(
|
||||
value,
|
||||
"suppress_unresponsive_warnings",
|
||||
True,
|
||||
),
|
||||
scaler=_string(value, "scaler", "fit"),
|
||||
filter=_string(value, "filter", "linear"),
|
||||
runtime=_string(value, "runtime", "auto"),
|
||||
proton_sync=_string(value, "proton_sync", "auto"),
|
||||
proton_path=_optional_string(value, "proton_path"),
|
||||
@@ -192,6 +191,7 @@ def _settings_need_migration(value: dict[str, object]) -> bool:
|
||||
value.get("schema") != SETTINGS_SCHEMA
|
||||
or not _SETTINGS_FIELD_NAMES.issubset(value)
|
||||
or value.get(_TITLE_FRAME_DEFAULT_MIGRATION_KEY) is not True
|
||||
or bool(_RETIRED_DISPLAY_SETTINGS.intersection(value))
|
||||
)
|
||||
|
||||
|
||||
@@ -201,6 +201,8 @@ def _write_settings(
|
||||
existing: dict[str, object] | None,
|
||||
) -> None:
|
||||
payload = dict(existing or {})
|
||||
for name in _RETIRED_DISPLAY_SETTINGS:
|
||||
payload.pop(name, None)
|
||||
payload.update(asdict(settings))
|
||||
payload["schema"] = SETTINGS_SCHEMA
|
||||
payload[_TITLE_FRAME_DEFAULT_MIGRATION_KEY] = True
|
||||
@@ -242,33 +244,13 @@ def _validate(settings: Settings) -> None:
|
||||
or any(ord(character) < 32 for character in settings.realm)
|
||||
):
|
||||
raise ValueError("realm must be a non-empty display name")
|
||||
for name, value in (
|
||||
("width", settings.width),
|
||||
("height", settings.height),
|
||||
("output_width", settings.output_width),
|
||||
("output_height", settings.output_height),
|
||||
):
|
||||
if value < 640 or value > 16384:
|
||||
raise ValueError(f"{name} must be between 640 and 16384")
|
||||
if settings.refresh_rate < 24 or settings.refresh_rate > 1000:
|
||||
raise ValueError("refresh_rate must be between 24 and 1000")
|
||||
if settings.scaler not in {"auto", "integer", "fit", "fill", "stretch"}:
|
||||
raise ValueError(f"unsupported legacy scaler setting: {settings.scaler}")
|
||||
if settings.filter not in {"linear", "nearest", "fsr", "nis", "pixel"}:
|
||||
raise ValueError(f"unsupported legacy filter setting: {settings.filter}")
|
||||
if settings.ui_scale not in UI_SCALE_CHOICES:
|
||||
raise ValueError(f"unsupported launcher UI scale: {settings.ui_scale}")
|
||||
if settings.runtime not in {"auto", "proton7", "proton7-container", "umu"}:
|
||||
raise ValueError(f"unsupported runtime selection: {settings.runtime}")
|
||||
if settings.proton_sync not in {"auto", "fsync", "esync"}:
|
||||
raise ValueError(f"unsupported Proton synchronization mode: {settings.proton_sync}")
|
||||
|
||||
|
||||
def _integer(value: dict[str, object], key: str, default: int) -> int:
|
||||
item = value.get(key, default)
|
||||
if isinstance(item, bool) or not isinstance(item, int):
|
||||
raise ValueError(f"{key} must be an integer")
|
||||
return item
|
||||
|
||||
|
||||
def _boolean(value: dict[str, object], key: str, default: bool) -> bool:
|
||||
item = value.get(key, default)
|
||||
if not isinstance(item, bool):
|
||||
|
||||
@@ -26,6 +26,7 @@ _DIALOG_PALETTES: dict[str, DialogPalette] = {
|
||||
"error": DialogPalette("#cf604c", "#3a1d19", "×"),
|
||||
"question": DialogPalette("#d98927", "#3a2819", "?"),
|
||||
}
|
||||
_BASE_TK_SCALING = 96.0 / 72.0
|
||||
|
||||
|
||||
def dialog_palette(kind: DialogKind) -> DialogPalette:
|
||||
@@ -111,6 +112,16 @@ def dragged_window_geometry(
|
||||
)
|
||||
|
||||
|
||||
def widget_ui_scale(widget: Any) -> float:
|
||||
"""Return the launcher scale already configured on a Tk interpreter."""
|
||||
|
||||
try:
|
||||
scaling = float(widget.tk.call("tk", "scaling"))
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
return 1.0
|
||||
return min(2.0, max(1.0, scaling / _BASE_TK_SCALING))
|
||||
|
||||
|
||||
def _geometry_offset(value: int) -> str:
|
||||
return f"+{value}" if value >= 0 else str(value)
|
||||
|
||||
@@ -138,7 +149,7 @@ def _bind_window_drag(handle: Any, *widgets: Any) -> None:
|
||||
|
||||
|
||||
class DashboardChrome:
|
||||
"""Controller for one frameless launcher window and its controls."""
|
||||
"""Controller for one launcher window and its custom controls."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -310,9 +321,10 @@ def create_window_chrome(
|
||||
show_minimize: bool = False,
|
||||
allow_close: bool = True,
|
||||
) -> DashboardChrome:
|
||||
"""Install polished managed chrome around one launcher-owned Tk window."""
|
||||
"""Install the launcher's custom frame and window controls."""
|
||||
|
||||
import tkinter as tk
|
||||
ui_scale = widget_ui_scale(root)
|
||||
|
||||
managed, identity_configured = _configure_x11_window(
|
||||
root,
|
||||
@@ -338,7 +350,7 @@ def create_window_chrome(
|
||||
title_bar = tk.Frame(
|
||||
surface,
|
||||
background="#17100d",
|
||||
height=32,
|
||||
height=round(32 * ui_scale),
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
cursor="fleur",
|
||||
@@ -354,7 +366,7 @@ def create_window_chrome(
|
||||
factor = icon_subsample_factor(
|
||||
window_icon.width(),
|
||||
window_icon.height(),
|
||||
20,
|
||||
round(20 * ui_scale),
|
||||
)
|
||||
title_icon = window_icon.subsample(factor, factor)
|
||||
except (tk.TclError, ValueError):
|
||||
@@ -363,8 +375,8 @@ def create_window_chrome(
|
||||
mark = tk.Label(
|
||||
title_bar,
|
||||
image=title_icon,
|
||||
width=22,
|
||||
height=22,
|
||||
width=round(22 * ui_scale),
|
||||
height=round(22 * ui_scale),
|
||||
background="#17100d",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
@@ -374,8 +386,8 @@ def create_window_chrome(
|
||||
else:
|
||||
mark = tk.Frame(
|
||||
title_bar,
|
||||
width=22,
|
||||
height=22,
|
||||
width=round(22 * ui_scale),
|
||||
height=round(22 * ui_scale),
|
||||
background="#17100d",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
@@ -414,10 +426,12 @@ def create_window_chrome(
|
||||
font=("Sans", 12),
|
||||
width=4,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
highlightthickness=2,
|
||||
highlightbackground="#17100d",
|
||||
highlightcolor="#f4bd61",
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
takefocus=False,
|
||||
takefocus=True,
|
||||
)
|
||||
close_button.pack(side="right", fill="y")
|
||||
minimize_button: Any = None
|
||||
@@ -432,10 +446,12 @@ def create_window_chrome(
|
||||
font=("Sans", 10, "bold"),
|
||||
width=4,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
highlightthickness=2,
|
||||
highlightbackground="#17100d",
|
||||
highlightcolor="#f4bd61",
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
takefocus=False,
|
||||
takefocus=True,
|
||||
)
|
||||
minimize_button.pack(side="right", fill="y")
|
||||
content = tk.Frame(
|
||||
@@ -489,9 +505,10 @@ def show_themed_dialog(
|
||||
cancel_text: str = "CANCEL",
|
||||
window_icon: Any | None = None,
|
||||
) -> bool:
|
||||
"""Show one frameless modal dialog matching the native launcher palette."""
|
||||
"""Show one custom-framed modal dialog matching the launcher palette."""
|
||||
|
||||
import tkinter as tk
|
||||
ui_scale = widget_ui_scale(parent)
|
||||
|
||||
palette = dialog_palette(kind)
|
||||
dialog = tk.Toplevel(parent)
|
||||
@@ -524,7 +541,11 @@ def show_themed_dialog(
|
||||
border.pack(fill="both", expand=True)
|
||||
surface = tk.Frame(border, background="#191311")
|
||||
surface.pack(fill="both", expand=True, padx=1, pady=1)
|
||||
header = tk.Frame(surface, background="#24170f", height=66)
|
||||
header = tk.Frame(
|
||||
surface,
|
||||
background="#24170f",
|
||||
height=round(66 * ui_scale),
|
||||
)
|
||||
header.pack(fill="x")
|
||||
header.pack_propagate(False)
|
||||
tk_accent = tk.Frame(header, background=palette.accent, width=5)
|
||||
@@ -560,21 +581,24 @@ def show_themed_dialog(
|
||||
font=("Sans", 12),
|
||||
width=4,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
highlightthickness=2,
|
||||
highlightbackground="#24170f",
|
||||
highlightcolor="#f4bd61",
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
takefocus=False,
|
||||
takefocus=True,
|
||||
)
|
||||
close_button.pack(side="right", fill="y")
|
||||
body = tk.Frame(surface, background="#191311", padx=20, pady=20)
|
||||
body.pack(fill="both", expand=True)
|
||||
modal_icon_size = round(46 * ui_scale)
|
||||
modal_icon: Any | None = None
|
||||
if window_icon is not None:
|
||||
try:
|
||||
factor = icon_subsample_factor(
|
||||
window_icon.width(),
|
||||
window_icon.height(),
|
||||
46,
|
||||
modal_icon_size,
|
||||
)
|
||||
modal_icon = window_icon.subsample(factor, factor)
|
||||
except (tk.TclError, ValueError):
|
||||
@@ -582,8 +606,8 @@ def show_themed_dialog(
|
||||
if modal_icon is not None:
|
||||
icon_host = tk.Frame(
|
||||
body,
|
||||
width=46,
|
||||
height=46,
|
||||
width=modal_icon_size,
|
||||
height=modal_icon_size,
|
||||
background="#191311",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
@@ -601,24 +625,24 @@ def show_themed_dialog(
|
||||
else:
|
||||
icon = tk.Canvas(
|
||||
body,
|
||||
width=46,
|
||||
height=46,
|
||||
width=modal_icon_size,
|
||||
height=modal_icon_size,
|
||||
background="#191311",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
)
|
||||
icon.create_oval(
|
||||
3,
|
||||
3,
|
||||
43,
|
||||
43,
|
||||
round(3 * ui_scale),
|
||||
round(3 * ui_scale),
|
||||
modal_icon_size - round(3 * ui_scale),
|
||||
modal_icon_size - round(3 * ui_scale),
|
||||
fill=palette.accent_soft,
|
||||
outline=palette.accent,
|
||||
width=2,
|
||||
)
|
||||
icon.create_text(
|
||||
23,
|
||||
23,
|
||||
modal_icon_size // 2,
|
||||
modal_icon_size // 2,
|
||||
text=palette.glyph,
|
||||
fill=palette.accent,
|
||||
font=("Sans", 17, "bold"),
|
||||
@@ -632,7 +656,7 @@ def show_themed_dialog(
|
||||
font=("Sans", 10),
|
||||
justify="left",
|
||||
anchor="w",
|
||||
wraplength=420,
|
||||
wraplength=round(420 * ui_scale),
|
||||
)
|
||||
message_label.pack(side="left", fill="both", expand=True, anchor="n")
|
||||
footer = tk.Frame(surface, background="#100d0c", padx=18, pady=12)
|
||||
@@ -660,7 +684,9 @@ def show_themed_dialog(
|
||||
padx=16,
|
||||
pady=7,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
highlightthickness=2,
|
||||
highlightbackground="#100d0c",
|
||||
highlightcolor="#f4bd61",
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
)
|
||||
@@ -681,12 +707,16 @@ def show_themed_dialog(
|
||||
)
|
||||
cancel_button.pack(side="right", padx=(0, 8))
|
||||
_bind_window_drag(dialog, header, header_copy, eyebrow, title_label)
|
||||
dialog.protocol("WM_DELETE_WINDOW", lambda: finish(False))
|
||||
dialog.bind("<Escape>", lambda _event: finish(False))
|
||||
dialog.bind("<Return>", lambda _event: finish(True))
|
||||
parent.update_idletasks()
|
||||
dialog.update_idletasks()
|
||||
width = 560
|
||||
height = max(218, dialog.winfo_reqheight())
|
||||
width = min(round(560 * ui_scale), max(1, dialog.winfo_screenwidth() - 64))
|
||||
height = min(
|
||||
max(round(218 * ui_scale), dialog.winfo_reqheight()),
|
||||
max(1, dialog.winfo_screenheight() - 64),
|
||||
)
|
||||
dialog.geometry(
|
||||
centered_window_geometry(
|
||||
width,
|
||||
|
||||
975
ptlinux/ui.py
975
ptlinux/ui.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user