Merge pull request #2525 from HullSeals/enhancement/2116/additional-tk-to-ttk

Updater Refactor and Additional TK->TTK
This commit is contained in:
David Sangrey authored and GitHub committed 2025-11-13 21:04:04 -05:00
commit d9e202b968
12 files changed
+159 -166

No files matched your search

+1 -1
View File
@@ -2052,7 +2052,7 @@ def show_killswitch_poppup(root=None):
idx += 1
idx += 1
ok_button = tk.Button(frame, text="Ok", command=tl.destroy)
ok_button = ttk.Button(frame, text="Ok", command=tl.destroy)
ok_button.grid(columnspan=2, sticky=tk.EW)
+2 -2
View File
@@ -38,8 +38,8 @@ You will need several pieces of software installed, or the files from their
auto-select some others). NB: If you have need to uninstall this it's
"Windows Software Development Kit - Windows 10.0.19041.1" in
"Apps & Features", *not* "Windows SDK AddOn".
1. [Python](https://python.org): 32-bit version of Python 3.10 for Windows.
[v3.10.3](https://www.python.org/downloads/release/python-3103/) is the most
1. [Python](https://python.org): 32-bit version of Python 3.13 for Windows.
[v3.13.9](https://www.python.org/downloads/release/python-3139/) is the most
recently tested version. You need the `Windows x86 executable installer`
file, for the 32-bit version. Double-check the version against the
`.python.version` file, as it should always contain the intended version.
+8 -13
View File
@@ -41,17 +41,14 @@ class Frame(ttk.Frame):
"""Custom ttk.Frame class to fix some display issues."""
def __init__(self, master: ttk.Notebook | None = None, **kw):
if sys.platform == 'win32':
ttk.Frame.__init__(self, master, style='nb.TFrame', **kw)
ttk.Frame(self).grid(pady=5) # top spacer
else:
ttk.Frame.__init__(self, master, **kw)
ttk.Frame(self).grid(pady=5) # top spacer
self.configure(takefocus=1) # let the frame take focus so that no particular child is focused
style = 'nb.TFrame' if sys.platform == 'win32' else None
super().__init__(master, style=style, **kw) # type: ignore
ttk.Frame(self).grid(pady=5) # Top spacer
self.configure(takefocus=1) # let the frame take focus so that no particular child is focused
class Label(tk.Label):
"""Custom tk.Label class to fix some display issues."""
class Label(ttk.Label):
"""Custom ttk.Label class to fix some display issues."""
def __init__(self, master: ttk.Frame | None = None, **kw):
kw['foreground'] = kw.pop('foreground', PAGEFG if sys.platform == 'win32'
@@ -134,10 +131,8 @@ class Button(ttk.Button):
"""Custom ttk.Button class to fix some display issues."""
def __init__(self, master: ttk.Frame | None = None, **kw):
if sys.platform == 'win32':
ttk.Button.__init__(self, master, style='nb.TButton', **kw)
else:
ttk.Button.__init__(self, master, **kw)
style = 'nb.TButton' if sys.platform == 'win32' else None
super().__init__(master, style=style, **kw) # type: ignore
class ColoredButton(tk.Button):
+2 -2
View File
@@ -302,8 +302,8 @@ def export(data, filename) -> None:
try:
m = lookup(v, ship_name_map)
if m:
h.write(f'{rowheader}, {m["category"]}, {m["name"]}, {m.get("mount","")},'
f'{m.get("guidance","")}, {m.get("ship","")}, {m["class"]}, {m["rating"]},'
h.write(f'{rowheader}, {m["category"]}, {m["name"]}, {m.get("mount", "")},'
f'{m.get("guidance", "")}, {m.get("ship", "")}, {m["class"]}, {m["rating"]},'
f'{m["id"]}, {data["timestamp"]}\n')
except ValueError as e:
+4 -5
View File
@@ -13,6 +13,7 @@ import logging
import operator
import os
import sys
from dataclasses import dataclass, field
import tkinter as tk
from datetime import datetime, timedelta, timezone
from pathlib import Path
@@ -33,14 +34,12 @@ PLUGINS_broken = []
# For asynchronous error display
@dataclass
class LastError:
"""Holds the last plugin error."""
msg: str | None
root: tk.Tk
def __init__(self) -> None:
self.msg = None
msg: str | None = None
root: tk.Tk | None = field(default=None, repr=False) # not initialized by default, hide in repr
last_error = LastError()
+28 -27
View File
@@ -29,6 +29,7 @@ import pathlib
import re
import sqlite3
import tkinter as tk
from tkinter import ttk
import warnings
from platform import system
from textwrap import dedent
@@ -115,15 +116,15 @@ class This:
# Tracking UI
self.ui: tk.Frame
self.ui_system_name: tk.Label
self.ui_system_address: tk.Label
self.ui_j_body_name: tk.Label
self.ui_j_body_id: tk.Label
self.ui_j_body_type: tk.Label
self.ui_s_body_name: tk.Label
self.ui_station_name: tk.Label
self.ui_station_type: tk.Label
self.ui_station_marketid: tk.Label
self.ui_system_name: ttk.Label
self.ui_system_address: ttk.Label
self.ui_j_body_name: ttk.Label
self.ui_j_body_id: ttk.Label
self.ui_j_body_type: ttk.Label
self.ui_s_body_name: ttk.Label
self.ui_station_name: ttk.Label
self.ui_station_type: ttk.Label
self.ui_station_marketid: ttk.Label
this = This()
@@ -2027,15 +2028,15 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None:
# System
#######################################################################
# SystemName
system_name_label = tk.Label(this.ui, text="J:SystemName:")
system_name_label = ttk.Label(this.ui, text="J:SystemName:")
system_name_label.grid(row=row, column=0, sticky=tk.W)
this.ui_system_name = tk.Label(this.ui, name='eddn_track_system_name', anchor=tk.W)
this.ui_system_name = ttk.Label(this.ui, name='eddn_track_system_name', anchor=tk.W)
this.ui_system_name.grid(row=row, column=1, sticky=tk.E)
row += 1
# SystemAddress
system_address_label = tk.Label(this.ui, text="J:SystemAddress:")
system_address_label = ttk.Label(this.ui, text="J:SystemAddress:")
system_address_label.grid(row=row, column=0, sticky=tk.W)
this.ui_system_address = tk.Label(this.ui, name='eddn_track_system_address', anchor=tk.W)
this.ui_system_address = ttk.Label(this.ui, name='eddn_track_system_address', anchor=tk.W)
this.ui_system_address.grid(row=row, column=1, sticky=tk.E)
row += 1
#######################################################################
@@ -2044,27 +2045,27 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None:
# Body
#######################################################################
# Body Name from Journal
journal_body_name_label = tk.Label(this.ui, text="J:BodyName:")
journal_body_name_label = ttk.Label(this.ui, text="J:BodyName:")
journal_body_name_label.grid(row=row, column=0, sticky=tk.W)
this.ui_j_body_name = tk.Label(this.ui, name='eddn_track_j_body_name', anchor=tk.W)
this.ui_j_body_name = ttk.Label(this.ui, name='eddn_track_j_body_name', anchor=tk.W)
this.ui_j_body_name.grid(row=row, column=1, sticky=tk.E)
row += 1
# Body ID from Journal
journal_body_id_label = tk.Label(this.ui, text="J:BodyID:")
journal_body_id_label = ttk.Label(this.ui, text="J:BodyID:")
journal_body_id_label.grid(row=row, column=0, sticky=tk.W)
this.ui_j_body_id = tk.Label(this.ui, name='eddn_track_j_body_id', anchor=tk.W)
this.ui_j_body_id = ttk.Label(this.ui, name='eddn_track_j_body_id', anchor=tk.W)
this.ui_j_body_id.grid(row=row, column=1, sticky=tk.E)
row += 1
# Body Type from Journal
journal_body_type_label = tk.Label(this.ui, text="J:BodyType:")
journal_body_type_label = ttk.Label(this.ui, text="J:BodyType:")
journal_body_type_label.grid(row=row, column=0, sticky=tk.W)
this.ui_j_body_type = tk.Label(this.ui, name='eddn_track_j_body_type', anchor=tk.W)
this.ui_j_body_type = ttk.Label(this.ui, name='eddn_track_j_body_type', anchor=tk.W)
this.ui_j_body_type.grid(row=row, column=1, sticky=tk.E)
row += 1
# Body Name from Status.json
status_body_name_label = tk.Label(this.ui, text="S:BodyName:")
status_body_name_label = ttk.Label(this.ui, text="S:BodyName:")
status_body_name_label.grid(row=row, column=0, sticky=tk.W)
this.ui_s_body_name = tk.Label(this.ui, name='eddn_track_s_body_name', anchor=tk.W)
this.ui_s_body_name = ttk.Label(this.ui, name='eddn_track_s_body_name', anchor=tk.W)
this.ui_s_body_name.grid(row=row, column=1, sticky=tk.E)
row += 1
#######################################################################
@@ -2073,21 +2074,21 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None:
# Station
#######################################################################
# Name
status_station_name_label = tk.Label(this.ui, text="J:StationName:")
status_station_name_label = ttk.Label(this.ui, text="J:StationName:")
status_station_name_label.grid(row=row, column=0, sticky=tk.W)
this.ui_station_name = tk.Label(this.ui, name='eddn_track_station_name', anchor=tk.W)
this.ui_station_name = ttk.Label(this.ui, name='eddn_track_station_name', anchor=tk.W)
this.ui_station_name.grid(row=row, column=1, sticky=tk.E)
row += 1
# Type
status_station_type_label = tk.Label(this.ui, text="J:StationType:")
status_station_type_label = ttk.Label(this.ui, text="J:StationType:")
status_station_type_label.grid(row=row, column=0, sticky=tk.W)
this.ui_station_type = tk.Label(this.ui, name='eddn_track_station_type', anchor=tk.W)
this.ui_station_type = ttk.Label(this.ui, name='eddn_track_station_type', anchor=tk.W)
this.ui_station_type.grid(row=row, column=1, sticky=tk.E)
row += 1
# MarketID
status_station_marketid_label = tk.Label(this.ui, text="J:StationID:")
status_station_marketid_label = ttk.Label(this.ui, text="J:StationID:")
status_station_marketid_label.grid(row=row, column=0, sticky=tk.W)
this.ui_station_marketid = tk.Label(this.ui, name='eddn_track_station_id', anchor=tk.W)
this.ui_station_marketid = ttk.Label(this.ui, name='eddn_track_station_id', anchor=tk.W)
this.ui_station_marketid.grid(row=row, column=1, sticky=tk.E)
row += 1
#######################################################################
+5 -5
View File
@@ -90,11 +90,11 @@ class This:
self.newgame: bool = False # starting up - batch initial burst of events
self.newgame_docked: bool = False # starting up while docked
self.navbeaconscan: int = 0 # batch up burst of Scan events after NavBeaconScan
self.system_link: tk.Widget | None = None
self.system_link: ttk.Widget | None = None
self.system_name: tk.Tk | None = None
self.system_address: int | None = None # Frontier SystemAddress
self.system_population: int | None = None
self.station_link: tk.Widget | None = None
self.station_link: ttk.Widget | None = None
self.station_name: str | None = None
self.station_marketid: int | None = None # Frontier MarketID
self.on_foot = False
@@ -109,7 +109,7 @@ class This:
self.log: tk.IntVar | None = None
self.log_button: ttk.Checkbutton | None = None
self.label: tk.Widget | None = None
self.label: ttk.Widget | None = None
self.cmdr_label: nb.Label | None = None
self.cmdr_text: nb.Label | None = None
@@ -729,8 +729,8 @@ def send_to_edsm( # noqa: CCR001
# Respect rate limits if they exist
if remaining == 0:
# Calculate sleep time until the rate limit reset time
reset_time = datetime.utcfromtimestamp(reset)
current_time = datetime.utcnow()
reset_time = datetime.fromtimestamp(reset, tz=timezone.utc)
current_time = datetime.now(timezone.utc)
sleep_time = (reset_time - current_time).total_seconds()
+5 -5
View File
@@ -113,20 +113,20 @@ class This:
self.timer_run = True
# Main window clicks
self.system_link: tk.Widget = None # type: ignore
self.system_link: ttk.Widget = None # type: ignore
self.system_name: str | None = None # type: ignore
self.system_address: str | None = None # type: ignore
self.system_population: int | None = None
self.station_link: tk.Widget = None # type: ignore
self.station_link: ttk.Widget = None # type: ignore
self.station_name = None
self.station_marketid = None
# Prefs UI
self.log: 'tk.IntVar'
self.log: tk.IntVar
self.log_button: nb.Checkbutton
self.label: HyperlinkLabel
self.apikey: nb.EntryMenu
self.apikey_label: tk.Label
self.apikey_label: ttk.Label
self.events: dict[Credentials, Deque[Event]] = defaultdict(deque)
self.event_lock: Lock = threading.Lock() # protects events, for use when rewriting events
@@ -1610,7 +1610,7 @@ def handle_api_error(data: Mapping[str, Any], status: int, reply: dict[str, Any]
"""
error_message = reply['header'].get('eventStatusText', "")
logger.warning(f'Inara\t{status} {error_message}')
logger.debug(f'JSON data:\n{json.dumps(data, indent=2, separators = (",", ": "))}')
logger.debug(f'JSON data:\n{json.dumps(data, indent=2, separators=(",", ": "))}')
# LANG: INARA API returned some kind of error (error message will be contained in {MSG})
plug.show_error(tr.tl('Error: Inara {MSG}').format(MSG=error_message))
+3 -2
View File
@@ -22,6 +22,7 @@ referenced in this file (or only in any other core plugin), and if so...
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from typing import Any
import requests
from companion import CAPIData
@@ -39,11 +40,11 @@ class This:
def __init__(self):
self.parent: tk.Tk
self.shutting_down = False # Plugin is shutting down.
self.system_link: tk.Widget = None # type: ignore
self.system_link: ttk.Widget = None # type: ignore
self.system_name: str | None = None
self.system_address: str | None = None
self.system_population: int | None = None
self.station_link: tk.Widget = None # type: ignore
self.station_link: ttk.Widget = None # type: ignore
self.station_name = None
self.station_marketid = None
self.on_foot = False
+1 -1
View File
@@ -457,7 +457,7 @@ class StatsResults(tk.Toplevel):
# label = HyperlinkLabel(parent, text=col_content, popup_copy=True)
label = nb.Label(parent, text=col_content)
if with_copy:
label.bind('<Button-1>', self.copy_callback(label, col_content))
label.bind('<Button-1>', self.copy_callback(label, col_content)) # type: ignore
if i == 0:
label.grid(padx=10, sticky=tk.W)
+2 -2
View File
@@ -117,7 +117,7 @@ class TestNewConfig:
@mark.parametrize("lst", _build_test_list(list_tests, _get_fuzz(list)))
def test_list(self, lst: list[str]) -> None:
"""Save a list and then ask for it back."""
name = f'list_test_{ hash("".join(lst)) }'
name = f'list_test_{hash("".join(lst))}'
config.set(name, lst)
config.save()
@@ -222,7 +222,7 @@ class TestOldNewConfig:
# So insert those spaces here, to ensure that it works otherwise.
lst = [e if len(e) > 0 else ' ' for e in lst]
name = self.KEY_PREFIX + f'list_test_{ hash("".join(lst)) }'
name = self.KEY_PREFIX + f'list_test_{hash("".join(lst))}'
old_config.set(name, lst)
old_config.save()
+98 -101
View File
@@ -12,9 +12,10 @@ import shutil
import sys
import threading
import os
from dataclasses import dataclass
from tkinter import messagebox
from traceback import print_exc
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast, Any
from xml.etree import ElementTree
import requests
import semantic_version
@@ -25,58 +26,68 @@ from l10n import translations as tr
if TYPE_CHECKING:
import tkinter as tk
logger = get_main_logger()
def check_for_fdev_updates(silent: bool = False, local: bool = False) -> None: # noqa: CCR001
def read_normalized_file(path: pathlib.Path) -> str:
"""Read a UTF-8 (with BOM-safe) file and normalize line endings and whitespace."""
try:
return path.read_bytes().decode('utf-8-sig').replace('\r\n', '\n').strip()
except FileNotFoundError:
return ''
def copy_bundle_file(filename: str, dest_dir: pathlib.Path) -> str:
"""Attempt to copy a missing file from the bundled FDevIDs folder."""
try:
shutil.copy(f"FDevIDs/{filename}", dest_dir)
return read_normalized_file(dest_dir / filename)
except (FileNotFoundError, shutil.SameFileError):
logger.info("Bundle copy failed or identical; continuing with empty content.")
return ''
def fetch_remote_file(url: str) -> str | None:
"""Fetch a remote file and normalize its content."""
try:
response = requests.get(url, timeout=20)
response.raise_for_status()
return response.text.replace('\r\n', '\n').strip()
except requests.RequestException:
return None
def check_for_fdev_updates(silent: bool = False, local: bool = False) -> None:
"""Check for and download FDEV ID file updates."""
pathway = config.respath_path if local else config.app_dir_path
base_path = config.respath_path if local else config.app_dir_path
fdevid_dir = pathlib.Path(base_path, 'FDevIDs')
fdevid_dir.mkdir(parents=True, exist_ok=True)
files_urls = [
('commodity.csv', 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/commodity.csv'),
('rare_commodity.csv', 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/rare_commodity.csv')
]
files_urls = {
'commodity.csv': 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/commodity.csv',
'rare_commodity.csv': 'https://raw.githubusercontent.com/EDCD/FDevIDs/master/rare_commodity.csv'
}
for file, url in files_urls:
fdevid_file = pathlib.Path(pathway / 'FDevIDs' / file)
fdevid_file.parent.mkdir(parents=True, exist_ok=True)
try:
with open(fdevid_file, newline='', encoding='utf-8') as f:
local_content = f.read()
except FileNotFoundError:
logger.info(f'File {file} not found. Writing from bundle...')
try:
for localfile in files_urls:
filepath = pathlib.Path(f"FDevIDs/{localfile[0]}")
try:
shutil.copy(filepath, pathway / 'FDevIDs')
except shutil.SameFileError:
logger.info("Not replacing same file...")
fdevid_file = pathlib.Path(pathway / 'FDevIDs' / file)
with open(fdevid_file, newline='', encoding='utf-8') as f:
local_content = f.read()
except FileNotFoundError:
local_content = None
try:
response = requests.get(url, timeout=20)
response.raise_for_status()
except requests.RequestException:
for filename, url in files_urls.items():
file_path = fdevid_dir / filename
local_content = read_normalized_file(file_path) or copy_bundle_file(filename, fdevid_dir)
remote_content = fetch_remote_file(url)
if not remote_content:
if not silent:
logger.error(f'Failed to download {file}! Unable to continue.')
logger.error(f'Failed to download {filename}! Unable to continue.')
continue
if local_content == response.text:
if local_content == remote_content:
if not silent:
logger.info(f'FDEV ID file {file} already up to date.')
else:
if not silent:
logger.info(f'FDEV ID file {file} not up to date. Downloading...')
with open(fdevid_file, 'w', newline='', encoding='utf-8') as csvfile:
csvfile.write(response.text)
logger.info(f'FDEV ID file {filename} already up to date.')
continue
if not silent:
logger.info(f'Updating FDEV ID file {filename}...')
file_path.write_text(remote_content, encoding='utf-8', newline='\n')
@dataclass(slots=True)
class EDMCVersion:
"""
Hold all the information about an EDMC version.
@@ -91,10 +102,9 @@ class EDMCVersion:
semantic_version object for this version
"""
def __init__(self, version: str, title: str, sv: semantic_version.base.Version):
self.version: str = version
self.title: str = title
self.sv: semantic_version.base.Version = sv
version: str
title: str
sv: semantic_version.Version
class Updater:
@@ -105,6 +115,21 @@ class Updater:
WinSparkle on win32.
"""
def __init__(self, tkroot: tk.Tk | None = None, provider: str = 'internal'):
"""
Initialise an Updater instance.
:param tkroot: reference to the root window of the GUI
:param provider: 'internal' or other string if not
"""
self.root: tk.Tk | None = tkroot
self.provider: str = provider
self.thread: threading.Thread | None = None
self.updater: Any | None = None # ensure attribute exists
if not self.use_internal() and sys.platform == 'win32':
self._init_winsparkle()
def shutdown_request(self) -> None:
"""Receive (Win)Sparkle shutdown request and send it to parent."""
if not config.shutting_down and self.root:
@@ -116,61 +141,38 @@ class Updater:
:return: bool
"""
if self.provider == 'internal':
return True
return self.provider == 'internal'
return False
def _init_winsparkle(self) -> None:
"""Initialize WinSparkle updater for Windows."""
import ctypes
try:
self.updater = cast(ctypes.CDLL, ctypes.cdll.WinSparkle)
self.updater.win_sparkle_set_appcast_url(get_update_feed().encode()) # Set the appcast URL
def __init__(self, tkroot: tk.Tk | None = None, provider: str = 'internal'):
"""
Initialise an Updater instance.
# Set the appversion *without* build metadata, as WinSparkle
# doesn't do proper Semantic Version checks.
# NB: It 'accidentally' supports pre-release due to how it
# splits and compares strings:
# <https://github.com/vslavik/winsparkle/issues/214>
self.updater.win_sparkle_set_app_build_version(str(appversion_nobuild()))
:param tkroot: reference to the root window of the GUI
:param provider: 'internal' or other string if not
"""
self.root: tk.Tk | None = tkroot
self.provider: str = provider
self.thread: threading.Thread | None = None
# set up shutdown callback
self.callback_t = ctypes.CFUNCTYPE(None) # keep reference
self.callback_fn = self.callback_t(self.shutdown_request)
self.updater.win_sparkle_set_shutdown_request_callback(self.callback_fn)
if self.use_internal():
return
# Get WinSparkle running
self.updater.win_sparkle_init()
if sys.platform == 'win32':
import ctypes
try:
self.updater: ctypes.CDLL | None = ctypes.cdll.WinSparkle
# Set the appcast URL
self.updater.win_sparkle_set_appcast_url(get_update_feed().encode())
# Set the appversion *without* build metadata, as WinSparkle
# doesn't do proper Semantic Version checks.
# NB: It 'accidentally' supports pre-release due to how it
# splits and compares strings:
# <https://github.com/vslavik/winsparkle/issues/214>
self.updater.win_sparkle_set_app_build_version(str(appversion_nobuild()))
# set up shutdown callback
self.callback_t = ctypes.CFUNCTYPE(None) # keep reference
self.callback_fn = self.callback_t(self.shutdown_request)
self.updater.win_sparkle_set_shutdown_request_callback(self.callback_fn)
# Get WinSparkle running
self.updater.win_sparkle_init()
except Exception:
print_exc()
self.updater = None
if not os.getenv("EDMC_NO_UI"):
messagebox.showerror(
title=appname,
message="Updater Failed to Initialize. Please file a bug report!"
)
else:
logger.error("Updater Failed to Initialize. Please file a bug report!")
return
except Exception:
print_exc()
self.updater = None
msg = "Updater Failed to Initialize. Please file a bug report!"
if not os.getenv("EDMC_NO_UI"):
messagebox.showerror(title=appname, message=msg)
else:
logger.error(msg)
def set_automatic_updates_check(self, onoroff: bool) -> None:
"""
@@ -187,10 +189,8 @@ class Updater:
def check_for_updates(self) -> None:
"""Trigger the requisite method to check for an update."""
if self.use_internal():
self.thread = threading.Thread(target=self.worker, name='update worker')
self.thread.daemon = True
self.thread = threading.Thread(target=self.worker, name='update worker', daemon=True)
self.thread.start()
elif sys.platform == 'win32' and self.updater:
self.updater.win_sparkle_check_update_with_ui()
@@ -218,7 +218,6 @@ class Updater:
except requests.RequestException as ex:
logger.exception(f'Error retrieving update_feed file: {ex}')
return None
try:
@@ -226,7 +225,6 @@ class Updater:
except SyntaxError as ex:
logger.exception(f'Syntax error in update_feed file: {ex}')
return None
# For *these* purposes all systems are the same as 'windows', as
@@ -270,7 +268,6 @@ class Updater:
# LANG: Update Available Text
status['text'] = tr.tl("{NEWVER} is available").format(NEWVER=newversion.title)
self.root.update_idletasks()
else:
logger.info("No new version available at this time")