1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-17 01:22:19 +03:00

[2051] Main GUI File

This commit is contained in:
David Sangrey 2023-11-17 17:46:32 -05:00
parent 3e1725cecc
commit 724dd2ce6a
No known key found for this signature in database
GPG Key ID: 3AEADBB0186884BC

@ -1,6 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Entry point for the main GUI application."""
"""
EDMarketConnector.py - Entry point for the GUI.
Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
from __future__ import annotations
import argparse
@ -9,14 +13,14 @@ import locale
import pathlib
import queue
import re
import subprocess
import sys
import threading
import webbrowser
from builtins import object, str
from os import chdir, environ
from os.path import dirname, join
from os import chdir, environ, path
from time import localtime, strftime, time
from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Literal
from constants import applongname, appname, protocolhandler_redirect
# Have this as early as possible for people running EDMarketConnector.exe
# from cmd.exe or a bat file or similar. Else they might not be in the correct
@ -24,17 +28,16 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple, Union
if getattr(sys, 'frozen', False):
# Under py2exe sys.path[0] is the executable name
if sys.platform == 'win32':
chdir(dirname(sys.path[0]))
chdir(path.dirname(sys.path[0]))
# Allow executable to be invoked from any cwd
environ['TCL_LIBRARY'] = join(dirname(sys.path[0]), 'lib', 'tcl')
environ['TK_LIBRARY'] = join(dirname(sys.path[0]), 'lib', 'tk')
environ['TCL_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tcl')
environ['TK_LIBRARY'] = path.join(path.dirname(sys.path[0]), 'lib', 'tk')
else:
# We still want to *try* to have CWD be where the main script is, even if
# not frozen.
chdir(pathlib.Path(__file__).parent)
from constants import applongname, appname, protocolhandler_redirect
# config will now cause an appname logger to be set up, so we need the
# console redirect before this
@ -46,7 +49,9 @@ if __name__ == '__main__':
import tempfile
# unbuffered not allowed for text in python3, so use `1 for line buffering
sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), f'{appname}.log'), mode='wt', buffering=1)
log_file_path = path.join(tempfile.gettempdir(), f'{appname}.log')
with open(log_file_path, mode='wt', buffering=1) as log_file:
sys.stdout = sys.stderr = log_file
# TODO: Test: Make *sure* this redirect is working, else py2exe is going to cause an exit popup
@ -87,6 +92,11 @@ if __name__ == '__main__': # noqa: C901
help='Suppress the popup from when the application detects another instance already running',
action='store_true'
)
parser.add_argument('--start_min',
help="Start the application minimized",
action="store_true"
)
###########################################################################
###########################################################################
@ -175,7 +185,7 @@ if __name__ == '__main__': # noqa: C901
)
###########################################################################
args = parser.parse_args()
args: argparse.Namespace = parser.parse_args()
if args.capi_pretend_down:
import config as conf_module
@ -187,7 +197,7 @@ if __name__ == '__main__': # noqa: C901
with open(conf_module.config.app_dir_path / 'access_token.txt', 'r') as at:
conf_module.capi_debug_access_token = at.readline().strip()
level_to_set: Optional[int] = None
level_to_set: int | None = None
if args.trace or args.trace_on:
level_to_set = logging.TRACE # type: ignore # it exists
logger.info('Setting TRACE level debugging due to either --trace or a --trace-on')
@ -216,7 +226,7 @@ if __name__ == '__main__': # noqa: C901
else:
print("--force-edmc-protocol is only valid on Windows")
parser.print_help()
exit(1)
sys.exit(1)
if args.debug_sender and len(args.debug_sender) > 0:
import config as conf_module
@ -237,7 +247,7 @@ if __name__ == '__main__': # noqa: C901
logger.info(f'marked {d} for TRACE')
def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001
"""Handle any edmc:// auth callback, else foreground existing window."""
"""Handle any edmc:// auth callback, else foreground an existing window."""
logger.trace_if('frontier-auth.windows', 'Begin...')
if sys.platform == 'win32':
@ -245,40 +255,40 @@ if __name__ == '__main__': # noqa: C901
# If *this* instance hasn't locked, then another already has and we
# now need to do the edmc:// checks for auth callback
if locked != JournalLockResult.LOCKED:
import ctypes
from ctypes import windll, c_int, create_unicode_buffer, WINFUNCTYPE
from ctypes.wintypes import BOOL, HWND, INT, LPARAM, LPCWSTR, LPWSTR
EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806
GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowText = ctypes.windll.user32.GetWindowTextW # noqa: N806
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW # noqa: N806
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd # noqa: N806
EnumWindows = windll.user32.EnumWindows # noqa: N806
GetClassName = windll.user32.GetClassNameW # noqa: N806
GetClassName.argtypes = [HWND, LPWSTR, c_int]
GetWindowText = windll.user32.GetWindowTextW # noqa: N806
GetWindowText.argtypes = [HWND, LPWSTR, c_int]
GetWindowTextLength = windll.user32.GetWindowTextLengthW # noqa: N806
GetProcessHandleFromHwnd = windll.oleacc.GetProcessHandleFromHwnd # noqa: N806
SW_RESTORE = 9 # noqa: N806
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow # noqa: N806
ShowWindow = ctypes.windll.user32.ShowWindow # noqa: N806
ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync # noqa: N806
SetForegroundWindow = windll.user32.SetForegroundWindow # noqa: N806
ShowWindow = windll.user32.ShowWindow # noqa: N806
ShowWindowAsync = windll.user32.ShowWindowAsync # noqa: N806
COINIT_MULTITHREADED = 0 # noqa: N806,F841
COINIT_APARTMENTTHREADED = 0x2 # noqa: N806
COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806
CoInitializeEx = ctypes.windll.ole32.CoInitializeEx # noqa: N806
CoInitializeEx = windll.ole32.CoInitializeEx # noqa: N806
ShellExecute = ctypes.windll.shell32.ShellExecuteW # noqa: N806
ShellExecute = windll.shell32.ShellExecuteW # noqa: N806
ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT]
def window_title(h: int) -> Optional[str]:
def window_title(h: int) -> str | None:
if h:
text_length = GetWindowTextLength(h) + 1
buf = ctypes.create_unicode_buffer(text_length)
buf = create_unicode_buffer(text_length)
if GetWindowText(h, buf, text_length):
return buf.value
return None
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
@WINFUNCTYPE(BOOL, HWND, LPARAM)
def enumwindowsproc(window_handle, l_param): # noqa: CCR001
"""
Determine if any window for the Application exists.
@ -286,15 +296,16 @@ if __name__ == '__main__': # noqa: C901
Called for each found window by EnumWindows().
When a match is found we check if we're being invoked as the
edmc://auth handler. If so we send the message to the existing
process/window. If not we'll raise that existing window to the
edmc://auth handler. If so we send the message to the existing
process/window. If not we'll raise that existing window to the
foreground.
:param window_handle: Window to check.
:param l_param: The second parameter to the EnumWindows() call.
:return: False if we found a match, else True to continue iteration
"""
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
cls = ctypes.create_unicode_buffer(257)
cls = create_unicode_buffer(257)
# This conditional is exploded to make debugging slightly easier
if GetClassName(window_handle, cls, 257):
if cls.value == 'TkTopLevel':
@ -323,13 +334,8 @@ if __name__ == '__main__': # noqa: C901
# Ref: <https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumwindows>
EnumWindows(enumwindowsproc, 0)
return
def already_running_popup():
"""Create the "already running" popup."""
import tkinter as tk
from tkinter import ttk
# Check for CL arg that suppresses this popup.
if args.suppress_dupe_process_popup:
sys.exit(0)
@ -339,8 +345,7 @@ if __name__ == '__main__': # noqa: C901
frame = tk.Frame(root)
frame.grid(row=1, column=0, sticky=tk.NSEW)
label = tk.Label(frame)
label['text'] = 'An EDMarketConnector.exe process was already running, exiting.'
label = tk.Label(frame, text='An EDMarketConnector.exe process was already running, exiting.')
label.grid(row=1, column=0, sticky=tk.NSEW)
button = ttk.Button(frame, text='OK', command=lambda: sys.exit(0))
@ -373,41 +378,23 @@ if __name__ == '__main__': # noqa: C901
git_branch = ""
try:
import subprocess
git_cmd = subprocess.Popen('git branch --show-current'.split(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
out, err = git_cmd.communicate()
git_branch = out.decode().rstrip('\n')
git_branch = out.decode().strip()
except Exception:
pass
if (
(
git_branch == 'develop'
or (
git_branch == '' and '-alpha0' in str(appversion())
)
) and (
(
sys.platform == 'linux'
and environ.get('USER') is not None
and environ['USER'] not in ['ad', 'athan']
)
or (
sys.platform == 'win32'
and environ.get('USERNAME') is not None
and environ['USERNAME'] not in ['Athan']
)
git_branch == 'develop'
or (
git_branch == '' and '-alpha0' in str(appversion())
)
):
print("Why are you running the develop branch if you're not a developer?")
print("Please check https://github.com/EDCD/EDMarketConnector/wiki/Running-from-source#running-from-source")
print("You probably want the 'stable' branch.")
print("\n\rIf Athanasius or A_D asked you to run this, tell them about this message.")
sys.exit(-1)
print("You're running in a DEVELOPMENT branch build. You might encounter bugs!")
# See EDMCLogging.py docs.
@ -427,8 +414,7 @@ import tkinter as tk
import tkinter.filedialog
import tkinter.font
import tkinter.messagebox
from tkinter import ttk, constants as tkc
from tkinter import ttk
import commodity
import plug
import prefs
@ -462,7 +448,7 @@ SHIPYARD_HTML_TEMPLATE = """
"""
class AppWindow(object):
class AppWindow:
"""Define the main application window."""
_CAPI_RESPONSE_TK_EVENT_NAME = '<<CAPIResponse>>'
@ -509,7 +495,7 @@ class AppWindow(object):
else:
self.w.tk.call('wm', 'iconphoto', self.w, '-default',
tk.PhotoImage(file=join(config.respath_path, 'io.edcd.EDMarketConnector.png')))
tk.PhotoImage(file=path.join(config.respath_path, 'io.edcd.EDMarketConnector.png')))
# TODO: Export to files and merge from them in future ?
self.theme_icon = tk.PhotoImage(
@ -637,7 +623,7 @@ class AppWindow(object):
self.updater = update.Updater(tkroot=self.w, provider='external')
else:
self.updater = update.Updater(tkroot=self.w, provider='internal')
self.updater = update.Updater(tkroot=self.w)
self.updater.check_for_updates() # Sparkle / WinSparkle does this automatically for packaged apps
if sys.platform == 'darwin':
@ -680,14 +666,14 @@ class AppWindow(object):
self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app
self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis
else:
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) # type: ignore
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
self.file_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status))
self.file_menu.add_command(command=self.save_raw)
self.file_menu.add_command(command=lambda: prefs.PreferencesDialog(self.w, self.postprefs))
self.file_menu.add_separator()
self.file_menu.add_command(command=self.onexit)
self.menubar.add_cascade(menu=self.file_menu)
self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) # type: ignore
self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy)
self.menubar.add_cascade(menu=self.edit_menu)
self.help_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) # type: ignore
@ -734,7 +720,7 @@ class AppWindow(object):
anchor=tk.W, compound=tk.LEFT
)
theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW)
self.drag_offset: Tuple[Optional[int], Optional[int]] = (None, None)
self.drag_offset: tuple[int | None, int | None] = (None, None)
theme_titlebar.bind('<Button-1>', self.drag_start)
theme_titlebar.bind('<B1-Motion>', self.drag_continue)
theme_titlebar.bind('<ButtonRelease-1>', self.drag_end)
@ -808,7 +794,7 @@ class AppWindow(object):
self.w.bind('<KP_Enter>', self.capi_request_data)
self.w.bind_all('<<Invoke>>', self.capi_request_data) # Ask for CAPI queries to be performed
self.w.bind_all(self._CAPI_RESPONSE_TK_EVENT_NAME, self.capi_handle_response)
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring
self.w.bind_all('<<JournalEvent>>', self.journal_event) # type: ignore # Journal monitoring
self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
self.w.bind_all('<<CompanionAuthEvent>>', self.auth) # cAPI auth
@ -826,6 +812,12 @@ class AppWindow(object):
config.delete('logdir', suppress=True)
self.postprefs(False) # Companion login happens in callback from monitor
self.toggle_suit_row(visible=False)
if args.start_min:
logger.warning("Trying to start minimized")
if root.overrideredirect():
self.oniconify()
else:
self.w.wm_iconify()
def update_suit_text(self) -> None:
"""Update the suit text for current type and loadout."""
@ -834,13 +826,14 @@ class AppWindow(object):
self.suit['text'] = ''
return
if (suit := monitor.state.get('SuitCurrent')) is None:
suit = monitor.state.get('SuitCurrent')
if suit is None:
self.suit['text'] = f'<{_("Unknown")}>' # LANG: Unknown suit
return
suitname = suit['edmcName']
if (suitloadout := monitor.state.get('SuitLoadoutCurrent')) is None:
suitloadout = monitor.state.get('SuitLoadoutCurrent')
if suitloadout is None:
self.suit['text'] = ''
return
@ -849,31 +842,18 @@ class AppWindow(object):
def suit_show_if_set(self) -> None:
"""Show UI Suit row if we have data, else hide."""
if self.suit['text'] != '':
self.toggle_suit_row(visible=True)
self.toggle_suit_row(self.suit['text'] != '')
else:
self.toggle_suit_row(visible=False)
def toggle_suit_row(self, visible: Optional[bool] = None) -> None:
def toggle_suit_row(self, visible: bool | None = None) -> None:
"""
Toggle the visibility of the 'Suit' row.
:param visible: Force visibility to this.
"""
if visible is True:
self.suit_shown = False
elif visible is False:
self.suit_shown = True
self.suit_shown = not visible
if not self.suit_shown:
if sys.platform != 'win32':
pady = 2
else:
pady = 0
pady = 2 if sys.platform != 'win32' else 0
self.suit_label.grid(row=self.suit_grid_row, column=0, sticky=tk.W, padx=self.PADX, pady=pady)
self.suit.grid(row=self.suit_grid_row, column=1, sticky=tk.EW, padx=self.PADX, pady=pady)
@ -1020,38 +1000,44 @@ class AppWindow(object):
:return: True if all OK, else False to trigger play_bad in caller.
"""
if config.get_int('output') & (config.OUT_STATION_ANY):
if not data['commander'].get('docked') and not monitor.state['OnFoot']:
if not self.status['text']:
# Signal as error because the user might actually be docked
# but the server hosting the Companion API hasn't caught up
# LANG: Player is not docked at a station, when we expect them to be
self.status['text'] = _("You're not docked at a station!")
return False
output_flags = config.get_int('output')
is_docked = data['commander'].get('docked')
has_commodities = data['lastStarport'].get('commodities')
has_modules = data['lastStarport'].get('modules')
commodities_flag = config.OUT_MKT_CSV | config.OUT_MKT_TD
# Ignore possibly missing shipyard info
elif (config.get_int('output') & config.OUT_EDDN_SEND_STATION_DATA) \
and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
if not self.status['text']:
# LANG: Status - Either no market or no modules data for station from Frontier CAPI
self.status['text'] = _("Station doesn't have anything!")
if output_flags & config.OUT_STATION_ANY:
if not is_docked and not monitor.state['OnFoot']:
# Signal as error because the user might actually be docked
# but the server hosting the Companion API hasn't caught up
self._handle_status(_("You're not docked at a station!"))
return False
elif not data['lastStarport'].get('commodities'):
if not self.status['text']:
# LANG: Status - No station market data from Frontier CAPI
self.status['text'] = _("Station doesn't have a market!")
if output_flags & config.OUT_EDDN_SEND_STATION_DATA and not (has_commodities or has_modules):
self._handle_status(_("Station doesn't have anything!"))
elif config.get_int('output') & (config.OUT_MKT_CSV | config.OUT_MKT_TD):
# Fixup anomalies in the commodity data
elif not has_commodities:
self._handle_status(_("Station doesn't have a market!"))
elif output_flags & commodities_flag:
fixed = companion.fixup(data)
if config.get_int('output') & config.OUT_MKT_CSV:
if output_flags & config.OUT_MKT_CSV:
commodity.export(fixed, COMMODITY_CSV)
if config.get_int('output') & config.OUT_MKT_TD:
if output_flags & config.OUT_MKT_TD:
td.export(fixed)
return True
def _handle_status(self, message: str) -> None:
"""
Set the status label text if it's not already set.
:param message: Status message to display.
"""
if not self.status['text']:
self.status['text'] = message
def capi_request_data(self, event=None) -> None: # noqa: CCR001
"""
Perform CAPI data retrieval and associated actions.
@ -1117,30 +1103,28 @@ class AppWindow(object):
self.login()
return
if not companion.session.retrying:
if time() < self.capi_query_holdoff_time: # Was invoked by key while in cooldown
if play_sound and (self.capi_query_holdoff_time - time()) < companion.capi_query_cooldown * 0.75:
self.status['text'] = ''
hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
if not companion.session.retrying and time() >= self.capi_query_holdoff_time:
if play_sound:
if time() < self.capi_query_holdoff_time: # Was invoked by key while in cooldown
if (self.capi_query_holdoff_time - time()) < companion.capi_query_cooldown * 0.75:
self.status['text'] = ''
hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
else:
hotkeymgr.play_good()
return
# LANG: Status - Attempting to retrieve data from Frontier CAPI
self.status['text'] = _('Fetching data...')
self.button['state'] = self.theme_button['state'] = tk.DISABLED
self.w.update_idletasks()
elif play_sound:
hotkeymgr.play_good()
# LANG: Status - Attempting to retrieve data from Frontier CAPI
self.status['text'] = _('Fetching data...')
self.button['state'] = self.theme_button['state'] = tk.DISABLED
self.w.update_idletasks()
query_time = int(time())
logger.trace_if('capi.worker', 'Requesting full station data')
config.set('querytime', query_time)
logger.trace_if('capi.worker', 'Calling companion.session.station')
companion.session.station(
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME,
play_sound=play_sound
)
query_time = int(time())
logger.trace_if('capi.worker', 'Requesting full station data')
config.set('querytime', query_time)
logger.trace_if('capi.worker', 'Calling companion.session.station')
companion.session.station(
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME,
play_sound=play_sound
)
def capi_request_fleetcarrier_data(self, event=None) -> None:
"""
@ -1174,30 +1158,31 @@ class AppWindow(object):
self.status['text'] = _('CAPI query aborted: GameVersion unknown')
return
if not companion.session.retrying:
if time() < self.capi_fleetcarrier_query_holdoff_time: # Was invoked while in cooldown
logger.debug('CAPI fleetcarrier query aborted, too soon since last request')
return
if not companion.session.retrying and time() >= self.capi_fleetcarrier_query_holdoff_time:
# LANG: Status - Attempting to retrieve data from Frontier CAPI
self.status['text'] = _('Fetching data...')
self.w.update_idletasks()
query_time = int(time())
logger.trace_if('capi.worker', 'Requesting fleetcarrier data')
config.set('fleetcarrierquerytime', query_time)
logger.trace_if('capi.worker', 'Calling companion.session.fleetcarrier')
companion.session.fleetcarrier(
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME
)
query_time = int(time())
logger.trace_if('capi.worker', 'Requesting fleetcarrier data')
config.set('fleetcarrierquerytime', query_time)
logger.trace_if('capi.worker', 'Calling companion.session.fleetcarrier')
companion.session.fleetcarrier(
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME
)
def capi_handle_response(self, event=None): # noqa: C901, CCR001
"""Handle the resulting data from a CAPI query."""
"""
Handle the resulting data from a CAPI query.
:param event: generated event details.
"""
logger.trace_if('capi.worker', 'Handling response')
play_bad: bool = False
err: Optional[str] = None
err: str | None = None
capi_response: Union[companion.EDMCCAPIFailedRequest, companion.EDMCCAPIResponse]
capi_response: companion.EDMCCAPIFailedRequest | companion.EDMCCAPIResponse
try:
logger.trace_if('capi.worker', 'Pulling answer off queue')
capi_response = companion.session.capi_response_queue.get(block=False)
@ -1206,8 +1191,7 @@ class AppWindow(object):
if capi_response.exception:
raise capi_response.exception
else:
raise ValueError(capi_response.message)
raise ValueError(capi_response.message)
logger.trace_if('capi.worker', 'Answer is not a Failure')
if not isinstance(capi_response, companion.EDMCCAPIResponse):
@ -1256,8 +1240,8 @@ class AppWindow(object):
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
elif (
not capi_response.capi_data.get('ship', {}).get('name')
or not capi_response.capi_data.get('ship', {}).get('modules')
not capi_response.capi_data.get('ship', {}).get('name')
or not capi_response.capi_data.get('ship', {}).get('modules')
):
# LANG: We don't know what ship the commander is in, when we should
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
@ -1268,8 +1252,8 @@ class AppWindow(object):
raise companion.CmdrError()
elif (
capi_response.auto_update and not monitor.state['OnFoot']
and not capi_response.capi_data['commander'].get('docked')
capi_response.auto_update and not monitor.state['OnFoot']
and not capi_response.capi_data['commander'].get('docked')
):
# auto update is only when just docked
logger.warning(f"{capi_response.auto_update!r} and not {monitor.state['OnFoot']!r} and "
@ -1282,14 +1266,14 @@ class AppWindow(object):
f"{monitor.state['SystemName']!r}")
raise companion.ServerLagging()
elif capi_response.capi_data['lastStarport']['name'] != monitor.state['StationName']:
if capi_response.capi_data['lastStarport']['name'] != monitor.state['StationName']:
if monitor.state['OnFoot'] and monitor.state['StationName']:
logger.warning(f"({capi_response.capi_data['lastStarport']['name']!r} != "
f"{monitor.state['StationName']!r}) AND "
f"{monitor.state['OnFoot']!r} and {monitor.state['StationName']!r}")
raise companion.ServerLagging()
elif capi_response.capi_data['commander']['docked'] and monitor.state['StationName'] is None:
if capi_response.capi_data['commander']['docked'] and monitor.state['StationName'] is None:
# Likely (re-)Embarked on ship docked at an EDO settlement.
# Both Disembark and Embark have `"Onstation": false` in Journal.
# So there's nothing to tell us which settlement we're (still,
@ -1311,8 +1295,8 @@ class AppWindow(object):
raise companion.ServerLagging()
elif (
not monitor.state['OnFoot']
and capi_response.capi_data['ship']['name'].lower() != monitor.state['ShipType']
not monitor.state['OnFoot']
and capi_response.capi_data['ship']['name'].lower() != monitor.state['ShipType']
):
# CAPI ship type must match
logger.warning(f"not {monitor.state['OnFoot']!r} and "
@ -1385,9 +1369,12 @@ class AppWindow(object):
# TODO: Set status text
return
except companion.ServerConnectionError:
except companion.ServerConnectionError as comp_err:
# LANG: Frontier CAPI server error when fetching data
self.status['text'] = _('Frontier CAPI server error')
logger.warning(f'Exception while contacting server: {comp_err}')
err = self.status['text'] = str(comp_err)
play_bad = True
except companion.CredentialsRequireRefresh:
# We need to 'close' the auth else it'll see STATE_OK and think login() isn't needed
@ -1425,11 +1412,6 @@ class AppWindow(object):
companion.session.invalidate()
self.login()
except companion.ServerConnectionError as e: # TODO: unreachable (subclass of ServerLagging -- move to above)
logger.warning(f'Exception while contacting server: {e}')
err = self.status['text'] = str(e)
play_bad = True
except Exception as e: # Including CredentialsError, ServerError
logger.debug('"other" exception', exc_info=e)
err = self.status['text'] = str(e)
@ -1448,7 +1430,7 @@ class AppWindow(object):
self.cooldown()
logger.trace_if('capi.worker', '...done')
def journal_event(self, event): # noqa: C901, CCR001 # Currently not easily broken up.
def journal_event(self, event: str): # noqa: C901, CCR001 # Currently not easily broken up.
"""
Handle a Journal event passed through event queue from monitor.py.
@ -1576,7 +1558,7 @@ class AppWindow(object):
logger.info('StartUp or LoadGame event')
# Disable WinSparkle automatic update checks, IFF configured to do so when in-game
if config.get_int('disable_autoappupdatecheckingame') and 1:
if config.get_int('disable_autoappupdatecheckingame'):
if self.updater is not None:
self.updater.set_automatic_updates_check(False)
@ -1721,21 +1703,21 @@ class AppWindow(object):
# Avoid file length limits if possible
provider = config.get_str('shipyard_provider', default='EDSY')
target = plug.invoke(provider, 'EDSY', 'shipyard_url', loadout, monitor.is_beta)
file_name = join(config.app_dir_path, "last_shipyard.html")
file_name = path.join(config.app_dir_path, "last_shipyard.html")
with open(file_name, 'w') as f:
print(SHIPYARD_HTML_TEMPLATE.format(
f.write(SHIPYARD_HTML_TEMPLATE.format(
link=html.escape(str(target)),
provider_name=html.escape(str(provider)),
ship_name=html.escape(str(shipname))
), file=f)
))
return f'file://localhost/{file_name}'
def system_url(self, system: str) -> str | None:
"""Despatch a system URL to the configured handler."""
return plug.invoke(
config.get_str('system_provider'), 'EDSM', 'system_url', monitor.state['SystemName']
config.get_str('system_provider', default='EDSM'), 'EDSM', 'system_url', monitor.state['SystemName']
)
def station_url(self, station: str) -> str | None:
@ -1749,12 +1731,9 @@ class AppWindow(object):
"""Display and update the cooldown timer for 'Update' button."""
if time() < self.capi_query_holdoff_time:
# Update button in main window
self.button['text'] = self.theme_button['text'] \
= _('cooldown {SS}s').format( # LANG: Cooldown on 'Update' button
SS=int(self.capi_query_holdoff_time - time())
)
cooldown_time = int(self.capi_query_holdoff_time - time())
self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS=cooldown_time)
self.w.after(1000, self.cooldown)
else:
self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window
self.button['state'] = self.theme_button['state'] = (
@ -1775,11 +1754,10 @@ class AppWindow(object):
def copy(self, event=None) -> None:
"""Copy system, and possible station, name to clipboard."""
if monitor.state['SystemName']:
clipboard_text = f"{monitor.state['SystemName']},{monitor.state['StationName']}" if monitor.state[
'StationName'] else monitor.state['SystemName']
self.w.clipboard_clear()
self.w.clipboard_append(
f"{monitor.state['SystemName']},{monitor.state['StationName']}" if monitor.state['StationName']
else monitor.state['SystemName']
)
self.w.clipboard_append(clipboard_text)
def help_general(self, event=None) -> None:
"""Open Wiki Help page in browser."""
@ -1805,9 +1783,14 @@ class AppWindow(object):
class HelpAbout(tk.Toplevel):
"""The applications Help > About popup."""
showing = False
showing: bool = False
def __init__(self, parent: tk.Tk):
def __init__(self, parent: tk.Tk) -> None:
"""
Initialize the HelpAbout popup.
:param parent: The parent Tk window.
"""
if self.__class__.showing:
return
@ -1848,11 +1831,11 @@ class AppWindow(object):
# version <link to changelog>
tk.Label(frame).grid(row=row, column=0) # spacer
row += 1
self.appversion_label = tk.Text(frame, height=1, width=len(str(appversion())), wrap=tkc.NONE, bd=0)
self.appversion_label = tk.Text(frame, height=1, width=len(str(appversion())), wrap=tk.NONE, bd=0)
self.appversion_label.insert("1.0", str(appversion()))
self.appversion_label.tag_configure("center", justify="center")
self.appversion_label.tag_add("center", "1.0", "end")
self.appversion_label.config(state=tkc.DISABLED, bg=frame.cget("background"), font="TkDefaultFont")
self.appversion_label.config(state=tk.DISABLED, bg=frame.cget("background"), font="TkDefaultFont")
self.appversion_label.grid(row=row, column=0, sticky=tk.E)
# LANG: Help > Release Notes
self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'),
@ -2068,7 +2051,7 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
)
def setup_killswitches(filename: Optional[str]):
def setup_killswitches(filename: str | None):
"""Download and setup the main killswitch list."""
logger.debug('fetching killswitches...')
if filename is not None:
@ -2130,7 +2113,7 @@ sys.path: {sys.path}'''
config.delete('font_size', suppress=True)
config.set('ui_scale', 100) # 100% is the default here
config.delete('geometry', suppress=True) # unset is recreated by other code
config.delete('geometry', suppress=True) # unset is recreated by other code
logger.info('reset theme, transparency, font, font size, ui scale, and ui geometry to default.')
@ -2167,12 +2150,12 @@ sys.path: {sys.path}'''
# <https://en.wikipedia.org/wiki/Windows_10_version_history#Version_1903_(May_2019_Update)>
# Windows 19, 1903 was build 18362
if (
sys.platform != 'win32'
or (
windows_ver.major == 10
and windows_ver.build >= 18362
)
or windows_ver.major > 10 # Paranoid future check
sys.platform != 'win32'
or (
windows_ver.major == 10
and windows_ver.build >= 18362
)
or windows_ver.major > 10 # Paranoid future check
):
# Set that same language, but utf8 encoding (it was probably cp1252
# or equivalent for other languages).
@ -2209,10 +2192,10 @@ sys.path: {sys.path}'''
# logger.debug('Test from __main__')
# test_logging()
class A(object):
class A:
"""Simple top-level class."""
class B(object):
class B:
"""Simple second-level class."""
def __init__(self):
@ -2268,7 +2251,7 @@ sys.path: {sys.path}'''
def messagebox_not_py3():
"""Display message about plugins not updated for Python 3.x."""
plugins_not_py3_last = config.get_int('plugins_not_py3_last', default=0)
if (plugins_not_py3_last + 86400) < int(time()) and len(plug.PLUGINS_not_py3):
if (plugins_not_py3_last + 86400) < int(time()) and plug.PLUGINS_not_py3:
# LANG: Popup-text about 'active' plugins without Python 3.x support
popup_text = _(
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the "
@ -2302,9 +2285,11 @@ sys.path: {sys.path}'''
ui_transparency = 100
root.wm_attributes('-alpha', ui_transparency / 100)
# Display message box about plugins without Python 3.x support
root.after(0, messagebox_not_py3)
# Show warning popup for killswitches matching current version
root.after(1, show_killswitch_poppup, root)
# Start the main event loop
root.mainloop()
logger.info('Exiting')