1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-12 15:27:14 +03:00
2023-07-22 09:39:05 -04:00

1293 lines
55 KiB
Python

# -*- coding: utf-8 -*-
"""EDMC preferences library."""
import contextlib
import logging
import sys
import tkinter as tk
import webbrowser
from os.path import expanduser, expandvars, join, normpath
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
from tkinter import ttk
from types import TracebackType
from typing import TYPE_CHECKING, Any, Callable, Optional, Type, Union
import myNotebook as nb # noqa: N813
import plug
from config import applongname, appversion_nobuild, config
from EDMCLogging import edmclogger, get_main_logger
from hotkey import hotkeymgr
from l10n import Translations
from monitor import monitor
from theme import theme
from ttkHyperlinkLabel import HyperlinkLabel
logger = get_main_logger()
if TYPE_CHECKING:
def _(x: str) -> str:
return x
# 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
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 == 'darwin':
import objc # type: ignore
from Foundation import NSFileManager # type: ignore
try:
from ApplicationServices import ( # type: ignore
AXIsProcessTrusted, AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt
)
except ImportError:
HIServices = objc.loadBundle(
'HIServices',
globals(),
'/System/Library/Frameworks/ApplicationServices.framework/Frameworks/HIServices.framework'
)
objc.loadBundleFunctions(
HIServices,
globals(),
[('AXIsProcessTrusted', 'B'), ('AXIsProcessTrustedWithOptions', 'B@')]
)
objc.loadBundleVariables(HIServices, globals(), [('kAXTrustedCheckOptionPrompt', '@^{__CFString=}')])
was_accessible_at_launch = AXIsProcessTrusted() # type: ignore
elif sys.platform == 'win32':
import ctypes
import winreg
from ctypes.wintypes import HINSTANCE, HWND, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT
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
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)
]
GetParent = ctypes.windll.user32.GetParent
GetParent.argtypes = [HWND]
GetWindowRect = ctypes.windll.user32.GetWindowRect
GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)]
SHGetLocalizedName = ctypes.windll.shell32.SHGetLocalizedName
SHGetLocalizedName.argtypes = [LPCWSTR, LPWSTR, UINT, ctypes.POINTER(ctypes.c_int)]
LoadString = ctypes.windll.user32.LoadStringW
LoadString.argtypes = [HINSTANCE, UINT, LPWSTR, ctypes.c_int]
class PreferencesDialog(tk.Toplevel):
"""The EDMC preferences dialog."""
def __init__(self, parent: tk.Tk, callback: Optional[Callable]):
tk.Toplevel.__init__(self, parent)
self.parent = parent
self.callback = callback
if sys.platform == 'darwin':
# LANG: File > Preferences menu entry for macOS
self.title(_('Preferences'))
else:
# LANG: File > Settings (macOS)
self.title(_('Settings'))
if parent.winfo_viewable():
self.transient(parent)
# position over parent
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
# TODO this is fixed supposedly.
self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}')
# remove decoration
if sys.platform == 'win32':
self.attributes('-toolwindow', tk.TRUE)
elif sys.platform == 'darwin':
# http://wiki.tcl.tk/13428
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
self.resizable(tk.FALSE, tk.FALSE)
self.cmdr: Union[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
frame = ttk.Frame(self)
frame.grid(sticky=tk.NSEW)
notebook: ttk.Notebook = nb.Notebook(frame)
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
self.PADX = 10
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
self.PADY = 2 # close spacing
# Set up different tabs
self.__setup_output_tab(notebook)
self.__setup_plugin_tabs(notebook)
self.__setup_config_tab(notebook)
self.__setup_privacy_tab(notebook)
self.__setup_appearance_tab(notebook)
self.__setup_plugin_tab(notebook)
if sys.platform == 'darwin':
self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes
else:
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=_('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 ontop of parent on OSX & Linux
self.wait_visibility()
self.grab_set()
# Ensure fully on-screen
if sys.platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
GetWindowRect(GetParent(self.winfo_id()), position)
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}")
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=1)
# LANG: Settings > Output - choosing what data to save to files
self.out_label = nb.Label(output_frame, text=_('Please choose what data to save'))
self.out_label.grid(columnspan=2, padx=self.PADX, 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=_('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, 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=_('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, 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=_('Ship loadout'), # LANG: Settings > Output option
variable=self.out_ship,
command=self.outvarchanged
)
self.out_ship_button.grid(columnspan=2, padx=self.BUTTONX, pady=(5, 0), 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=_('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=(5, 0), 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=_('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=(5, 0), sticky=tk.W, row=row.get()) # type: ignore
self.outdir_entry = nb.Entry(output_frame, takefocus=False)
self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=(0, self.PADY), sticky=tk.EW, row=row.get())
if sys.platform == 'darwin':
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
else:
text = (_('Browse...')) # LANG: NOT-macOS Settings - files location selection button
self.outbutton = nb.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(_('File location'), self.outdir)
)
self.outbutton.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.NSEW, row=row.get())
nb.Frame(output_frame).grid(row=row.get()) # bottom spacer # TODO: does nothing?
# LANG: Label for 'Output' Settings/Preferences tab
root_notebook.add(output_frame, text=_('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=1)
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 = nb.Entry(config_frame, takefocus=False)
# Location of the Journal files
nb.Label(
config_frame,
# LANG: Settings > Configuration - Label for Journal files location
text=_('E:D journal file location')+':'
).grid(columnspan=4, padx=self.PADX, sticky=tk.W, row=row.get())
self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=(0, self.PADY), sticky=tk.EW, row=row.get())
if sys.platform == 'darwin':
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
else:
text = (_('Browse...')) # LANG: NOT-macOS Setting - files location selection button
self.logbutton = nb.Button(
config_frame,
text=text,
# LANG: Settings > Configuration - Label for Journal files location
command=lambda: self.filebrowse(_('E:D journal file location'), self.logdir)
)
self.logbutton.grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=row.get())
if config.default_journal_dir_path:
# Appearance theme and language setting
nb.Button(
config_frame,
# LANG: Settings > Configuration - Label on 'reset journal files location to default' button
text=_('Default'),
command=self.logdir_reset,
state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED
).grid(column=2, pady=self.PADY, sticky=tk.EW, row=row.get())
# 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.PADY*4, sticky=tk.EW, row=row.get()
)
nb.Label(
config_frame,
text=_('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
nb.Checkbutton(
config_frame,
# LANG: Configuration - Enable or disable the Fleet Carrier CAPI calls
text=_('Enable Fleetcarrier CAPI Queries'),
variable=self.capi_fleetcarrier
).grid(columnspan=4, padx=self.PADX, pady=(5, 0), sticky=tk.W, row=row.get())
if sys.platform in ('darwin', 'win32'):
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, 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'))
nb.Label(
config_frame,
text=_('Keyboard shortcut') if # LANG: Hotkey/Shortcut settings prompt on OSX
sys.platform == 'darwin' else
_('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
if sys.platform == 'darwin' and not was_accessible_at_launch:
if AXIsProcessTrusted():
# Shortcut settings prompt on OSX
nb.Label(
config_frame,
# LANG: macOS Preferences > Configuration - restart the app message
text=_('Re-start {APP} to use shortcuts').format(APP=applongname),
foreground='firebrick'
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
else:
# Shortcut settings prompt on OSX
nb.Label(
config_frame,
# LANG: macOS - Configuration - need to grant the app permission for keyboard shortcuts
text=_('{APP} needs permission to use shortcuts').format(APP=applongname),
foreground='firebrick'
).grid(columnspan=4, padx=self.PADX, sticky=tk.W, row=row.get())
# LANG: Shortcut settings button on OSX
nb.Button(config_frame, text=_('Open System Preferences'), command=self.enableshortcuts).grid(
padx=self.PADX, sticky=tk.E, row=row.get()
)
else:
self.hotkey_text = nb.Entry(config_frame, width=(
20 if sys.platform == 'darwin' else 30), justify=tk.CENTER)
self.hotkey_text.insert(
0,
# No hotkey/shortcut currently defined
# TODO: display Only shows up on darwin or windows
# LANG: No hotkey/shortcut set
hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None')
)
self.hotkey_text.bind('<FocusIn>', self.hotkeystart)
self.hotkey_text.bind('<FocusOut>', self.hotkeyend)
self.hotkey_text.grid(column=1, columnspan=2, pady=(5, 0), sticky=tk.W, row=row.get())
# Hotkey/Shortcut setting
self.hotkey_only_btn = nb.Checkbutton(
config_frame,
# LANG: Configuration - Act on hotkey only when ED is in foreground
text=_('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.PADX, pady=(5, 0), 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=_('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.PADX, sticky=tk.W, row=row.get())
# Option to disabled Automatic Check For Updates whilst in-game
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, sticky=tk.EW, row=row.get()
)
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=_('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.PADX, sticky=tk.W, row=row.get())
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, 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=_('Preferred websites')).grid(
columnspan=4, padx=self.PADX, 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=_('Shipyard')).grid(padx=self.PADX, pady=2*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, 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=_('Use alternate URL method'),
variable=self.alt_shipyard_open,
command=self.alt_shipyard_open_changed,
)
self.alt_shipyard_open_btn.grid(column=2, 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=_('System')).grid(padx=self.PADX, pady=2*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, 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=_('Station')).grid(padx=self.PADX, pady=2*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, sticky=tk.W, row=cur_row)
# Set loglevel
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, 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=_('Log Level')
).grid(padx=self.PADX, pady=2*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, sticky=tk.W, 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=_('Configuration'))
def __setup_privacy_tab(self, notebook: ttk.Notebook) -> None:
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()
# LANG: UI elements privacy section header in privacy tab of preferences
nb.Label(frame, text=_('Main UI privacy options')).grid(
row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=self.PADY
)
nb.Checkbutton(
frame, text=_('Hide private group name in UI'), # LANG: Hide private group owner name from UI checkbox
variable=self.hide_private_group
).grid(row=row.get(), column=0, padx=self.PADX, pady=self.PADY)
nb.Checkbutton(
frame, text=_('Hide multi-crew captain name'), # LANG: Hide multicrew captain name from main UI checkbox
variable=self.hide_multicrew_captain
).grid(row=row.get(), column=0, padx=self.PADX, pady=self.PADY)
notebook.add(frame, text=_('Privacy')) # LANG: Preferences privacy tab title
def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None:
self.languages = Translations.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'), _('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
_('Normal text'), # Dark theme color setting
# LANG: Label for Settings > Appeareance > selection of 'highlightes' text colour
_('Highlighted text'), # Dark theme color setting
]
row = AutoInc(start=1)
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=_('Language')).grid(padx=self.PADX, 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=self.PADX, sticky=tk.W, row=cur_row)
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.PADY*4, sticky=tk.EW, row=row.get()
)
# Appearance setting
# LANG: Label for Settings > Appearance > Theme selection
nb.Label(appearance_frame, text=_('Theme')).grid(columnspan=3, padx=self.PADX, sticky=tk.W, row=row.get())
# Appearance theme and language setting
nb.Radiobutton(
# LANG: Label for 'Default' theme radio button
appearance_frame, text=_('Default'), variable=self.theme,
value=theme.THEME_DEFAULT, command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get())
# Appearance theme setting
nb.Radiobutton(
# LANG: Label for 'Dark' theme radio button
appearance_frame, text=_('Dark'), variable=self.theme, value=theme.THEME_DARK, command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get())
if sys.platform == 'win32':
nb.Radiobutton(
appearance_frame,
# LANG: Label for 'Transparent' theme radio button
text=_('Transparent'), # Appearance theme setting
variable=self.theme,
value=theme.THEME_TRANSPARENT,
command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, 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, sticky=tk.W, row=cur_row)
# Main window
self.theme_button_0 = nb.ColoredButton(
appearance_frame,
# LANG: Appearance - Example 'Normal' text
text=_('Station'),
background='grey4',
command=lambda: self.themecolorbrowse(0)
)
self.theme_button_0.grid(column=1, padx=self.PADX, pady=self.PADY, 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, sticky=tk.W, row=cur_row)
self.theme_button_1 = nb.ColoredButton(
appearance_frame,
text=' Hutton Orbital ', # Do not translate
background='grey4',
command=lambda: self.themecolorbrowse(1)
)
self.theme_button_1.grid(column=1, padx=self.PADX, pady=self.PADY, 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.PADY*4, sticky=tk.EW, row=row.get()
)
with row as cur_row:
# LANG: Appearance - Label for selection of UI scaling
nb.Label(appearance_frame, text=_('UI Scale Percentage')).grid(
padx=self.PADX, pady=2*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, sticky=tk.W, row=cur_row)
self.ui_scaling_defaultis = nb.Label(
appearance_frame,
# LANG: Appearance - Help/hint text for UI scaling selection
text=_('100 means Default{CR}Restart Required for{CR}changes to take effect!')
).grid(column=3, padx=self.PADX, pady=2*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.PADY*4, 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=_("Main window transparency")).grid(
padx=self.PADX, pady=self.PADY*2, 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=_(
"100 means fully opaque.{CR}"
"Window is updated in real time"
).format(CR='\n')
).grid(
column=3,
padx=self.PADX,
pady=self.PADY*2,
sticky=tk.E,
row=cur_row
)
self.transparency_bar.grid(column=1, sticky=tk.W, row=cur_row)
# Always on top
ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.PADY*4, 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=_('Always on top'),
variable=self.always_ontop,
command=self.themevarchanged
)
self.ontop_button.grid(columnspan=3, padx=self.BUTTONX, 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=_('Minimize to system tray'),
variable=self.minimize_system_tray,
command=self.themevarchanged
).grid(columnspan=3, padx=self.BUTTONX, 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=_('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)
plugdir = tk.StringVar()
plugdir.set(config.plugin_dir)
row = AutoInc(1)
plugdirentry = nb.Entry(plugins_frame, justify=tk.LEFT)
self.displaypath(plugdir, plugdirentry)
with row as cur_row:
# Section heading in settings
# LANG: Label for location of third-party plugins folder
nb.Label(plugins_frame, text=_('Plugins folder') + ':').grid(padx=self.PADX, sticky=tk.W, row=cur_row)
plugdirentry.grid(padx=self.PADX, sticky=tk.EW, row=cur_row)
nb.Button(
plugins_frame,
# LANG: Label on button used to open a filesystem folder
text=_('Open'), # Button that opens a folder in Explorer/Finder
command=lambda: webbrowser.open(f'file:///{config.plugin_dir_path}')
).grid(column=1, padx=(0, self.PADX), sticky=tk.NSEW, row=cur_row)
nb.Label(
plugins_frame,
# Help text in settings
# LANG: Tip/label about how to disable plugins
text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')
).grid(columnspan=2, padx=self.PADX, pady=10, sticky=tk.NSEW, row=row.get())
enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS))
if len(enabled_plugins):
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.PADY * 8, sticky=tk.EW
)
nb.Label(
plugins_frame,
# LANG: Label on list of enabled plugins
text=_('Enabled Plugins')+':' # List of plugins in settings
).grid(padx=self.PADX, 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.PADX*2, sticky=tk.W, row=row.get())
############################################################
# Show which plugins don't have Python 3.x support
############################################################
if len(plug.PLUGINS_not_py3):
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.PADY * 8, 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=_('Plugins Without Python 3.x Support:')+':').grid(padx=self.PADX, sticky=tk.W)
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.PADX*2, sticky=tk.W)
HyperlinkLabel(
# LANG: Plugins - Label on URL to documentation about migrating plugins from Python 2.7
plugins_frame, text=_('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, sticky=tk.W)
############################################################
disabled_plugins = list(filter(lambda x: x.folder and not x.module, plug.PLUGINS))
if len(disabled_plugins):
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
columnspan=3, padx=self.PADX, pady=self.PADY * 8, sticky=tk.EW, row=row.get()
)
nb.Label(
plugins_frame,
# LANG: Lable on list of user-disabled plugins
text=_('Disabled Plugins')+':' # List of plugins in settings
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
for plugin in disabled_plugins:
nb.Label(plugins_frame, text=plugin.name).grid(
columnspan=2, padx=self.PADX*2, sticky=tk.W, row=row.get()
)
# LANG: Label on Settings > Plugins tab
notebook.add(plugins_frame, text=_('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()
if sys.platform == 'darwin':
# Hack to recompute size so that buttons show up under Mojave
notebook = event.widget
frame = self.nametowidget(notebook.winfo_parent())
temp = nb.Label(frame)
temp.grid()
temp.update_idletasks()
temp.destroy()
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=expanduser(pathvar.get()),
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
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
elif sys.platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()):
if pathvar.get().startswith(config.home):
display = ['~'] + NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())[
len(NSFileManager.defaultManager().componentsToDisplayForPath_(config.home)):
]
else:
display = NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())
entryfield.insert(0, '/'.join(display))
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 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 _('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, _('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:
"""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 in ('darwin', '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('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
Translations.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)
# Notify
if self.callback:
self.callback()
plug.notify_prefs_changed(monitor.cmdr, monitor.is_beta)
self._destroy()
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()
if sys.platform == 'darwin':
def enableshortcuts(self) -> None:
"""Set up macOS preferences shortcut."""
self.apply()
# popup System Preferences dialog
try:
# http://stackoverflow.com/questions/6652598/cocoa-button-opens-a-system-preference-page/6658201
from ScriptingBridge import SBApplication # type: ignore
sysprefs = 'com.apple.systempreferences'
prefs = SBApplication.applicationWithBundleIdentifier_(sysprefs)
pane = [x for x in prefs.panes() if x.id() == 'com.apple.preference.security'][0]
prefs.setCurrentPane_(pane)
anchor = [x for x in pane.anchors() if x.name() == 'Privacy_Accessibility'][0]
anchor.reveal()
prefs.activate()
except Exception:
AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: True})
if not config.shutting_down:
self.parent.event_generate('<<Quit>>', when="tail")