diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index d0f21e70..2c7e615b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,18 +1,26 @@
---
name: Bug report
-about: Create a report to help us improve
+about: Report an issue with the program
title: ''
labels: bug, unconfirmed
assignees: ''
---
+**Please check the [Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) in case this has already been reported.**
+
+**Please also check if the issue is covered in our [Troubleshooting Guide](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting).** It might be something with a known work around, or where a third party (such as EDSM) is causing logging that is harmless.
+
**Please complete the following information:**
- Version [e.g. 4.0.6 - See 'Help > About E:D Market Connector']
- OS: [e.g. Windows 10]
- OS Locale: [e.g. English, French, Serbian...]
- If applicable: Browser [e.g. chrome, safari]
- - Please attach the file `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file). Use the Icon that looks like `_`, `|` and `^` all in one character towards the right of the toolbar above.
+ - Please attach a log file:
+ 1. If running 4.1.0 (including betas) or later: `%TEMP%\EDMarketConnector\EDMarketConnector.log`. See [Debug Log File](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files).
+ 1. Else `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file).
+
+ Use the Icon that looks like `_`, `|` and `^` all in one character towards the right of the toolbar above.
**Describe the bug**
A clear and concise description of what the bug is.
diff --git a/ChangeLog.md b/ChangeLog.md
index fe54941b..dbeaeb09 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,6 +1,51 @@
This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first).
---
+Pre-Release 4.1.0-beta6
+===
+
+* Include a manifest in both EDMarketConnector.exe and EDMC.exe to specify
+ they're Unicode applications so that they default to using the UTF-8
+ codepage. Fixes [#711](https://github.com/EDCD/EDMarketConnector/issues/711).
+
+* Add extra debug logging to EDSM plugin for `CarrierJump`, `FSDJump`,
+ `Location` and `Docked` events. Hopefully this will help track down the
+ cause of [#713](https://github.com/EDCD/EDMarketConnector/issues/713).
+
+* Various code cleanups which shouldn't impact user experience.
+
+* EDMC.exe will now log useful startup state information if run with the
+ `--loglevel DEBUG` arguments.
+
+Pre-Release 4.1.0-beta5
+===
+
+* We are now explicitly setting a UTF8 encoding at startup. This *shouldn't*
+ have any side effects and has allowed us to switch to the native tkinter
+ file dialogues rather than some custom code.
+
+ If you do encounter errors that might be related to this then it would be
+ useful to see the logging output that details the Locale settings at
+ various points during startup. Examples might include incorrect text being
+ rendered for your language when you have it set, or issues with filenames
+ and their content, but any of these are unlikely.
+
+* The error `'list' object has no attribute 'values'` should now be fixed.
+
+* Code dealing with Frontier's CAPI was cleaned up, so please report any
+ issues related to that (mostly when just docked or when you press the Update
+ button).
+
+* Extra logging added for when we process FSDJump and CarrierJump events for
+ EDSM. This is aimed at checking if we do have a bug with CarrierJump events,
+ but having FSDJump trigger the logging as well made it easier to test.
+
+* Default `logging` level for plugins is now DEBUG. This won't change what's
+ actually logged, it just ensures that everything gets through to the two
+ channels that then decide what is output.
+
+* Translations updated. Thanks again to all contributors!
+
Pre-Release 4.1.0-beta4
===
diff --git a/EDMC.manifest b/EDMC.manifest
new file mode 100644
index 00000000..9b412178
--- /dev/null
+++ b/EDMC.manifest
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UTF-8
+
+
+
+
+
+
+
+
+
+
diff --git a/EDMC.py b/EDMC.py
index 8ef2e78c..d1f9411d 100755
--- a/EDMC.py
+++ b/EDMC.py
@@ -6,6 +6,7 @@
import argparse
import json
+import locale
import logging
import os
import re
@@ -14,6 +15,9 @@ from os.path import getmtime, join
from time import sleep, time
from typing import Any, Optional
+# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
+os.environ["EDMC_NO_UI"] = "1"
+
import collate
import commodity
import companion
@@ -36,15 +40,22 @@ sys.path.append(config.internal_plugin_dir)
import eddn # noqa: E402
# isort: on
-# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
-os.environ["EDMC_NO_UI"] = "1"
-
-l10n.Translations.install_dummy()
-
logger = EDMCLogging.Logger(appcmdname).get_logger()
logger.setLevel(logging.INFO)
+def log_locale(prefix: str) -> None:
+ logger.debug(f'''Locale: {prefix}
+Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)}
+Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)}
+Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)}
+Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)}
+Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
+ )
+
+
+l10n.Translations.install_dummy()
+
SERVER_RETRY = 5 # retry pause for Companion servers [s]
EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR, EXIT_ARGS = range(7)
@@ -112,7 +123,15 @@ def main():
sys.exit(EXIT_ARGS)
logger.setLevel(args.loglevel)
- logger.debug('Startup')
+ logger.debug(f'Startup v{appversion} : Running on Python v{sys.version}')
+ logger.debug(f'''Platform: {sys.platform}
+argv[0]: {sys.argv[0]}
+exec_prefix: {sys.exec_prefix}
+executable: {sys.executable}
+sys.path: {sys.path}'''
+ )
+
+ log_locale('Initial Locale')
if args.j:
logger.debug('Import and collate from JSON dump')
diff --git a/EDMCLogging.py b/EDMCLogging.py
index 4208dc28..f882f924 100644
--- a/EDMCLogging.py
+++ b/EDMCLogging.py
@@ -10,13 +10,14 @@ strings.
import inspect
import logging
import logging.handlers
+import os
import pathlib
import tempfile
# So that any warning about accessing a protected member is only in one place.
from sys import _getframe as getframe
from typing import Tuple
-from config import appname, config
+from config import appcmdname, appname, config
# TODO: Tests:
#
@@ -41,7 +42,7 @@ from config import appname, config
#
# 14. Call from *package*
-_default_loglevel = logging.INFO
+_default_loglevel = logging.DEBUG
class Logger:
@@ -122,7 +123,7 @@ class Logger:
return self.logger_channel
-def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.Logger:
+def get_plugin_logger(plugin_name: str, loglevel: int = _default_loglevel) -> logging.Logger:
"""
Return a logger suitable for a plugin.
@@ -143,7 +144,12 @@ def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.L
:param loglevel: Optional logLevel for this Logger.
:return: logging.Logger instance, all set up.
"""
- plugin_logger = logging.getLogger(name)
+ if not os.getenv('EDMC_NO_UI'):
+ base_logger_name = appname
+ else:
+ base_logger_name = appcmdname
+
+ plugin_logger = logging.getLogger(f'{base_logger_name}.{plugin_name}')
plugin_logger.setLevel(loglevel)
plugin_logger.addFilter(EDMCContextFilter())
@@ -326,6 +332,18 @@ class EDMCContextFilter(logging.Filter):
return module_name
+
+def get_main_logger() -> logging.Logger:
+ """Return the correct logger for how the program is being run."""
+
+ if not os.getenv("EDMC_NO_UI"):
+ # GUI app being run
+ return logging.getLogger(appname)
+ else:
+ # Must be the CLI
+ return logging.getLogger(appcmdname)
+
+
# Singleton
loglevel = config.get('loglevel')
if not loglevel:
diff --git a/EDMarketConnector.manifest b/EDMarketConnector.manifest
index 2985bbfa..ef78813a 100644
--- a/EDMarketConnector.manifest
+++ b/EDMarketConnector.manifest
@@ -19,6 +19,8 @@
true
+
+ UTF-8
diff --git a/EDMarketConnector.py b/EDMarketConnector.py
index 4d25fe17..ccfac5fe 100755
--- a/EDMarketConnector.py
+++ b/EDMarketConnector.py
@@ -23,6 +23,7 @@ if __name__ == "__main__":
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
sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), f'{appname}.log'), mode='wt', buffering=1)
@@ -45,6 +46,7 @@ if __debug__:
if platform != 'win32':
import pdb
import signal
+
signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame))
import companion
@@ -61,7 +63,6 @@ from protocol import protocolhandler
from dashboard import dashboard
from theme import theme
-
SERVER_RETRY = 5 # retry pause for Companion servers [s]
SHIPYARD_HTML_TEMPLATE = """
@@ -81,7 +82,6 @@ SHIPYARD_HTML_TEMPLATE = """
class AppWindow(object):
-
# Tkinter Event types
EVENT_KEYPRESS = 2
EVENT_BUTTON = 4
@@ -104,10 +104,14 @@ class AppWindow(object):
if platform == 'win32':
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
else:
- self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501
- self.theme_icon = tk.PhotoImage(data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501
- self.theme_minimize = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
- self.theme_close = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
+ self.w.tk.call('wm', 'iconphoto', self.w, '-default',
+ tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501
+ self.theme_icon = tk.PhotoImage(
+ data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501
+ self.theme_minimize = tk.BitmapImage(
+ data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
+ self.theme_close = tk.BitmapImage(
+ data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
frame = tk.Frame(self.w, name=appname.lower())
frame.grid(sticky=tk.NSEW)
@@ -152,7 +156,8 @@ class AppWindow(object):
row = frame.grid_size()[1]
self.button.grid(row=row, columnspan=2, sticky=tk.NSEW)
self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW)
- theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501
+ theme.register_alternate((self.button, self.theme_button, self.theme_button),
+ {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501
self.status.grid(columnspan=2, sticky=tk.EW)
self.button.bind('', self.getandsend)
theme.button_bind(self.theme_button, self.getandsend)
@@ -291,6 +296,7 @@ class AppWindow(object):
# Check that the titlebar will be at least partly on screen
import ctypes
from ctypes.wintypes import POINT
+
# https://msdn.microsoft.com/en-us/library/dd145064
MONITOR_DEFAULTTONULL = 0 # noqa: N806
if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16),
@@ -322,6 +328,7 @@ class AppWindow(object):
# Load updater after UI creation (for WinSparkle)
import update
+
if getattr(sys, 'frozen', False):
# Running in frozen .exe, so use (Win)Sparkle
self.updater = update.Updater(tkroot=self.w, provider='external')
@@ -453,7 +460,7 @@ class AppWindow(object):
if not retrying:
if time() < self.holdofftime: # Was invoked by key while in cooldown
self.status['text'] = ''
- if play_sound and (self.holdofftime-time()) < companion.holdoff*0.75:
+ if play_sound and (self.holdofftime - time()) < companion.holdoff * 0.75:
hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
return
elif play_sound:
@@ -493,7 +500,7 @@ class AppWindow(object):
if isdir('dump'):
with open('dump/{system}{station}.{timestamp}.json'.format(
system=data['lastSystem']['name'],
- station=data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '',
+ station=data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '',
timestamp=strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') as h:
h.write(json.dumps(data,
ensure_ascii=False,
@@ -525,7 +532,7 @@ class AppWindow(object):
self.status['text'] = _("You're not docked at a station!")
play_bad = True
# Ignore possibly missing shipyard info
- elif (config.getint('output') & config.OUT_MKT_EDDN)\
+ elif (config.getint('output') & config.OUT_MKT_EDDN) \
and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
if not self.status['text']:
self.status['text'] = _("Station doesn't have anything!")
@@ -585,12 +592,12 @@ class AppWindow(object):
if not data['commander'].get('docked'):
# might have un-docked while we were waiting for retry in which case station data is unreliable
pass
- elif (data.get('lastSystem', {}).get('name') == monitor.system and
+ elif (data.get('lastSystem', {}).get('name') == monitor.system and
data.get('lastStarport', {}).get('name') == monitor.station and
data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')):
self.eddn.export_shipyard(data, monitor.is_beta)
elif tries > 1: # bogus data - retry
- self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries-1))
+ self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries - 1))
except Exception:
pass
@@ -600,11 +607,11 @@ class AppWindow(object):
def crewroletext(role):
# Return translated crew role. Needs to be dynamic to allow for changing language.
return {
- None: '',
- 'Idle': '',
+ None : '',
+ 'Idle' : '',
'FighterCon': _('Fighter'), # Multicrew role
- 'FireCon': _('Gunner'), # Multicrew role
- 'FlightCon': _('Helm'), # Multicrew role
+ 'FireCon' : _('Gunner'), # Multicrew role
+ 'FlightCon' : _('Helm'), # Multicrew role
}.get(role, role)
while True:
@@ -626,8 +633,8 @@ class AppWindow(object):
self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship.configure(
text=monitor.state['ShipName']
- or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType'])
- or '',
+ or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType'])
+ or '',
url=self.shipyard_url)
else:
self.cmdr['text'] = ''
@@ -673,7 +680,7 @@ class AppWindow(object):
logger.info("Can't start Status monitoring")
# Export loadout
- if entry['event'] == 'Loadout' and not monitor.state['Captain']\
+ if entry['event'] == 'Loadout' and not monitor.state['Captain'] \
and config.getint('output') & config.OUT_SHIP:
monitor.export_ship()
@@ -690,10 +697,10 @@ class AppWindow(object):
hotkeymgr.play_bad()
# Auto-Update after docking, but not if auth callback is pending
- if entry['event'] in ('StartUp', 'Location', 'Docked')\
- and monitor.station\
- and not config.getint('output') & config.OUT_MKT_MANUAL\
- and config.getint('output') & config.OUT_STATION_ANY\
+ if entry['event'] in ('StartUp', 'Location', 'Docked') \
+ and monitor.station \
+ and not config.getint('output') & config.OUT_MKT_MANUAL \
+ and config.getint('output') & config.OUT_STATION_ANY \
and companion.session.state != companion.Session.STATE_AUTH:
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
@@ -760,10 +767,10 @@ class AppWindow(object):
return f'file://localhost/{file_name}'
def system_url(self, system):
- return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system)
+ return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system)
def station_url(self, station):
- return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station)
+ return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station)
def cooldown(self):
if time() < self.holdofftime:
@@ -837,7 +844,7 @@ class AppWindow(object):
############################################################
# version
- ttk.Label(frame).grid(row=row, column=0) # spacer
+ ttk.Label(frame).grid(row=row, column=0) # spacer
row += 1
self.appversion_label = tk.Label(frame, text=appversion)
self.appversion_label.grid(row=row, column=0, sticky=tk.E)
@@ -855,7 +862,7 @@ class AppWindow(object):
############################################################
#
- ttk.Label(frame).grid(row=row, column=0) # spacer
+ ttk.Label(frame).grid(row=row, column=0) # spacer
row += 1
self.copyright = tk.Label(frame, text=copyright)
self.copyright.grid(row=row, columnspan=3, sticky=tk.EW)
@@ -864,7 +871,7 @@ class AppWindow(object):
############################################################
# OK button to close the window
- ttk.Label(frame).grid(row=row, column=0) # spacer
+ ttk.Label(frame).grid(row=row, column=0) # spacer
row += 1
button = ttk.Button(frame, text=_('OK'), command=self.apply)
button.grid(row=row, column=2, sticky=tk.E)
@@ -895,7 +902,7 @@ class AppWindow(object):
last_system: str = data.get("lastSystem", {}).get("name", "Unknown")
last_starport: str = ''
if data['commander'].get('docked'):
- last_starport = '.'+data.get('lastStarport', {}).get('name', 'Unknown')
+ last_starport = '.' + data.get('lastStarport', {}).get('name', 'Unknown')
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
f = tkinter.filedialog.asksaveasfilename(parent=self.w,
defaultextension=default_extension,
@@ -971,6 +978,7 @@ def enforce_single_instance() -> None:
if platform == 'win32':
import ctypes
from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM
+
EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806
GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806
@@ -1004,9 +1012,9 @@ def enforce_single_instance() -> None:
def enumwindowsproc(window_handle, l_param):
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
cls = ctypes.create_unicode_buffer(257)
- if GetClassName(window_handle, cls, 257)\
- and cls.value == 'TkTopLevel'\
- and window_title(window_handle) == applongname\
+ if GetClassName(window_handle, cls, 257) \
+ and cls.value == 'TkTopLevel' \
+ and window_title(window_handle) == applongname \
and GetProcessHandleFromHwnd(window_handle):
# If GetProcessHandleFromHwnd succeeds then the app is already running as this user
if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect):
@@ -1028,38 +1036,53 @@ def test_logging():
logger.debug('Test from EDMarketConnector.py top-level test_logging()')
-# Run the app
-if __name__ == "__main__":
- enforce_single_instance()
-
- from EDMCLogging import logger
- logger.info(f'Startup v{appversion} : Running on Python v{sys.version}')
- logger.debug(f'''Platform: {sys.platform}
-argv[0]: {sys.argv[0]}
-exec_prefix: {sys.exec_prefix}
-executable: {sys.executable}
-sys.path: {sys.path}
+def log_locale(prefix: str) -> None:
+ logger.debug(f'''Locale: {prefix}
Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)}
Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)}
Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)}
Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)}
Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
-)
+ )
+
+
+# Run the app
+if __name__ == "__main__":
+ enforce_single_instance()
+
+ from EDMCLogging import logger
+
+ logger.info(f'Startup v{appversion} : Running on Python v{sys.version}')
+ logger.debug(f'''Platform: {sys.platform}
+argv[0]: {sys.argv[0]}
+exec_prefix: {sys.exec_prefix}
+executable: {sys.executable}
+sys.path: {sys.path}'''
+ )
# Change locale to a utf8 one
- # First make sure the local is actually set as per locale's idea of defaults
+ # Log what we have at startup
+ log_locale('Initial Locale')
+ # Make sure the local is actually set as per locale's idea of defaults
locale.setlocale(locale.LC_ALL, '')
+ log_locale('After LC_ALL defaults set')
# Now find out the current locale, mostly the language
locale_startup = locale.getlocale(locale.LC_ALL)
+ logger.debug(f'Locale LC_ALL: {locale_startup}')
# Now set that same language, but utf8 encoding (it was probably cp1252
# or equivalent for other languages).
- locale.setlocale(locale.LC_ALL, (locale_startup[0], 'utf8'))
+ # UTF-8, not utf8:
+ locale.setlocale(locale.LC_ALL, (locale_startup[0], 'UTF-8'))
+ log_locale('After switching to UTF-8 encoding (same language)')
# TODO: unittests in place of these
# logger.debug('Test from __main__')
# test_logging()
+
class A(object):
+
class B(object):
+
def __init__(self):
logger.debug('A call from A.B.__init__')
@@ -1102,7 +1125,8 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
# Now the string should match, so try translation
popup_text = _(popup_text)
# And substitute in the other words.
- popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled')
+ popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'),
+ DISABLED='.disabled')
# And now we do need these to be actual \r\n
popup_text = popup_text.replace('\\n', '\n')
popup_text = popup_text.replace('\\r', '\r')
diff --git a/L10n/cs.strings b/L10n/cs.strings
index 36e26ab8..fad071e0 100644
--- a/L10n/cs.strings
+++ b/L10n/cs.strings
@@ -1,9 +1,6 @@
/* Label for 'UI Scaling' option [prefs.py] */
"UI Scale Percentage" = "Procenta UI škálování";
-
-/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
-"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 je výchozí hodnota{CR}Je potřeba restartovat{CR}aby se změna projevila!";
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 je výchozí hodnota{CR}je potřeba restartovat,{CR}aby se změna projevila!";
@@ -535,8 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} vyžaduje oprávnění pro použití klávesových zkratek";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "UI škálování";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Míra logování";
diff --git a/L10n/de.strings b/L10n/de.strings
index e70acc1a..73239607 100644
--- a/L10n/de.strings
+++ b/L10n/de.strings
@@ -1,9 +1,6 @@
/* Label for 'UI Scaling' option [prefs.py] */
"UI Scale Percentage" = "UI-Skalierung in Prozent";
-
-/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
-"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 bedeutet Standardwert{CR}Neustart erforderlich, um{CR}Änderungen zu übernehmen!";
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 bedeutet Standardwert{CR}Neustart erforderlich, um{CR}Änderungen zu übernehmen!";
@@ -535,8 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} benötigt für Tastenkürzel Systemrechte (Bedienungshilfen)";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "UI-Skalierung";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Log-Level";
diff --git a/L10n/en.template b/L10n/en.template
index a8ca6dc6..cf14aa37 100644
--- a/L10n/en.template
+++ b/L10n/en.template
@@ -1,3 +1,9 @@
+/* Label for 'UI Scaling' option [prefs.py] */
+"UI Scale Percentage" = "UI Scale Percentage";
+
+/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
+"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!";
+
/* Language name */
"!Language" = "English";
@@ -526,11 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts";
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scale Percentage" = "UI Scale Percentage";
-
-/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
-"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!";
-
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Log Level";
diff --git a/L10n/fi.strings b/L10n/fi.strings
index fd0ff40a..e3e5eacb 100644
--- a/L10n/fi.strings
+++ b/L10n/fi.strings
@@ -1,3 +1,9 @@
+/* Label for 'UI Scaling' option [prefs.py] */
+"UI Scale Percentage" = "Käyttöliittymän skaalausprosentti";
+
+/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
+"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 tarkoittaa oletusta{CR}Muutosten käyttöönotto{CR}edellyttää uudelleenkäynnistystä!";
+
/* Language name */
"!Language" = "Suomi";
@@ -526,3 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} tarvitsee lupasi pikakuvakkeiden käyttämiseen";
+/* Label for user configured level of logging [prefs.py] */
+"Log Level" = "Lokin syvyys";
diff --git a/L10n/it.strings b/L10n/it.strings
index a6659043..0706161f 100644
--- a/L10n/it.strings
+++ b/L10n/it.strings
@@ -526,8 +526,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} ha bisogno dei permessi per usare le scorciatoie";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "Adattamento UI";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Livello di Log";
diff --git a/L10n/ja.strings b/L10n/ja.strings
index 61d89a98..64bca474 100644
--- a/L10n/ja.strings
+++ b/L10n/ja.strings
@@ -1,9 +1,6 @@
/* Label for 'UI Scaling' option [prefs.py] */
"UI Scale Percentage" = "UIスケール比率";
-
-/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
-"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0はデフォルトを意味します{CR}変更した値を反映するには{CR}再起動が必要です!";
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100はデフォルトを意味します{CR}変更した値を反映するには{CR}再起動が必要です!";
@@ -535,8 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} がショートカットを利用するには許可が必要です";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "UIスケーリング";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "ログレベル";
diff --git a/L10n/pt-BR.strings b/L10n/pt-BR.strings
index eaeb23b1..7e6b1e5f 100644
--- a/L10n/pt-BR.strings
+++ b/L10n/pt-BR.strings
@@ -1,9 +1,6 @@
/* Label for 'UI Scaling' option [prefs.py] */
"UI Scale Percentage" = "Porcentagem de escala da IU";
-
-/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
-"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 significa Padrão{CR}Necessário Reiniciar para as{CR}alterações entrarem em vigor!";
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 significa Padrão{CR}Necessário Reiniciar para as{CR}alterações entrarem em vigor!";
@@ -535,8 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} precisa de permissão para usar atalhos";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "Escala da UI";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Nível de Log";
diff --git a/L10n/pt-PT.strings b/L10n/pt-PT.strings
index 9cd3d0b7..79542cf1 100644
--- a/L10n/pt-PT.strings
+++ b/L10n/pt-PT.strings
@@ -1,3 +1,9 @@
+/* Label for 'UI Scaling' option [prefs.py] */
+"UI Scale Percentage" = "Percentagem da escala de UI";
+
+/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
+"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 significa Predefinido{CR}Requer reinício para{CR}que as mudanças façam efeito!";
+
/* Language name */
"!Language" = "Português (Portugal)";
@@ -526,3 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "A {APP} precisa de autorização para usar atalhos";
+/* Label for user configured level of logging [prefs.py] */
+"Log Level" = "Nível de Log";
diff --git a/L10n/ru.strings b/L10n/ru.strings
index b31c20ac..64a8c6dc 100644
--- a/L10n/ru.strings
+++ b/L10n/ru.strings
@@ -1,3 +1,9 @@
+/* Label for 'UI Scaling' option [prefs.py] */
+"UI Scale Percentage" = "Процент масштабирования интерфейса";
+
+/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
+"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 - значение по умолчанию{CR}требуется перезапуск для вступления в силу изменений{CR}!";
+
/* Language name */
"!Language" = "Русский";
@@ -56,7 +62,7 @@
"Chief Petty Officer" = "Главный старшина";
/* Main window. [EDMarketConnector.py] */
-"Cmdr" = "Командир";
+"Cmdr" = "КМДР";
/* Ranking. [stats.py] */
"Combat" = "Боевой";
@@ -86,7 +92,7 @@
"Dangerous" = "Опасный";
/* Appearance theme setting. [prefs.py] */
-"Dark" = "Темная";
+"Dark" = "Тёмная";
/* Combat rank. [stats.py] */
"Deadly" = "Смертоносный";
@@ -101,7 +107,7 @@
"Delay sending until docked" = "Отложить отправку данных до завершения стыковки";
/* Option to disabled Automatic Check For Updates whilst in-game [prefs.py] */
-"Disable Automatic Application Updates Check when in-game" = "Отключить автоматическое обновление приложения во время нахождения в игре.";
+"Disable Automatic Application Updates Check when in-game" = "Отключить автоматическое обновление приложения во время нахождения в игре";
/* List of plugins in settings. [prefs.py] */
"Disabled Plugins" = "Отключенные плагины";
@@ -125,13 +131,13 @@
"Elite" = "Элита";
/* Section heading in settings. [edsm.py] */
-"Elite Dangerous Star Map credentials" = "Учетные данные Elite Dangerous Star Map";
+"Elite Dangerous Star Map credentials" = "Учётные данные Elite Dangerous Star Map";
/* Ranking. [stats.py] */
"Empire" = "Империя";
/* List of plugins in settings. [prefs.py] */
-"Enabled Plugins" = "Включенные плагины";
+"Enabled Plugins" = "Включённые плагины";
/* Federation rank. [stats.py] */
"Ensign" = "Энсин";
@@ -221,7 +227,7 @@
"Hotkey" = "Горячая клавиша";
/* Section heading in settings. [inara.py] */
-"Inara credentials" = "Учетные данные Inara";
+"Inara credentials" = "Учётные данные Inara";
/* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */
"Keyboard shortcut" = "Сочетание клавиш";
@@ -302,7 +308,7 @@
"Open" = "Открыть";
/* Shortcut settings button on OSX. [prefs.py] */
-"Open System Preferences" = "Открыты «Системные настройки»";
+"Open System Preferences" = "Открыты «Настройки системы»";
/* Tab heading in settings. [prefs.py] */
"Output" = "Данные";
@@ -356,7 +362,7 @@
"Post Commander" = "Начальник гарнизона";
/* Ranking. [stats.py] */
-"Powerplay" = "Ранг в Игре Сил";
+"Powerplay" = "Ранг у галактических держав";
/* [prefs.py] */
"Preferences" = "Настройки";
@@ -416,13 +422,13 @@
"Semi Professional" = "Полупрофессионал";
/* [edsm.py] */
-"Send flight log and Cmdr status to EDSM" = "Отправлять данные полетного журнала и информацию статусе командира в EDSM";
+"Send flight log and Cmdr status to EDSM" = "Отправлять данные журнала полёта и информацию статусе командира в EDSM";
/* [inara.py] */
-"Send flight log and Cmdr status to Inara" = "Отправлять журнал полёта и состояние командира в Inara";
+"Send flight log and Cmdr status to Inara" = "Отправлять данные журнала полёта и информацию статусе командира в Inara";
/* Output setting. [eddn.py] */
-"Send station data to the Elite Dangerous Data Network" = "Отправлять станционную информацию на Elite Dangerous Data Network";
+"Send station data to the Elite Dangerous Data Network" = "Отправлять информацию о станциях на Elite Dangerous Data Network";
/* Output setting new in E:D 2.2. [eddn.py] */
"Send system and scan data to the Elite Dangerous Data Network" = "Отправлять данные о системе и данные сканирования в Elite Dangerous Data Network";
@@ -437,13 +443,13 @@
"Settings" = "Настройки";
/* Main window. [EDMarketConnector.py] */
-"Ship" = "Судно";
+"Ship" = "Корабль";
/* Output setting. [prefs.py] */
-"Ship loadout" = "Оборудование судна";
+"Ship loadout" = "Оборудование корабля";
/* Status dialog title. [stats.py] */
-"Ships" = "Суда";
+"Ships" = "Корабли";
/* Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis. [prefs.py] */
"Shipyard" = "Верфи";
@@ -491,7 +497,7 @@
"Update" = "Обновить данные";
/* Option to use alternate URL method on shipyard links [prefs.py] */
-"Use alternate URL method" = "Альтернативный URL(адрес)";
+"Use alternate URL method" = "Альтернативный URL (адрес)";
/* Status dialog subtitle - CR value of ship. [stats.py] */
"Value" = "Стоимость";
@@ -509,7 +515,7 @@
"Warrant Officer" = "Уорент-офицер";
/* Shouldn't happen. [EDMarketConnector.py] */
-"What are you flying?!" = "Невозможно определить тип судна!";
+"What are you flying?!" = "Невозможно определить тип корабля!";
/* Shouldn't happen. [EDMarketConnector.py] */
"Where are you?!" = "Невозможно определить местоположение!";
@@ -521,13 +527,10 @@
"Window" = "Окно";
/* [EDMarketConnector.py] */
-"You're not docked at a station!" = "Ваше судно не пристыковано к станции!";
+"You're not docked at a station!" = "Ваш корабль не пристыкован к станции!";
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} требуется разрешение на использование глобальных сочетаний клавиш";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "Масштабирование интерфейса";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Уровень лога";
diff --git a/L10n/sr-Latn-BA.strings b/L10n/sr-Latn-BA.strings
index 0c55376b..70c6e66b 100644
--- a/L10n/sr-Latn-BA.strings
+++ b/L10n/sr-Latn-BA.strings
@@ -1,9 +1,6 @@
/* Label for 'UI Scaling' option [prefs.py] */
"UI Scale Percentage" = "Procenat UI skaliranja";
-
-/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
-"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 je standardna vrijednost{CR}Potreban je restart{CR}za primjenu podešavanja!";
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 je standardna vrijednost{CR}Potreban je restart{CR}za primjenu podešavanja!";
@@ -535,8 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} traži dozvolu da koristi prečice";
-
-/* Label for 'UI Scaling' option [prefs.py] */
-"UI Scaling" = "UI skaliranje";
/* Label for user configured level of logging [prefs.py] */
"Log Level" = "Log nivo";
diff --git a/L10n/sr-Latn.strings b/L10n/sr-Latn.strings
index 45eacc40..39f5bce3 100644
--- a/L10n/sr-Latn.strings
+++ b/L10n/sr-Latn.strings
@@ -1,3 +1,9 @@
+/* Label for 'UI Scaling' option [prefs.py] */
+"UI Scale Percentage" = "Procenat UI skaliranja";
+
+/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
+"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 znači podrazumevano{CR}Restart je potreban{CR}da bi se promene primenile!";
+
/* Language name */
"!Language" = "Srpski (Latinica)";
@@ -526,3 +532,5 @@
/* Shortcut settings prompt on OSX. [prefs.py] */
"{APP} needs permission to use shortcuts" = "{APP} traži dozvolu da koristi skraćenice";
+/* Label for user configured level of logging [prefs.py] */
+"Log Level" = "Log Nivo";
diff --git a/PLUGINS.md b/PLUGINS.md
index c391a3ab..717171f7 100644
--- a/PLUGINS.md
+++ b/PLUGINS.md
@@ -3,6 +3,7 @@
Plugins allow you to customise and extend the behavior of EDMC.
## Installing a Plugin
+
See [Plugins](https://github.com/EDCD/EDMarketConnector/wiki/Plugins) on the
wiki.
@@ -12,16 +13,22 @@ Plugins are loaded when EDMC starts up.
Each plugin has it's own folder in the `plugins` directory:
-* Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins`
-* Mac: `~/Library/Application Support/EDMarketConnector/plugins`
-* Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or
- `~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset.
+- Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins`
+- Mac: `~/Library/Application Support/EDMarketConnector/plugins`
+- Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or `~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset.
Plugins are python files. The plugin folder must have a file named `load.py`
that must provide one module level function and optionally provide a few
others.
---
+
+### Examples
+
+We have some example plugins available in the docs/examples directory. See the readme in each folder for more info.
+
+---
+
### Available imports
**`import`ing anything from the core EDMarketConnector code that is not
@@ -32,34 +39,36 @@ breaking with future code changes.**
`from theme import theme` - So plugins can theme their own UI elements to
match the main UI.
-
+
`from config import appname, applongname, appcmdname, appversion
, copyright, config` - to access config.
`from prefs import prefsVersion` - to allow for versioned preferences.
`from companion import category_map` - Or any of the other static date
- contained therein. NB: There's a plan to move such to a `data` module.
+contained therein. NB: There's a plan to move such to a `data` module.
`import plug` - Mostly for using `plug.show_error()`. Also the flags
- for `dashboard_entry()` to be useful (see example below). Relying on anything
- else isn't supported.
-
+for `dashboard_entry()` to be useful (see example below). Relying on anything
+else isn't supported.
+
`from monitor import gamerunning` - in case a plugin needs to know if we
think the game is running.
`import timeout_session` - provides a method called `new_session` that creates a requests.session with a default timeout
on all requests. Recommended to reduce noise in HTTP requests
-
```python
from ttkHyperlinkLabel import HyperlinkLabel
import myNotebook as nb
```
+
For creating UI elements.
---
+
### Logging
+
In the past the only way to provide any logged output from a
plugin was to use `print(...)` statements. When running the application from
the packaged executeable all output is redirected to a log file. See
@@ -70,8 +79,8 @@ EDMC now implements proper logging using the Python `logging` module. Plugin
developers should now use the following code instead of simple `print(...)`
statements.
-Insert this at the top-level of your load.py file (so not inside
-`plugin_start3()` ):
+Insert this at the top-level of your load.py file (so not inside `plugin_start3()` ):
+
```python
import logging
@@ -82,6 +91,8 @@ plugin_name = os.path.basename(os.path.dirname(__file__))
# A Logger is used per 'found' plugin to make it easy to include the plugin's
# folder name in the logging output format.
+# NB: plugin_name here *must* be the plugin's folder name as per the preceding
+# code, else the logger won't be properly set up.
logger = logging.getLogger(f'{appname}.{plugin_name}')
# If the Logger has handlers then it was already set up by the core code, else
@@ -98,6 +109,13 @@ if not logger.hasHandlers():
logger.addHandler(logger_channel)
```
+Note the admonishment about `plugin_name` being the folder name of your plugin.
+It can't be anything else (such as a different string returned from
+`plugin_start3()`) because the code in plug.py that sets up the logger uses
+exactly the folder name. Our custom `qualname` and `class` formatters won't
+work with a 'bare' logger, and will cause your code to throw exceptions if
+you're not using our supplied logger.
+
If running with 4.1.0-beta1 or later of EDMC the logging setup happens in
the core code and will include the extra logfile destinations. If your
plugin is run under a pre-4.1.0 version of EDMC then the above will set up
@@ -132,7 +150,7 @@ Replace all `print(...)` statements with one of the following:
logger.debug('Exception we only note in debug output', exc_info=e)
```
-Remember you can use fstrings to include variables, and even the returns of
+Remember you can use [fstrings](https://www.python.org/dev/peps/pep-0498/) to include variables, and even the returns of
functions, in the output.
```python
@@ -140,40 +158,50 @@ functions, in the output.
```
---
+
### Startup
+
EDMC will import the `load.py` file as a module and then call the
`plugin_start3()` function.
```python
-def plugin_start3(plugin_dir):
+def plugin_start3(plugin_dir: str) -> str:
"""
Load this plugin into EDMC
"""
- print("I am loaded! My plugin folder is {}".format(plugin_dir))
+ print(f"I am loaded! My plugin folder is {plugin_dir}")
return "Test"
```
+
The string you return is used as the internal name of the plugin.
Any errors or print statements from your plugin will appear in
`%TMP%\EDMarketConnector.log` on Windows, `$TMPDIR/EDMarketConnector.log` on
Mac, and `$TMP/EDMarketConnector.log` on Linux.
+| Parameter | Type | Description |
+| :----------- | :---: | :------------------------------------------------------ |
+| `plugin_dir` | `str` | The directory that your plugin is located in. |
+| `RETURN` | `str` | The name you want to be used for your plugin internally |
+
### Shutdown
+
This gets called when the user closes the program:
```python
-def plugin_stop():
+def plugin_stop() -> None:
"""
EDMC is closing
"""
print("Farewell cruel world!")
```
-If your plugin uses one or more threads to handle Events then stop and join()
-the threads before returning from this function.
+If your plugin uses one or more threads to handle Events then `stop()` and `join()`
+(to wait for their exit -- Recommended, not required) the threads before returning from this function.
## Plugin Hooks
-### Configuration
+
+### Configuration
If you want your plugin to be configurable via the GUI you can define a frame
(panel) to be displayed on its own tab in EDMC's settings dialog. The tab
@@ -192,39 +220,53 @@ numbers in a locale-independent way.
```python
import tkinter as tk
+from tkinter import ttk
import myNotebook as nb
from config import config
+from typing import Optional
-this = sys.modules[__name__] # For holding module globals
+my_setting: Optional[tk.IntVar] = None
-def plugin_prefs(parent, cmdr, is_beta):
+def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]:
"""
Return a TK Frame for adding to the EDMC settings dialog.
"""
- this.mysetting = tk.IntVar(value=config.getint("MyPluginSetting")) # Retrieve saved value from config
+ global my_setting
+ my_setting = tk.IntVar(value=config.getint("MyPluginSetting")) # Retrieve saved value from config
frame = nb.Frame(parent)
nb.Label(frame, text="Hello").grid()
nb.Label(frame, text="Commander").grid()
- nb.Checkbutton(frame, text="My Setting", variable=this.mysetting).grid()
+ nb.Checkbutton(frame, text="My Setting", variable=my_setting).grid()
return frame
```
+| Parameter | Type | Description |
+| :-------- | :-----------: | :----------------------------------------------- |
+| `parent` | `nb.Notebook` | Root Notebook object the preferences window uses |
+| `cmdr` | `str` | The current commander |
+| `is_beta` | `bool` | If the game is currently a beta version |
+
This gets called when the user dismisses the settings dialog:
```python
-def prefs_changed(cmdr, is_beta):
+def prefs_changed(cmdr: str, is_beta: bool) -> None:
"""
Save settings.
"""
- config.set('MyPluginSetting', this.mysetting.getint()) # Store new value in config
+ config.set('MyPluginSetting', my_setting.get()) # Store new value in config
```
+| Parameter | Type | Description |
+| :-------- | :----: | :-------------------------------------- |
+| `cmdr` | `str` | The current commander |
+| `is_beta` | `bool` | If the game is currently a beta version |
+
### Display
You can also have your plugin add an item to the EDMC main window and update
from your event hooks. This works in the same way as `plugin_prefs()`. For a
-simple one-line item return a tk.Label widget or a pair of widgets as a tuple.
+simple one-line item return a `tk.Label` widget or a 2 tuple of widgets.
For a more complicated item create a tk.Frame widget and populate it with other
ttk widgets. Return `None` if you just want to use this as a callback after the
main window and all other plugins are initialised.
@@ -233,42 +275,62 @@ You can use `stringFromNumber()` from EDMC's `l10n.Locale` object to format
numbers in your widgets in a locale-independent way.
```python
-this = sys.modules[__name__] # For holding module globals
+from typing import Optional, Tuple
+import tkinter as tk
-def plugin_app(parent):
+status: Optional[tk.Label]
+
+
+def plugin_app(parent: tk.Frame) -> Tuple[tk.Label, tk.Label]:
"""
Create a pair of TK widgets for the EDMC main window
"""
+ global status
label = tk.Label(parent, text="Status:") # By default widgets inherit the current theme's colors
- this.status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color
- return (label, this.status)
-
+ status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color
+ return (label, status)
+
# later on your event functions can update the contents of these widgets
- this.status["text"] = "Happy!"
- this.status["foreground"] = "green"
+def some_other_function() -> None:
+ global status
+ status["text"] = "Happy!"
+ status["foreground"] = "green"
```
-You can dynamically add and remove widgets on the main window by returning a
-tk.Frame from `plugin_app()` and later creating and destroying child widgets
-of that frame.
+| Parameter | Type | Description |
+| :-------- | :---------------------------------------------: | :---------------------------------------------------------- |
+| `parent` | `tk.Frame` | The root EDMC window |
+| `RETURN` | `Union[tk.Widget, Tuple[tk.Widget, tk.Widget]]` | A widget to add to the main window. See below for more info |
+
+The return from plugin_app can either be any widget (Frame, Label, Notebook, etc.), or a 2 tuple of widgets. In the case of
+a 2 tuple, indices 0 and 1 are placed automatically in the outer grid on column indices 0 and 1. Otherwise, the only thing done
+to your return widget is it is set to use a columnspan of 2, and placed on the grid.
+
+You can dynamically add and remove widgets on the main window by returning a tk.Frame from `plugin_app()` and later
+creating and destroying child widgets of that frame.
```python
+from typing import Option
+import tkinter as tk
+
from theme import theme
-this = sys.modules[__name__] # For holding module globals
+frame: Optional[tk.Frame] = None
-def plugin_app(parent):
+def plugin_app(parent: tk.Frame) -> tk.Frame:
"""
Create a frame for the EDMC main window
"""
- this.frame = tk.Frame(parent)
- return this.frame
+ global frame
+ frame = tk.Frame(parent)
+ return frame
+def some_other_function_called_later() -> None:
# later on your event functions can add or remove widgets
- row = this.frame.grid_size()[1]
- new_widget_1 = tk.Label(this.frame, text="Status:")
+ row = frame.grid_size()[1]
+ new_widget_1 = tk.Label(frame, text="Status:")
new_widget_1.grid(row=row, column=0, sticky=tk.W)
- new_widget_2 = tk.Label(this.frame, text="Unhappy!", foreground="red") # Override theme's foreground color
+ new_widget_2 = tk.Label(frame, text="Unhappy!", foreground="red") # Override theme's foreground color
new_widget_2.grid(row=row, column=1, sticky=tk.W)
theme.update(this.frame) # Apply theme colours to the frame and its children, including the new widgets
```
@@ -295,58 +357,54 @@ for an example of these techniques.
#### Journal Entry
```python
-def journal_entry(cmdr, is_beta, system, station, entry, state):
+def journal_entry(
+ cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any]
+) -> None:
if entry['event'] == 'FSDJump':
# We arrived at a new system!
if 'StarPos' in entry:
- sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos'])))
+ logger.info(f'Arrived at {entry["StarSystem"]} {entry["StarPos"')
+
else:
- sys.stderr.write("Arrived at {}\n".format(entry['StarSystem']))
+ logger.info(f'Arrived at {entry["StarSystem"]}')
```
This gets called when EDMC sees a new entry in the game's journal.
-
-- `cmdr` is a `str` denoting the current Commander Name.
-- `is_beta` is a `bool` denoting if data came from a beta version of the game.
-- `system` is a `str` holding the name of the current system, or `None` if not
- yet known.
-- `station` is a `str` holding the name of the current station, or `None` if
- not yet known or appropriate.
-- `entry` is an `OrderedDict` holding the Journal event.
-- `state` is a `dictionary` containing information about the Cmdr and their
- ship and cargo (including the effect of the current journal entry).
- - `Captain` - `str` of name of Commander's crew you joined in multi-crew,
- else `None`
- - `Cargo` - `dict` with details of current cargo.
- - `Credits` - Current credit balance.
- - `FID` - Frontier Cmdr ID
- - `Horizons` - `bool` denoting if Horizons expansion active.
- - `Loan` - Current loan amount, else None.
- - `Raw` - `dict` with details of "Raw" materials held.
- - `Manufactured` - `dict` with details of "Manufactured" materials held.
- - `Encoded` - `dict` with details of "Encoded" materials held.
- - `Engineers` - `dict` with details of Rank Progress for Engineers.
- - `Rank` - `dict` of current Ranks. Each entry is a `tuple` of
- (,