diff --git a/EDMarketConnector.py b/EDMarketConnector.py
index 09e6fc4e..747d50ce 100755
--- a/EDMarketConnector.py
+++ b/EDMarketConnector.py
@@ -11,13 +11,16 @@ from __future__ import annotations
import argparse
import html
import locale
+import os
import pathlib
import queue
import re
+import signal
import subprocess
import sys
import threading
import webbrowser
+import tempfile
from os import chdir, environ, path
from time import localtime, strftime, time
from typing import TYPE_CHECKING, Any, Literal
@@ -47,8 +50,6 @@ if __name__ == '__main__':
# output until after this redirect is done, if needed.
if getattr(sys, 'frozen', False):
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
- import tempfile
-
# unbuffered not allowed for text in python3, so use `1 for line buffering
log_file_path = path.join(tempfile.gettempdir(), f'{appname}.log')
sys.stdout = sys.stderr = open(log_file_path, mode='wt', buffering=1) # Do NOT use WITH here.
@@ -336,29 +337,13 @@ if __name__ == '__main__': # noqa: C901
def already_running_popup():
"""Create the "already running" popup."""
- import tkinter as tk
- from tkinter import ttk
+ from tkinter import messagebox
# Check for CL arg that suppresses this popup.
if args.suppress_dupe_process_popup:
sys.exit(0)
- root = tk.Tk(className=appname.lower())
-
- frame = tk.Frame(root)
- frame.grid(row=1, column=0, sticky=tk.NSEW)
-
- 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))
- button.grid(row=2, column=0, sticky=tk.S)
-
- try:
- root.mainloop()
- except KeyboardInterrupt:
- logger.info("Ctrl+C Detected, Attempting Clean Shutdown")
- sys.exit()
- logger.info('Exiting')
+ messagebox.showerror(title=appname, message="An EDMarketConnector process was already running, exiting.")
+ sys.exit(0)
journal_lock = JournalLock()
locked = journal_lock.obtain_lock()
@@ -432,25 +417,10 @@ from hotkey import hotkeymgr
from l10n import translations as tr
from monitor import monitor
from theme import theme
-from ttkHyperlinkLabel import HyperlinkLabel
+from ttkHyperlinkLabel import HyperlinkLabel, SHIPYARD_HTML_TEMPLATE
SERVER_RETRY = 5 # retry pause for Companion servers [s]
-SHIPYARD_HTML_TEMPLATE = """
-
-
-
-
- Redirecting you to your {ship_name} at {provider_name}...
-
-
-
- You should be redirected to your {ship_name} at {provider_name} shortly...
-
-
-
-"""
-
class AppWindow:
"""Define the main application window."""
@@ -515,13 +485,13 @@ class AppWindow:
self.cmdr_label = tk.Label(frame, name='cmdr_label')
self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr')
self.ship_label = tk.Label(frame, name='ship_label')
- self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship')
+ self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship', popup_copy=True)
self.suit_label = tk.Label(frame, name='suit_label')
self.suit = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='suit')
self.system_label = tk.Label(frame, name='system_label')
self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True, name='system')
self.station_label = tk.Label(frame, name='station_label')
- self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station')
+ self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station', popup_copy=True)
# system and station text is set/updated by the 'provider' plugins
# edsm and inara. Look for:
#
@@ -648,7 +618,8 @@ class AppWindow:
self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates...
# About E:D Market Connector
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
- self.help_menu.add_command(command=prefs.help_open_log_folder) # Open Log Folder
+ logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname
+ self.help_menu.add_command(command=lambda: prefs.open_folder(logfile_loc)) # Open Log Folder
self.menubar.add_cascade(menu=self.help_menu)
if sys.platform == 'win32':
@@ -1627,7 +1598,7 @@ class AppWindow:
hotkeymgr.play_bad()
def shipyard_url(self, shipname: str) -> str | None:
- """Despatch a ship URL to the configured handler."""
+ """Dispatch a ship URL to the configured handler."""
if not (loadout := monitor.ship()):
logger.warning('No ship loadout, aborting.')
return ''
@@ -1654,13 +1625,13 @@ class AppWindow:
return f'file://localhost/{file_name}'
def system_url(self, system: str) -> str | None:
- """Despatch a system URL to the configured handler."""
+ """Dispatch a system URL to the configured handler."""
return plug.invoke(
config.get_str('system_provider', default='EDSM'), 'EDSM', 'system_url', monitor.state['SystemName']
)
def station_url(self, station: str) -> str | None:
- """Despatch a station URL to the configured handler."""
+ """Dispatch a station URL to the configured handler."""
return plug.invoke(
config.get_str('station_provider', default='EDSM'), 'EDSM', 'station_url',
monitor.state['SystemName'], monitor.state['StationName']
@@ -2226,7 +2197,29 @@ sys.path: {sys.path}'''
if theme.default_ui_scale is not None:
root.tk.call('tk', 'scaling', theme.default_ui_scale * float(ui_scale) / 100.0)
- app = AppWindow(root)
+ try:
+ app = AppWindow(root)
+ except Exception as err:
+ logger.exception(f"EDMC Critical Error: {err}")
+ title = tr.tl("Error") # LANG: Generic error prefix
+ message = tr.tl( # LANG: EDMC Critical Error Notification
+ "EDSM encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!"
+ )
+ err = f"{err.__class__.__name__}: {err}" # type: ignore # hijacking the existing exception detection
+ detail = tr.tl( # LANG: EDMC Critical Error Details
+ r"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?"
+ ).format(ERR=err)
+ detail = detail.replace('\\n', '\n')
+ detail = detail.replace('\\r', '\r')
+ msg = tk.messagebox.askyesno(
+ title=title, message=message, detail=detail, icon=tkinter.messagebox.ERROR, type=tkinter.messagebox.YESNO
+ )
+ if msg:
+ webbrowser.open(
+ "https://github.com/EDCD/EDMarketConnector/issues/new?"
+ "assignees=&labels=bug%2C+unconfirmed&projects=&template=bug_report.md&title="
+ )
+ os.kill(os.getpid(), signal.SIGTERM)
def messagebox_broken_plugins():
"""Display message about 'broken' plugins that failed to load."""
diff --git a/L10n/en.template b/L10n/en.template
index 200743c8..38a7fbed 100644
--- a/L10n/en.template
+++ b/L10n/en.template
@@ -36,6 +36,12 @@
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */
"Error: unable to get token" = "Error: unable to get token";
+/* EDMarketConnector.py: EDMC Critical Error Notification; */
+"EDSM encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!" = "EDSM encountered a critical error, and cannot recover. EDMC is shutting down for its own protection!";
+
+/* EDMarketConnector.py: EDMC Critical Error Details; */
+"Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?" = "Here's what EDMC Detected:\r\n\r\n{ERR}\r\n\r\nDo you want to file a Bug Report on GitHub?";
+
/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */
"Frontier CAPI down for maintenance" = "Frontier CAPI down for maintenance";
@@ -782,3 +788,6 @@
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Cannot paste non-text content.";
+
+/* ttkHyperlinkLabel.py: Open Element In Selected Provider; */
+"Open in {URL}" = "Open in {URL}";
\ No newline at end of file
diff --git a/monitor.py b/monitor.py
index f93473ba..9d33f5eb 100644
--- a/monitor.py
+++ b/monitor.py
@@ -38,7 +38,7 @@ if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR
- from watchdog.events import FileCreatedEvent, FileSystemEventHandler
+ from watchdog.events import FileSystemEventHandler, FileSystemEvent
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
@@ -60,7 +60,7 @@ else:
FileSystemEventHandler = object # dummy
if TYPE_CHECKING:
# this isn't ever used, but this will make type checking happy
- from watchdog.events import FileCreatedEvent
+ from watchdog.events import FileSystemEvent
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
@@ -346,7 +346,7 @@ class EDLogs(FileSystemEventHandler):
"""
return bool(self.thread and self.thread.is_alive())
- def on_created(self, event: 'FileCreatedEvent') -> None:
+ def on_created(self, event: 'FileSystemEvent') -> None:
"""Watchdog callback when, e.g. client (re)started."""
if not event.is_directory and self._RE_LOGFILE.search(basename(event.src_path)):
diff --git a/myNotebook.py b/myNotebook.py
index 43acfebd..8a3cf901 100644
--- a/myNotebook.py
+++ b/myNotebook.py
@@ -121,7 +121,7 @@ class EntryMenu(ttk.Entry):
class Entry(EntryMenu):
"""Custom ttk.Entry class to fix some display issues."""
- # DEPRECATED: Migrate to EntryMenu. Will remove in 5.12 or later.
+ # DEPRECATED: Migrate to EntryMenu. Will remove in 6.0 or later.
def __init__(self, master: ttk.Frame | None = None, **kw):
EntryMenu.__init__(self, master, **kw)
@@ -139,7 +139,7 @@ class Button(ttk.Button):
class ColoredButton(tk.Button):
"""Custom tk.Button class to fix some display issues."""
- # DEPRECATED: Migrate to tk.Button. Will remove in 5.12 or later.
+ # DEPRECATED: Migrate to tk.Button. Will remove in 6.0 or later.
def __init__(self, master: ttk.Frame | None = None, **kw):
tk.Button.__init__(self, master, **kw)
diff --git a/plugins/inara.py b/plugins/inara.py
index 7c565ab3..f2c9830e 100644
--- a/plugins/inara.py
+++ b/plugins/inara.py
@@ -550,23 +550,6 @@ def journal_entry( # noqa: C901, CCR001
# Ship change
if event_name == 'Loadout' and this.shipswap:
- cur_ship = {
- 'shipType': state['ShipType'],
- 'shipGameID': state['ShipID'],
- 'shipName': state['ShipName'], # Can be None
- 'shipIdent': state['ShipIdent'], # Can be None
- 'isCurrentShip': True,
- }
-
- if state['HullValue']:
- cur_ship['shipHullValue'] = state['HullValue']
-
- if state['ModulesValue']:
- cur_ship['shipModulesValue'] = state['ModulesValue']
-
- cur_ship['shipRebuyCost'] = state['Rebuy']
- new_add_event('setCommanderShip', entry['timestamp'], cur_ship)
-
this.loadout = make_loadout(state)
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
this.shipswap = False
@@ -854,7 +837,7 @@ def journal_entry( # noqa: C901, CCR001
for ship in this.fleet:
new_add_event('setCommanderShip', entry['timestamp'], ship)
# Loadout
- if event_name == 'Loadout' and not this.newsession:
+ if event_name == 'Loadout':
loadout = make_loadout(state)
if this.loadout != loadout:
this.loadout = loadout
@@ -868,6 +851,26 @@ def journal_entry( # noqa: C901, CCR001
new_add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout)
+ cur_ship = {
+ 'shipType': state['ShipType'],
+ 'shipGameID': state['ShipID'],
+ 'shipName': state['ShipName'], # Can be None
+ 'shipIdent': state['ShipIdent'], # Can be None
+ 'isCurrentShip': True,
+ 'shipMaxJumpRange': entry['MaxJumpRange'],
+ 'shipCargoCapacity': entry['CargoCapacity']
+ }
+ if state['HullValue']:
+ cur_ship['shipHullValue'] = state['HullValue']
+
+ if state['ModulesValue']:
+ cur_ship['shipModulesValue'] = state['ModulesValue']
+
+ if state['Rebuy']:
+ cur_ship['shipRebuyCost'] = state['Rebuy']
+
+ new_add_event('setCommanderShip', entry['timestamp'], cur_ship)
+
# Stored modules
if event_name == 'StoredModules':
items = {mod['StorageSlot']: mod for mod in entry['Items']} # Impose an order
diff --git a/prefs.py b/prefs.py
index 2d4c8d51..1569a0b7 100644
--- a/prefs.py
+++ b/prefs.py
@@ -8,7 +8,6 @@ import pathlib
import sys
import tempfile
import tkinter as tk
-import webbrowser
from os import system
from os.path import expanduser, expandvars, join, normpath
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
@@ -40,14 +39,21 @@ logger = get_main_logger()
def help_open_log_folder() -> None:
"""Open the folder logs are stored in."""
- logfile_loc = pathlib.Path(tempfile.gettempdir())
- logfile_loc /= f'{appname}'
+ logger.warning(
+ DeprecationWarning("This function is deprecated, use open_log_folder instead. "
+ "This function will be removed in 6.0 or later")
+ )
+ open_folder(pathlib.Path(tempfile.gettempdir()) / appname)
+
+
+def open_folder(file: pathlib.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 "" "{logfile_loc}"')
+ system(f'start "" "{file}"')
elif sys.platform.startswith('linux'):
# On Linux, use the "xdg-open" command to open the folder
- system(f'xdg-open "{logfile_loc}"')
+ system(f'xdg-open "{file}"')
class PrefsVersion:
@@ -296,6 +302,9 @@ class PreferencesDialog(tk.Toplevel):
):
self.geometry(f"+{position.left}+{position.top}")
+ # Set Log Directory
+ self.logfile_loc = pathlib.Path(tempfile.gettempdir()) / appname
+
def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None:
output_frame = nb.Frame(root_notebook)
output_frame.columnconfigure(0, weight=1)
@@ -623,7 +632,7 @@ class PreferencesDialog(tk.Toplevel):
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: help_open_log_folder()
+ command=lambda: open_folder(self.logfile_loc)
).grid(column=2, padx=self.PADX, pady=0, sticky=tk.NSEW, row=cur_row)
# Big spacer
@@ -884,7 +893,7 @@ class PreferencesDialog(tk.Toplevel):
plugins_frame,
# LANG: Label on button used to open a filesystem folder
text=tr.tl('Open'), # Button that opens a folder in Explorer/Finder
- command=lambda: webbrowser.open(f'file:///{config.plugin_dir_path}')
+ command=lambda: open_folder(config.plugin_dir_path)
).grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.N, row=cur_row)
enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS))
diff --git a/requirements.txt b/requirements.txt
index 8c450362..a7f8aa7c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
requests==2.31.0
pillow==10.3.0
-watchdog==3.0.0
+watchdog==4.0.0
infi.systray==0.1.12; sys_platform == 'win32'
semantic-version==2.10.0
diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py
index d026f619..3ec0a26d 100644
--- a/ttkHyperlinkLabel.py
+++ b/ttkHyperlinkLabel.py
@@ -19,14 +19,34 @@ In addition to standard ttk.Label arguments, takes the following arguments:
May be imported by plugins
"""
from __future__ import annotations
-
+import html
+from functools import partial
import sys
import tkinter as tk
import webbrowser
from tkinter import font as tk_font
from tkinter import ttk
from typing import Any
+import plug
+from os import path
+from config import config, logger
from l10n import translations as tr
+from monitor import monitor
+
+SHIPYARD_HTML_TEMPLATE = """
+
+
+
+
+ Redirecting you to your {ship_name} at {provider_name}...
+
+
+
+ You should be redirected to your {ship_name} at {provider_name} shortly...
+
+
+
+"""
class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
@@ -64,6 +84,72 @@ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
text=kw.get('text'),
font=kw.get('font', ttk.Style().lookup('TLabel', 'font')))
+ # Add Menu Options
+ self.plug_options = kw.pop('plug_options', None)
+ self.name = kw.get('name', None)
+ if self.name == 'ship':
+ self.menu.add_separator()
+ for url in plug.provides('shipyard_url'):
+ self.menu.add_command(
+ label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider
+ command=partial(self.open_shipyard, url)
+ )
+
+ if self.name == 'station':
+ self.menu.add_separator()
+ for url in plug.provides('station_url'):
+ self.menu.add_command(
+ label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider
+ command=partial(self.open_station, url)
+ )
+
+ if self.name == 'system':
+ self.menu.add_separator()
+ for url in plug.provides('system_url'):
+ self.menu.add_command(
+ label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider
+ command=partial(self.open_system, url)
+ )
+
+ def open_shipyard(self, url: str):
+ """Open the Current Ship Loadout in the Selected Provider."""
+ if not (loadout := monitor.ship()):
+ logger.warning('No ship loadout, aborting.')
+ return ''
+ if not bool(config.get_int("use_alt_shipyard_open")):
+ opener = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta)
+ if opener:
+ return webbrowser.open(opener)
+ else:
+ # 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 = path.join(config.app_dir_path, "last_shipyard.html")
+
+ with open(file_name, 'w') as f:
+ f.write(SHIPYARD_HTML_TEMPLATE.format(
+ link=html.escape(str(target)),
+ provider_name=html.escape(str(provider)),
+ ship_name=html.escape("Ship")
+ ))
+
+ webbrowser.open(f'file://localhost/{file_name}')
+
+ def open_system(self, url: str):
+ """Open the Current System in the Selected Provider."""
+ opener = plug.invoke(url, 'EDSM', 'system_url', monitor.state['SystemName'])
+ if opener:
+ return webbrowser.open(opener)
+
+ def open_station(self, url: str):
+ """Open the Current Station in the Selected Provider."""
+ opener = plug.invoke(
+ url, 'EDSM', 'station_url',
+ monitor.state['SystemName'], monitor.state['StationName']
+ )
+ if opener:
+ return webbrowser.open(opener)
+
def configure( # noqa: CCR001
self, cnf: dict[str, Any] | None = None, **kw: Any
) -> dict[str, tuple[str, str, str, Any, Any]] | None: