1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-04 19:40:02 +03:00
2024-09-30 18:42:09 -04:00

1329 lines
57 KiB
Python

# -*- coding: utf-8 -*-
"""EDMC preferences library."""
from __future__ import annotations
import contextlib
import logging
from os.path import expandvars, join, normpath
from pathlib import Path
import subprocess
import sys
import tkinter as tk
import warnings
from os import system
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
from tkinter import ttk
from types import TracebackType
from typing import Any, Callable, Optional, Type
import myNotebook as nb # noqa: N813
import plug
from config import appversion_nobuild, config
from EDMCLogging import edmclogger, get_main_logger
from hotkey import hotkeymgr
from l10n import translations as tr
from monitor import monitor
from theme import theme
from ttkHyperlinkLabel import HyperlinkLabel
logger = get_main_logger()
# TODO: Decouple this from platform as far as possible
###########################################################################
# Versioned preferences, so we know whether to set an 'on' default on
# 'new' preferences, or not.
###########################################################################
# May be imported by plugins
# DEPRECATED: Migrate to open_log_folder. Will remove in 6.0 or later.
def help_open_log_folder() -> None:
"""Open the folder logs are stored in."""
warnings.warn('prefs.help_open_log_folder is deprecated, use open_log_folder instead. '
'This function will be removed in 6.0 or later', DeprecationWarning, stacklevel=2)
open_folder(Path(config.app_dir_path / 'logs'))
def open_folder(file: Path) -> None:
"""Open the given file in the OS file explorer."""
if sys.platform.startswith('win'):
# On Windows, use the "start" command to open the folder
system(f'start "" "{file}"')
elif sys.platform.startswith('linux'):
# On Linux, use the "xdg-open" command to open the folder
system(f'xdg-open "{file}"')
def help_open_system_profiler(parent) -> None:
"""Open the EDMC System Profiler."""
profiler_path = config.respath_path
try:
if getattr(sys, 'frozen', False):
profiler_path /= 'EDMCSystemProfiler.exe'
subprocess.run(profiler_path, check=True)
else:
subprocess.run(['python', "EDMCSystemProfiler.py"], shell=True, check=True)
except Exception as err:
parent.status["text"] = tr.tl("Error in System Profiler") # LANG: Catch & Record Profiler Errors
logger.exception(err)
class PrefsVersion:
"""
PrefsVersion contains versioned preferences.
It allows new defaults to be set as they are added if they are found to be missing
"""
versions = {
'0.0.0.0': 1,
'1.0.0.0': 2,
'3.4.6.0': 3,
'3.5.1.0': 4,
# Only add new versions that add new Preferences
# Should always match the last specific version, but only increment after you've added the new version.
# Guess at it if anticipating a new version.
'current': 4,
}
def __init__(self):
return
def stringToSerial(self, versionStr: str) -> int: # noqa: N802, N803 # used in plugins
"""
Convert a version string into a preferences version serial number.
If the version string isn't known returns the 'current' (latest) serial number.
:param versionStr:
:return int:
"""
if versionStr in self.versions:
return self.versions[versionStr]
return self.versions['current']
def shouldSetDefaults(self, addedAfter: str, oldTest: bool = True) -> bool: # noqa: N802,N803 # used in plugins
"""
Whether or not defaults should be set if they were added after the specified version.
:param addedAfter: The version after which these settings were added
:param oldTest: Default, if we have no current settings version, defaults to True
:raises ValueError: on serial number after the current latest
:return: bool indicating the answer
"""
# config.get('PrefsVersion') is the version preferences we last saved for
pv = config.get_int('PrefsVersion')
# If no PrefsVersion yet exists then return oldTest
if not pv:
return oldTest
# Convert addedAfter to a version serial number
if addedAfter not in self.versions:
# Assume it was added at the start
aa = 1
else:
aa = self.versions[addedAfter]
# Sanity check, if something was added after then current should be greater
if aa >= self.versions['current']:
raise ValueError(
'ERROR: Call to prefs.py:PrefsVersion.shouldSetDefaults() with '
'"addedAfter" >= current latest in "versions" table.'
' You probably need to increase "current" serial number.'
)
# If this preference was added after the saved PrefsVersion we should set defaults
if aa >= pv:
return True
return False
prefsVersion = PrefsVersion() # noqa: N816 # Cannot rename as used in plugins
class AutoInc(contextlib.AbstractContextManager):
"""
Autoinc is a self incrementing int.
As a context manager, it increments on enter, and does nothing on exit.
"""
def __init__(self, start: int = 0, step: int = 1) -> None:
self.current = start
self.step = step
def get(self, increment=True) -> int:
"""
Get the current integer, optionally incrementing it.
:param increment: whether or not to increment the stored value, defaults to True
:return: the current value
"""
current = self.current
if increment:
self.current += self.step
return current
def __enter__(self):
"""
Increments once, alias to .get.
:return: the current value
"""
return self.get()
def __exit__(
self,
exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]
) -> Optional[bool]:
"""Do nothing."""
return None
if sys.platform == 'win32':
import ctypes
import winreg
from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL
import win32gui
import win32api
is_wine = False
try:
WINE_REGISTRY_KEY = r'HKEY_LOCAL_MACHINE\Software\Wine'
reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
winreg.OpenKey(reg, WINE_REGISTRY_KEY)
is_wine = True
except OSError: # Assumed to be 'path not found', i.e. not-wine
pass
CalculatePopupWindowPosition = None
if not is_wine:
try:
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
CalculatePopupWindowPosition.argtypes = [POINT, SIZE, UINT, RECT, RECT]
CalculatePopupWindowPosition.restype = BOOL
except AttributeError as e:
logger.error(
'win32 and not is_wine, but ctypes.windll.user32.CalculatePopupWindowPosition invalid',
exc_info=e
)
else:
CalculatePopupWindowPosition.argtypes = [
ctypes.POINTER(POINT),
ctypes.POINTER(SIZE),
UINT,
ctypes.POINTER(RECT),
ctypes.POINTER(RECT)
]
SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName
SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)]
class PreferencesDialog(tk.Toplevel):
"""The EDMC preferences dialog."""
def __init__(self, parent: tk.Tk, callback: Optional[Callable]):
super().__init__(parent)
self.parent = parent
self.callback = callback
self.req_restart = False
# LANG: File > Settings (macOS)
self.title(tr.tl('Settings'))
if parent.winfo_viewable():
self.transient(parent)
# Position over parent
self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}')
# Remove decoration
if sys.platform == 'win32':
self.attributes('-toolwindow', tk.TRUE)
# Allow the window to be resizable
self.resizable(tk.TRUE, tk.TRUE)
self.cmdr: str | bool | None = False # Note if Cmdr changes in the Journal
self.is_beta: bool = False # Note if Beta status changes in the Journal
self.cmdrchanged_alarm: Optional[str] = None # This stores an ID that can be used to cancel a scheduled call
# Set up the main frame
frame = ttk.Frame(self)
frame.grid(sticky=tk.NSEW)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(0, weight=1)
frame.rowconfigure(1, weight=0)
notebook: nb.Notebook = nb.Notebook(frame)
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
self.PADX = 10
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
self.LISTX = 25 # indent listed items
self.PADY = 1 # close spacing
self.BOXY = 2 # box spacing
self.SEPY = 10 # separator line spacing
# Set up different tabs
self.__setup_config_tab(notebook)
self.__setup_appearance_tab(notebook)
self.__setup_output_tab(notebook)
self.__setup_privacy_tab(notebook)
self.__setup_plugin_tab(notebook)
self.__setup_plugin_tabs(notebook)
# Set up the button frame
buttonframe = ttk.Frame(frame)
buttonframe.grid(padx=self.PADX, pady=self.PADX, sticky=tk.NSEW)
buttonframe.columnconfigure(0, weight=1)
ttk.Label(buttonframe).grid(row=0, column=0) # spacer
# LANG: 'OK' button on Settings/Preferences window
button = ttk.Button(buttonframe, text=tr.tl('OK'), command=self.apply)
button.grid(row=0, column=1, sticky=tk.E)
button.bind("<Return>", lambda event: self.apply())
self.protocol("WM_DELETE_WINDOW", self._destroy)
# FIXME: Why are these being called when *creating* the Settings window?
# Selectively disable buttons depending on output settings
self.cmdrchanged()
self.themevarchanged()
# disable hotkey for the duration
hotkeymgr.unregister()
# wait for window to appear on screen before calling grab_set
self.parent.update_idletasks()
self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear on top of parent on Linux
self.wait_visibility()
self.grab_set()
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id()))
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
SIZE(position.right - position.left, position.bottom - position.top), # type: ignore
0x10000, None, position
):
self.geometry(f"+{position.left}+{position.top}")
# Set Log Directory
self.logfile_loc = Path(config.app_dir_path / 'logs')
# Set minimum size to prevent content cut-off
self.update_idletasks() # Update "requested size" from geometry manager
min_width = self.winfo_reqwidth()
min_height = self.winfo_reqheight()
self.wm_minsize(min_width, min_height)
def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None:
output_frame = nb.Frame(root_notebook)
output_frame.columnconfigure(0, weight=1)
if prefsVersion.shouldSetDefaults('0.0.0.0', not bool(config.get_int('output'))):
output = config.OUT_SHIP # default settings
else:
output = config.get_int('output')
row = AutoInc(start=0)
# LANG: Settings > Output - choosing what data to save to files
self.out_label = nb.Label(output_frame, text=tr.tl('Please choose what data to save'))
self.out_label.grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
self.out_csv = tk.IntVar(value=1 if (output & config.OUT_MKT_CSV) else 0)
self.out_csv_button = nb.Checkbutton(
output_frame,
text=tr.tl('Market data in CSV format file'), # LANG: Settings > Output option
variable=self.out_csv,
command=self.outvarchanged
)
self.out_csv_button.grid(columnspan=2, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
self.out_td = tk.IntVar(value=1 if (output & config.OUT_MKT_TD) else 0)
self.out_td_button = nb.Checkbutton(
output_frame,
text=tr.tl('Market data in Trade Dangerous format file'), # LANG: Settings > Output option
variable=self.out_td,
command=self.outvarchanged
)
self.out_td_button.grid(columnspan=2, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
self.out_ship = tk.IntVar(value=1 if (output & config.OUT_SHIP) else 0)
# Output setting
self.out_ship_button = nb.Checkbutton(
output_frame,
text=tr.tl('Ship loadout'), # LANG: Settings > Output option
variable=self.out_ship,
command=self.outvarchanged
)
self.out_ship_button.grid(columnspan=2, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
self.out_auto = tk.IntVar(value=0 if output & config.OUT_MKT_MANUAL else 1) # inverted
# Output setting
self.out_auto_button = nb.Checkbutton(
output_frame,
text=tr.tl('Automatically update on docking'), # LANG: Settings > Output option
variable=self.out_auto,
command=self.outvarchanged
)
self.out_auto_button.grid(columnspan=2, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
self.outdir = tk.StringVar()
self.outdir.set(str(config.get_str('outdir')))
# LANG: Settings > Output - Label for "where files are located"
self.outdir_label = nb.Label(output_frame, text=tr.tl('File location')+':') # Section heading in settings
# Type ignored due to incorrect type annotation. a 2 tuple does padding for each side
self.outdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) # type: ignore
self.outdir_entry = ttk.Entry(output_frame, takefocus=False)
self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button
self.outbutton = ttk.Button(
output_frame,
text=text,
# Technically this is different from the label in Settings > Output, as *this* is used
# as the title of the popup folder selection window.
# LANG: Settings > Output - Label for "where files are located"
command=lambda: self.filebrowse(tr.tl('File location'), self.outdir)
)
self.outbutton.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=row.get())
# LANG: Label for 'Output' Settings/Preferences tab
root_notebook.add(output_frame, text=tr.tl('Output')) # Tab heading in settings
def __setup_plugin_tabs(self, notebook: ttk.Notebook) -> None:
for plugin in plug.PLUGINS:
plugin_frame = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta)
if plugin_frame:
notebook.add(plugin_frame, text=plugin.name)
def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001
config_frame = nb.Frame(notebook)
config_frame.columnconfigure(1, weight=1)
row = AutoInc(start=0)
self.logdir = tk.StringVar()
default = config.default_journal_dir if config.default_journal_dir_path is not None else ''
logdir = config.get_str('journaldir')
if logdir is None or logdir == '':
logdir = default
self.logdir.set(logdir)
self.logdir_entry = ttk.Entry(config_frame, takefocus=False)
# Location of the Journal files
nb.Label(
config_frame,
# LANG: Settings > Configuration - Label for Journal files location
text=tr.tl('E:D journal file location')+':'
).grid(columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
text = tr.tl('Browse...') # LANG: NOT-macOS Setting - files location selection button
with row as cur_row:
self.logbutton = ttk.Button(
config_frame,
text=text,
# LANG: Settings > Configuration - Label for Journal files location
command=lambda: self.filebrowse(tr.tl('E:D journal file location'), self.logdir)
)
self.logbutton.grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
if config.default_journal_dir_path:
# Appearance theme and language setting
ttk.Button(
config_frame,
# LANG: Settings > Configuration - Label on 'reset journal files location to default' button
text=tr.tl('Default'),
command=self.logdir_reset,
state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED
).grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
# CAPI settings
self.capi_fleetcarrier = tk.BooleanVar(value=config.get_bool('capi_fleetcarrier'))
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
nb.Label(
config_frame,
text=tr.tl('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
nb.Checkbutton(
config_frame,
# LANG: Configuration - Enable or disable the Fleet Carrier CAPI calls
text=tr.tl('Enable Fleet Carrier CAPI Queries'),
variable=self.capi_fleetcarrier
).grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
if sys.platform == 'win32':
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
self.hotkey_code = config.get_int('hotkey_code')
self.hotkey_mods = config.get_int('hotkey_mods')
self.hotkey_only = tk.IntVar(value=not config.get_int('hotkey_always'))
self.hotkey_play = tk.IntVar(value=not config.get_int('hotkey_mute'))
with row as cur_row:
nb.Label(
config_frame,
text=tr.tl('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
self.hotkey_text = ttk.Entry(config_frame, width=30, justify=tk.CENTER)
self.hotkey_text.insert(
0,
# No hotkey/shortcut currently defined
# TODO: display Only shows up on windows
# LANG: No hotkey/shortcut set
hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None')
)
self.hotkey_text.bind('<FocusIn>', self.hotkeystart)
self.hotkey_text.bind('<FocusOut>', self.hotkeyend)
self.hotkey_text.grid(column=1, columnspan=2, pady=self.BOXY, sticky=tk.W, row=cur_row)
# Hotkey/Shortcut setting
self.hotkey_only_btn = nb.Checkbutton(
config_frame,
# LANG: Configuration - Act on hotkey only when ED is in foreground
text=tr.tl('Only when Elite: Dangerous is the active app'),
variable=self.hotkey_only,
state=tk.NORMAL if self.hotkey_code else tk.DISABLED
)
self.hotkey_only_btn.grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
# Hotkey/Shortcut setting
self.hotkey_play_btn = nb.Checkbutton(
config_frame,
# LANG: Configuration - play sound when hotkey used
text=tr.tl('Play sound'),
variable=self.hotkey_play,
state=tk.NORMAL if self.hotkey_code else tk.DISABLED
)
self.hotkey_play_btn.grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
# Options to select the Update Path and Disable Automatic Checks For Updates whilst in-game
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
with row as curr_row:
nb.Label(config_frame, text=tr.tl('Update Track')).grid( # LANG: Select the Update Track (Beta, Stable)
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=curr_row
)
self.curr_update_track = "Beta" if config.get_bool('beta_optin') else "Stable"
self.update_paths = tk.StringVar(value=self.curr_update_track)
update_paths = [
tr.tl("Stable"), # LANG: Stable Version of EDMC
tr.tl("Beta") # LANG: Beta Version of EDMC
]
self.update_track = nb.OptionMenu(
config_frame, self.update_paths, self.update_paths.get(), *update_paths
)
self.update_track.configure(width=15)
self.update_track.grid(column=1, pady=self.BOXY, padx=self.PADX, sticky=tk.W, row=curr_row)
self.disable_autoappupdatecheckingame = tk.IntVar(value=config.get_int('disable_autoappupdatecheckingame'))
self.disable_autoappupdatecheckingame_btn = nb.Checkbutton(
config_frame,
# LANG: Configuration - disable checks for app updates when in-game
text=tr.tl('Disable Automatic Application Updates Check when in-game'),
variable=self.disable_autoappupdatecheckingame,
command=self.disable_autoappupdatecheckingame_changed
)
self.disable_autoappupdatecheckingame_btn.grid(
columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()
)
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
# Settings prompt for preferred ship loadout, system and station info websites
# LANG: Label for preferred shipyard, system and station 'providers'
nb.Label(config_frame, text=tr.tl('Preferred websites')).grid(
columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
)
with row as cur_row:
shipyard_provider = config.get_str('shipyard_provider')
self.shipyard_provider = tk.StringVar(
value=str(shipyard_provider if shipyard_provider in plug.provides('shipyard_url') else 'EDSY')
)
# Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis
# LANG: Label for Shipyard provider selection
nb.Label(config_frame, text=tr.tl('Shipyard')).grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row
)
self.shipyard_button = nb.OptionMenu(
config_frame, self.shipyard_provider, self.shipyard_provider.get(), *plug.provides('shipyard_url')
)
self.shipyard_button.configure(width=15)
self.shipyard_button.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row)
# Option for alternate URL opening
self.alt_shipyard_open = tk.IntVar(value=config.get_int('use_alt_shipyard_open'))
self.alt_shipyard_open_btn = nb.Checkbutton(
config_frame,
# LANG: Label for checkbox to utilise alternative Coriolis URL method
text=tr.tl('Use alternate URL method'),
variable=self.alt_shipyard_open,
command=self.alt_shipyard_open_changed,
)
self.alt_shipyard_open_btn.grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
with row as cur_row:
system_provider = config.get_str('system_provider')
self.system_provider = tk.StringVar(
value=str(system_provider if system_provider in plug.provides('system_url') else 'EDSM')
)
# LANG: Configuration - Label for selection of 'System' provider website
nb.Label(config_frame, text=tr.tl('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
self.system_button = nb.OptionMenu(
config_frame,
self.system_provider,
self.system_provider.get(),
*plug.provides('system_url')
)
self.system_button.configure(width=15)
self.system_button.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row)
with row as cur_row:
station_provider = config.get_str('station_provider')
self.station_provider = tk.StringVar(
value=str(station_provider if station_provider in plug.provides('station_url') else 'EDSM')
)
# LANG: Configuration - Label for selection of 'Station' provider website
nb.Label(config_frame, text=tr.tl('Station')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
self.station_button = nb.OptionMenu(
config_frame,
self.station_provider,
self.station_provider.get(),
*plug.provides('station_url')
)
self.station_button.configure(width=15)
self.station_button.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row)
# Set loglevel
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
with row as cur_row:
# Set the current loglevel
nb.Label(
config_frame,
# LANG: Configuration - Label for selection of Log Level
text=tr.tl('Log Level')
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
current_loglevel = config.get_str('loglevel')
if not current_loglevel:
current_loglevel = logging.getLevelName(logging.INFO)
self.select_loglevel = tk.StringVar(value=str(current_loglevel))
loglevels = list(
map(logging.getLevelName, (
logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG
))
)
self.loglevel_dropdown = nb.OptionMenu(
config_frame,
self.select_loglevel,
self.select_loglevel.get(),
*loglevels
)
self.loglevel_dropdown.configure(width=15)
self.loglevel_dropdown.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row)
ttk.Button(
config_frame,
# LANG: Label on button used to open a filesystem folder
text=tr.tl('Open Log Folder'), # Button that opens a folder in Explorer/Finder
command=lambda: open_folder(self.logfile_loc)
).grid(column=2, padx=self.PADX, pady=0, sticky=tk.NSEW, row=cur_row)
# Big spacer
nb.Label(config_frame).grid(sticky=tk.W, row=row.get())
# LANG: Label for 'Configuration' tab in Settings
notebook.add(config_frame, text=tr.tl('Configuration'))
def __setup_privacy_tab(self, notebook: ttk.Notebook) -> None:
privacy_frame = nb.Frame(notebook)
self.hide_multicrew_captain = tk.BooleanVar(value=config.get_bool('hide_multicrew_captain', default=False))
self.hide_private_group = tk.BooleanVar(value=config.get_bool('hide_private_group', default=False))
row = AutoInc(start=0)
# LANG: UI elements privacy section header in privacy tab of preferences
nb.Label(privacy_frame, text=tr.tl('Main UI privacy options')).grid(
row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=self.PADY
)
nb.Checkbutton(
# LANG: Hide private group owner name from UI checkbox
privacy_frame, text=tr.tl('Hide private group name in UI'),
variable=self.hide_private_group
).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W)
nb.Checkbutton(
# LANG: Hide multicrew captain name from main UI checkbox
privacy_frame, text=tr.tl('Hide multi-crew captain name'),
variable=self.hide_multicrew_captain
).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W)
notebook.add(privacy_frame, text=tr.tl('Privacy')) # LANG: Preferences privacy tab title
def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None:
self.languages = tr.available_names()
# Appearance theme and language setting
# LANG: The system default language choice in Settings > Appearance
self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), tr.tl('Default')))
self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop')))
self.minimize_system_tray = tk.BooleanVar(value=config.get_bool('minimize_system_tray'))
self.theme = tk.IntVar(value=config.get_int('theme'))
self.theme_colors = [config.get_str('dark_text'), config.get_str('dark_highlight')]
self.theme_prompts = [
# LANG: Label for Settings > Appeareance > selection of 'normal' text colour
tr.tl('Normal text'), # Dark theme color setting
# LANG: Label for Settings > Appeareance > selection of 'highlightes' text colour
tr.tl('Highlighted text'), # Dark theme color setting
]
row = AutoInc(start=0)
appearance_frame = nb.Frame(notebook)
appearance_frame.columnconfigure(2, weight=1)
with row as cur_row:
# LANG: Appearance - Label for selection of application display language
nb.Label(appearance_frame, text=tr.tl('Language')).grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row
)
self.lang_button = nb.OptionMenu(appearance_frame, self.lang, self.lang.get(), *self.languages.values())
self.lang_button.grid(column=1, columnspan=2, padx=0, pady=self.BOXY, sticky=tk.W, row=cur_row)
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
# Appearance setting
# LANG: Label for Settings > Appearance > Theme selection
nb.Label(appearance_frame, text=tr.tl('Theme')).grid(
columnspan=3, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
)
# Appearance theme and language setting
nb.Radiobutton(
# LANG: Label for 'Default' theme radio button
appearance_frame, text=tr.tl('Default'), variable=self.theme,
value=theme.THEME_DEFAULT, command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
# Appearance theme setting
nb.Radiobutton(
# LANG: Label for 'Dark' theme radio button
appearance_frame, text=tr.tl('Dark'), variable=self.theme,
value=theme.THEME_DARK, command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
if sys.platform == 'win32':
nb.Radiobutton(
appearance_frame,
# LANG: Label for 'Transparent' theme radio button
text=tr.tl('Transparent'), # Appearance theme setting
variable=self.theme,
value=theme.THEME_TRANSPARENT,
command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
with row as cur_row:
self.theme_label_0 = nb.Label(appearance_frame, text=self.theme_prompts[0])
self.theme_label_0.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
# Main window
self.theme_button_0 = tk.Button(
appearance_frame,
# LANG: Appearance - Example 'Normal' text
text=tr.tl('Station'),
background='grey4',
command=lambda: self.themecolorbrowse(0)
)
self.theme_button_0.grid(column=1, padx=0, pady=self.BOXY, sticky=tk.NSEW, row=cur_row)
with row as cur_row:
self.theme_label_1 = nb.Label(appearance_frame, text=self.theme_prompts[1])
self.theme_label_1.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
self.theme_button_1 = tk.Button(
appearance_frame,
text=' Hutton Orbital ', # Do not translate
background='grey4',
command=lambda: self.themecolorbrowse(1)
)
self.theme_button_1.grid(column=1, padx=0, pady=self.BOXY, sticky=tk.NSEW, row=cur_row)
# UI Scaling
"""
The provided UI Scale setting is a percentage value relative to the
tk-scaling setting on startup.
So, if at startup we find tk-scaling is 1.33 and have a user setting
of 200 we'll end up setting 2.66 as the tk-scaling value.
"""
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
with row as cur_row:
# LANG: Appearance - Label for selection of UI scaling
nb.Label(appearance_frame, text=tr.tl('UI Scale Percentage')).grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row
)
self.ui_scale = tk.IntVar()
self.ui_scale.set(config.get_int('ui_scale'))
self.uiscale_bar = tk.Scale(
appearance_frame,
variable=self.ui_scale, # type: ignore # TODO: intvar, but annotated as DoubleVar
orient=tk.HORIZONTAL,
length=300 * (float(theme.startup_ui_scale) / 100.0 * theme.default_ui_scale), # type: ignore # runtime
from_=0,
to=400,
tickinterval=50,
resolution=10,
)
self.uiscale_bar.grid(column=1, padx=0, pady=self.BOXY, sticky=tk.W, row=cur_row)
self.ui_scaling_defaultis = nb.Label(
appearance_frame,
# LANG: Appearance - Help/hint text for UI scaling selection
text=tr.tl('100 means Default{CR}Restart Required for{CR}changes to take effect!')
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row)
# Transparency slider
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
with row as cur_row:
# LANG: Appearance - Label for selection of main window transparency
nb.Label(appearance_frame, text=tr.tl("Main window transparency")).grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row
)
self.transparency = tk.IntVar()
self.transparency.set(config.get_int('ui_transparency') or 100) # Default to 100 for users
self.transparency_bar = tk.Scale(
appearance_frame,
variable=self.transparency, # type: ignore # Its accepted as an intvar
orient=tk.HORIZONTAL,
length=300 * (float(theme.startup_ui_scale) / 100.0 * theme.default_ui_scale), # type: ignore # runtime
from_=100,
to=5,
tickinterval=10,
resolution=5,
command=lambda _: self.parent.wm_attributes("-alpha", self.transparency.get() / 100)
)
nb.Label(
appearance_frame,
# LANG: Appearance - Help/hint text for Main window transparency selection
text=tr.tl(
"100 means fully opaque.{CR}"
"Window is updated in real time"
).format(CR='\n')
).grid(
column=3,
padx=self.PADX,
pady=self.PADY,
sticky=tk.E,
row=cur_row
)
self.transparency_bar.grid(column=1, padx=0, pady=self.BOXY, sticky=tk.W, row=cur_row)
# Always on top
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
self.ontop_button = nb.Checkbutton(
appearance_frame,
# LANG: Appearance - Label for checkbox to select if application always on top
text=tr.tl('Always on top'),
variable=self.always_ontop,
command=self.themevarchanged
)
self.ontop_button.grid(
columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()
) # Appearance setting
if sys.platform == 'win32':
nb.Checkbutton(
appearance_frame,
# LANG: Appearance option for Windows "minimize to system tray"
text=tr.tl('Minimize to system tray'),
variable=self.minimize_system_tray,
command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance setting
nb.Label(appearance_frame).grid(sticky=tk.W) # big spacer
# LANG: Label for Settings > Appearance tab
notebook.add(appearance_frame, text=tr.tl('Appearance')) # Tab heading in settings
def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001
# Plugin settings and info
plugins_frame = nb.Frame(notebook)
plugins_frame.columnconfigure(0, weight=1)
row = AutoInc(start=0)
self.plugdir = tk.StringVar()
self.plugdir.set(str(config.get_str('plugin_dir')))
# LANG: Label for location of third-party plugins folder
self.plugdir_label = nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':')
self.plugdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
self.plugdir_entry = ttk.Entry(plugins_frame, takefocus=False,
textvariable=self.plugdir) # Link StringVar to Entry widget
self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
with row as cur_row:
nb.Label(
plugins_frame,
# Help text in settings
# LANG: Tip/label about how to disable plugins
text=tr.tl(
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')
).grid(columnspan=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
# Open Plugin Folder Button
self.open_plug_folder_btn = ttk.Button(
plugins_frame,
# LANG: Label on button used to open the Plugin Folder
text=tr.tl('Open Plugins Folder'),
command=lambda: open_folder(config.plugin_dir_path)
)
self.open_plug_folder_btn.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
# Browse Button
text = tr.tl('Browse...') # LANG: NOT-macOS Settings - files location selection button
self.plugbutton = ttk.Button(
plugins_frame,
text=text,
# LANG: Selecting the Location of the Plugin Directory on the Filesystem
command=lambda: self.filebrowse(tr.tl('Plugin Directory Location'), self.plugdir)
)
self.plugbutton.grid(column=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
if config.default_journal_dir_path:
# Appearance theme and language setting
ttk.Button(
plugins_frame,
# LANG: Settings > Configuration - Label on 'reset journal files location to default' button
text=tr.tl('Default'),
command=self.plugdir_reset,
state=tk.NORMAL if config.get_str('plugin_dir') else tk.DISABLED
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS))
if enabled_plugins:
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
nb.Label(
plugins_frame,
# LANG: Label on list of enabled plugins
text=tr.tl('Enabled Plugins')+':' # List of plugins in settings
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
for plugin in enabled_plugins:
if plugin.name == plugin.folder:
label = nb.Label(plugins_frame, text=plugin.name)
else:
label = nb.Label(plugins_frame, text=f'{plugin.folder} ({plugin.name})')
label.grid(columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get())
############################################################
# Show which plugins don't have Python 3.x support
############################################################
if plug.PLUGINS_not_py3:
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
# LANG: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x
nb.Label(plugins_frame, text=tr.tl('Plugins Without Python 3.x Support')+':').grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
)
HyperlinkLabel(
# LANG: Plugins - Label on URL to documentation about migrating plugins from Python 2.7
plugins_frame, text=tr.tl('Information on migrating plugins'),
background=nb.Label().cget('background'),
url='https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#migration-from-python-27',
underline=True
).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
for plugin in plug.PLUGINS_not_py3:
if plugin.folder: # 'system' ones have this set to None to suppress listing in Plugins prefs tab
nb.Label(plugins_frame, text=plugin.name).grid(
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
)
############################################################
# Show disabled plugins
############################################################
disabled_plugins = list(filter(lambda x: x.folder and not x.module, plug.PLUGINS))
if disabled_plugins:
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
nb.Label(
plugins_frame,
# LANG: Label on list of user-disabled plugins
text=tr.tl('Disabled Plugins')+':' # List of plugins in settings
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
for plugin in disabled_plugins:
nb.Label(plugins_frame, text=plugin.name).grid(
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
)
############################################################
# Show plugins that failed to load
############################################################
if plug.PLUGINS_broken:
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
# LANG: Plugins - Label for list of 'broken' plugins that failed to load
nb.Label(plugins_frame, text=tr.tl('Broken Plugins')+':').grid(
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
)
for plugin in plug.PLUGINS_broken:
if plugin.folder: # 'system' ones have this set to None to suppress listing in Plugins prefs tab
nb.Label(plugins_frame, text=plugin.name).grid(
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
)
# LANG: Label on Settings > Plugins tab
notebook.add(plugins_frame, text=tr.tl('Plugins')) # Tab heading in settings
def cmdrchanged(self, event=None):
"""
Notify plugins of cmdr change.
:param event: Unused, defaults to None
"""
if self.cmdr != monitor.cmdr or self.is_beta != monitor.is_beta:
# Cmdr has changed - update settings
if self.cmdr is not False: # Don't notify on first run
plug.notify_prefs_cmdr_changed(monitor.cmdr, monitor.is_beta)
self.cmdr = monitor.cmdr
self.is_beta = monitor.is_beta
# Poll
self.cmdrchanged_alarm = self.after(1000, self.cmdrchanged)
def tabchanged(self, event: tk.Event) -> None:
"""Handle preferences active tab changing."""
self.outvarchanged()
def outvarchanged(self, event: Optional[tk.Event] = None) -> None:
"""Handle Output tab variable changes."""
self.displaypath(self.outdir, self.outdir_entry)
self.displaypath(self.logdir, self.logdir_entry)
self.out_label['state'] = tk.NORMAL
self.out_csv_button['state'] = tk.NORMAL
self.out_td_button['state'] = tk.NORMAL
self.out_ship_button['state'] = tk.NORMAL
def filebrowse(self, title, pathvar):
"""
Open a directory selection dialog.
:param title: Title of the window
:param pathvar: the path to start the dialog on
"""
import tkinter.filedialog
directory = tkinter.filedialog.askdirectory(
parent=self,
initialdir=Path(pathvar.get()).expanduser(),
title=title,
mustexist=tk.TRUE
)
if directory:
pathvar.set(directory)
self.outvarchanged()
def displaypath(self, pathvar: tk.StringVar, entryfield: tk.Entry) -> None:
"""
Display a path in a locked tk.Entry.
:param pathvar: the path to display
:param entryfield: the entry in which to display the path
"""
# TODO: This is awful.
entryfield['state'] = tk.NORMAL # must be writable to update
entryfield.delete(0, tk.END)
if sys.platform == 'win32':
start = len(config.home.split('\\')) if pathvar.get().lower().startswith(config.home.lower()) else 0
display = []
components = normpath(pathvar.get()).split('\\')
buf = ctypes.create_unicode_buffer(MAX_PATH)
pidsRes = ctypes.c_int() # noqa: N806 # Windows convention
for i in range(start, len(components)):
try:
if (not SHGetLocalizedName('\\'.join(components[:i+1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and
win32api.LoadString(ctypes.WinDLL(expandvars(buf.value))._handle,
pidsRes.value, buf, MAX_PATH)):
display.append(buf.value)
else:
display.append(components[i])
except Exception:
display.append(components[i])
entryfield.insert(0, '\\'.join(display))
# None if path doesn't exist
else:
if pathvar.get().startswith(config.home):
entryfield.insert(0, '~' + pathvar.get()[len(config.home):])
else:
entryfield.insert(0, pathvar.get())
entryfield['state'] = 'readonly'
def logdir_reset(self) -> None:
"""Reset the log dir to the default."""
if config.default_journal_dir_path:
self.logdir.set(config.default_journal_dir)
self.outvarchanged()
def plugdir_reset(self) -> None:
"""Reset the log dir to the default."""
if config.default_plugin_dir_path:
self.plugdir.set(config.default_plugin_dir)
self.outvarchanged()
def disable_autoappupdatecheckingame_changed(self) -> None:
"""Save out the auto update check in game config."""
config.set('disable_autoappupdatecheckingame', self.disable_autoappupdatecheckingame.get())
# If it's now False, re-enable WinSparkle ? Need access to the AppWindow.updater variable to call down
def alt_shipyard_open_changed(self) -> None:
"""Save out the status of the alt shipyard config."""
config.set('use_alt_shipyard_open', self.alt_shipyard_open.get())
def themecolorbrowse(self, index: int) -> None:
"""
Show a color browser.
:param index: Index of the color type, 0 for dark text, 1 for dark highlight
"""
(_, color) = tkColorChooser.askcolor(
self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent
)
if color:
self.theme_colors[index] = color
self.themevarchanged()
def themevarchanged(self) -> None:
"""Update theme examples."""
self.theme_button_0['foreground'], self.theme_button_1['foreground'] = self.theme_colors
if self.theme.get() == theme.THEME_DEFAULT:
state = tk.DISABLED # type: ignore
else:
state = tk.NORMAL # type: ignore
self.theme_label_0['state'] = state
self.theme_label_1['state'] = state
self.theme_button_0['state'] = state
self.theme_button_1['state'] = state
def hotkeystart(self, event: 'tk.Event[Any]') -> None:
"""Start listening for hotkeys."""
event.widget.bind('<KeyPress>', self.hotkeylisten)
event.widget.bind('<KeyRelease>', self.hotkeylisten)
event.widget.delete(0, tk.END)
hotkeymgr.acquire_start()
def hotkeyend(self, event: 'tk.Event[Any]') -> None:
"""Stop listening for hotkeys."""
event.widget.unbind('<KeyPress>')
event.widget.unbind('<KeyRelease>')
hotkeymgr.acquire_stop() # in case focus was lost while in the middle of acquiring
event.widget.delete(0, tk.END)
self.hotkey_text.insert(
0,
# LANG: No hotkey/shortcut set
hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else tr.tl('None'))
def hotkeylisten(self, event: 'tk.Event[Any]') -> str:
"""
Hotkey handler.
:param event: tkinter event for the hotkey
:return: "break" as a literal, to halt processing
"""
good = hotkeymgr.fromevent(event)
if good and isinstance(good, tuple):
hotkey_code, hotkey_mods = good
event.widget.delete(0, tk.END)
event.widget.insert(0, hotkeymgr.display(hotkey_code, hotkey_mods))
if hotkey_code:
# done
(self.hotkey_code, self.hotkey_mods) = (hotkey_code, hotkey_mods)
self.hotkey_only_btn['state'] = tk.NORMAL
self.hotkey_play_btn['state'] = tk.NORMAL
self.hotkey_only_btn.focus() # move to next widget - calls hotkeyend() implicitly
else:
if good is None: # clear
(self.hotkey_code, self.hotkey_mods) = (0, 0)
event.widget.delete(0, tk.END)
if self.hotkey_code:
event.widget.insert(0, hotkeymgr.display(self.hotkey_code, self.hotkey_mods))
self.hotkey_only_btn['state'] = tk.NORMAL
self.hotkey_play_btn['state'] = tk.NORMAL
else:
# LANG: No hotkey/shortcut set
event.widget.insert(0, tr.tl('None'))
self.hotkey_only_btn['state'] = tk.DISABLED
self.hotkey_play_btn['state'] = tk.DISABLED
self.hotkey_only_btn.focus() # move to next widget - calls hotkeyend() implicitly
return 'break' # stops further processing - insertion, Tab traversal etc
def apply(self) -> None: # noqa: CCR001
"""Update the config with the options set on the dialog."""
config.set('PrefsVersion', prefsVersion.stringToSerial(appversion_nobuild()))
config.set(
'output',
(self.out_td.get() and config.OUT_MKT_TD) +
(self.out_csv.get() and config.OUT_MKT_CSV) +
(config.OUT_MKT_MANUAL if not self.out_auto.get() else 0) +
(self.out_ship.get() and config.OUT_SHIP) +
(config.get_int('output') & (
config.OUT_EDDN_SEND_STATION_DATA | config.OUT_EDDN_SEND_NON_STATION | config.OUT_EDDN_DELAY
))
)
config.set(
'outdir',
join(config.home_path, self.outdir.get()[2:]) if self.outdir.get().startswith('~') else self.outdir.get()
)
logdir = self.logdir.get()
if config.default_journal_dir_path and logdir.lower() == config.default_journal_dir.lower():
config.set('journaldir', '') # default location
else:
config.set('journaldir', logdir)
config.set('capi_fleetcarrier', self.capi_fleetcarrier.get())
if sys.platform == 'win32':
config.set('hotkey_code', self.hotkey_code)
config.set('hotkey_mods', self.hotkey_mods)
config.set('hotkey_always', int(not self.hotkey_only.get()))
config.set('hotkey_mute', int(not self.hotkey_play.get()))
config.set('beta_optin', 0 if self.update_paths.get() == "Stable" else 1)
config.set('shipyard_provider', self.shipyard_provider.get())
config.set('system_provider', self.system_provider.get())
config.set('station_provider', self.station_provider.get())
config.set('loglevel', self.select_loglevel.get())
edmclogger.set_console_loglevel(self.select_loglevel.get())
lang_codes = {v: k for k, v in self.languages.items()} # Codes by name
config.set('language', lang_codes.get(self.lang.get()) or '') # or '' used here due to Default being None above
tr.install(config.get_str('language', default=None)) # type: ignore # This sets self in weird ways.
# Privacy options
config.set('hide_private_group', self.hide_private_group.get())
config.set('hide_multicrew_captain', self.hide_multicrew_captain.get())
config.set('ui_scale', self.ui_scale.get())
config.set('ui_transparency', self.transparency.get())
config.set('always_ontop', self.always_ontop.get())
config.set('minimize_system_tray', self.minimize_system_tray.get())
config.set('theme', self.theme.get())
config.set('dark_text', self.theme_colors[0])
config.set('dark_highlight', self.theme_colors[1])
theme.apply(self.parent)
if self.plugdir.get() != config.get('plugin_dir'):
config.set(
'plugin_dir',
join(config.home_path, self.plugdir.get()[2:]) if self.plugdir.get().startswith(
'~') else self.plugdir.get()
)
self.req_restart = True
# Notify
if self.callback:
self.callback()
plug.notify_prefs_changed(monitor.cmdr, monitor.is_beta)
self._destroy()
# Send to the Post Config if we updated the update branch or need to restart
post_flags = {
'Update': True if self.curr_update_track != self.update_paths.get() else False,
'Track': self.update_paths.get(),
'Parent': self,
'Restart_Req': True if self.req_restart else False
}
if self.callback:
self.callback(**post_flags)
def _destroy(self) -> None:
"""widget.destroy wrapper that does some extra cleanup."""
if self.cmdrchanged_alarm is not None:
self.after_cancel(self.cmdrchanged_alarm)
self.cmdrchanged_alarm = None
self.parent.wm_attributes('-topmost', 1 if config.get_int('always_ontop') else 0)
self.destroy()