mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-13 07:47:14 +03:00
This flag controls whether commodity, outfitting or shipyard schema messages are sent. Thus 'MKT' ('market') is misleading. Rename it so the intent when used is clear.
1306 lines
55 KiB
Python
1306 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 myNotebook import Notebook
|
|
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, LPARAM, LPCWSTR, LPVOID, 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:
|
|
pass
|
|
|
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb762115
|
|
BIF_RETURNONLYFSDIRS = 0x00000001
|
|
BIF_USENEWUI = 0x00000050
|
|
BFFM_INITIALIZED = 1
|
|
BFFM_SETSELECTION = 0x00000467
|
|
BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, HWND, ctypes.c_uint, LPARAM, LPARAM)
|
|
|
|
class BROWSEINFO(ctypes.Structure):
|
|
"""Windows file browser fields."""
|
|
|
|
_fields_ = [("hwndOwner", HWND), ("pidlRoot", LPVOID), ("pszDisplayName", LPWSTR), ("lpszTitle", LPCWSTR),
|
|
("ulFlags", UINT), ("lpfn", BrowseCallbackProc), ("lParam", LPCWSTR), ("iImage", ctypes.c_int)]
|
|
|
|
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 = 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)
|
|
|
|
# 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: nb.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: 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: 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())
|
|
|
|
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 'eddb')
|
|
)
|
|
|
|
# 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: 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: 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=0, 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=1, 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=2,
|
|
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: 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 locale
|
|
|
|
# If encoding isn't UTF-8 we can't use the tkinter dialog
|
|
current_locale = locale.getlocale(locale.LC_CTYPE)
|
|
directory = None
|
|
if sys.platform == 'win32' and current_locale[1] not in ('utf8', 'UTF8', 'utf-8', 'UTF-8'):
|
|
def browsecallback(hwnd, uMsg, lParam, lpData): # noqa: N803 # Windows API convention
|
|
# set initial folder
|
|
if uMsg == BFFM_INITIALIZED and lpData:
|
|
ctypes.windll.user32.SendMessageW(hwnd, BFFM_SETSELECTION, 1, lpData)
|
|
return 0
|
|
|
|
browseInfo = BROWSEINFO() # noqa: N806 # Windows convention
|
|
browseInfo.lpszTitle = title
|
|
browseInfo.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI
|
|
browseInfo.lpfn = BrowseCallbackProc(browsecallback)
|
|
browseInfo.lParam = pathvar.get().startswith('~') and join(config.home_path,
|
|
pathvar.get()[2:]) or pathvar.get()
|
|
ctypes.windll.ole32.CoInitialize(None)
|
|
pidl = ctypes.windll.shell32.SHBrowseForFolderW(ctypes.byref(browseInfo))
|
|
if pidl:
|
|
path = ctypes.create_unicode_buffer(MAX_PATH)
|
|
ctypes.windll.shell32.SHGetPathFromIDListW(pidl, path)
|
|
ctypes.windll.ole32.CoTaskMemFree(pidl)
|
|
directory = path.value
|
|
else:
|
|
directory = None
|
|
|
|
else:
|
|
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
|
|
|
|
state = tk.NORMAL if self.theme.get() else tk.DISABLED
|
|
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:
|
|
(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_DO_NOT_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)
|
|
|
|
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")
|