mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-04 19:40:02 +03:00
1329 lines
57 KiB
Python
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()
|