diff --git a/EDMC.py b/EDMC.py index 6b72c7d1..fd241ec2 100755 --- a/EDMC.py +++ b/EDMC.py @@ -10,12 +10,12 @@ from __future__ import annotations import argparse import json -import locale import os import queue import sys from time import sleep, time from typing import TYPE_CHECKING, Any +from common_utils import log_locale, SERVER_RETRY # isort: off os.environ["EDMC_NO_UI"] = "1" @@ -52,31 +52,12 @@ import eddn # noqa: E402 # isort: on - - -def log_locale(prefix: str) -> None: - """Log the current state of locale settings.""" - 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)}''' - ) - - tr.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, \ EXIT_JOURNAL_READ_ERR, EXIT_COMMANDER_UNKNOWN = range(9) -def versioncmp(versionstring) -> list: - """Quick and dirty version comparison assuming "strict" numeric only version numbers.""" - return list(map(int, versionstring.split('.'))) - - def deep_get(target: dict | companion.CAPIData, *args: str, default=None) -> Any: """ Walk into a dict and return the specified deep value. @@ -108,7 +89,7 @@ def deep_get(target: dict | companion.CAPIData, *args: str, default=None) -> Any return current -def main(): # noqa: C901, CCR001 +def main() -> None: # noqa: C901, CCR001 """Run the main code of the program.""" try: # arg parsing diff --git a/EDMCLogging.py b/EDMCLogging.py index b6612570..59c8e312 100644 --- a/EDMCLogging.py +++ b/EDMCLogging.py @@ -335,8 +335,6 @@ class EDMCContextFilter(logging.Filter): # try: frame_info = inspect.getframeinfo(frame) - # raise(IndexError) # TODO: Remove, only for testing - except Exception: # Separate from the print below to guarantee we see at least this much. print('EDMCLogging:EDMCContextFilter:caller_attributes(): Failed in `inspect.getframinfo(frame)`') diff --git a/EDMCSystemProfiler.py b/EDMCSystemProfiler.py index 873282b8..a5d97ce4 100644 --- a/EDMCSystemProfiler.py +++ b/EDMCSystemProfiler.py @@ -38,17 +38,17 @@ from monitor import monitor from EDMCLogging import get_main_logger -def get_sys_report(config: config.AbstractConfig) -> str: +def get_sys_report(active_config: config.AbstractConfig) -> str: """Gather system information about Elite, the Host Computer, and EDMC.""" # Calculate Requested Information plt = platform.uname() locale.setlocale(locale.LC_ALL, "") lcl = locale.getlocale() - monitor.currentdir = config.get_str( - "journaldir", default=config.default_journal_dir + monitor.currentdir = active_config.get_str( + "journaldir", default=active_config.default_journal_dir ) if not monitor.currentdir: - monitor.currentdir = config.default_journal_dir + monitor.currentdir = active_config.default_journal_dir try: logfile = monitor.journal_newest_filename(monitor.currentdir) if logfile is None: @@ -108,21 +108,21 @@ def copy_sys_report(root: tk.Tk, report: str) -> None: messagebox.showinfo("System Profiler", "System Report copied to Clipboard", parent=root) -def main() -> None: +def main(active_config: config.AbstractConfig) -> None: """Entry Point for the System Profiler.""" # Now Let's Begin root: tk.Tk = tk.Tk() root.withdraw() # Hide the window initially to calculate the dimensions try: icon_image = tk.PhotoImage( - file=path.join(cur_config.respath_path, "io.edcd.EDMarketConnector.png") + file=path.join(active_config.respath_path, "io.edcd.EDMarketConnector.png") ) root.iconphoto(True, icon_image) except tk.TclError: - root.iconbitmap(path.join(cur_config.respath_path, "EDMarketConnector.ico")) + root.iconbitmap(path.join(active_config.respath_path, "EDMarketConnector.ico")) - sys_report = get_sys_report(cur_config) + sys_report = get_sys_report(active_config) # Set up styling style = ttk.Style(root) @@ -182,7 +182,8 @@ if __name__ == "__main__": # Args: Only work if not frozen parser = argparse.ArgumentParser( prog=appname, - description="Prints diagnostic and debugging information about the current EDMC configuration.", + description="Prints diagnostic and debugging information " + "about the current EDMC configuration.", ) parser.add_argument( "--out-console", @@ -203,4 +204,4 @@ if __name__ == "__main__": print(sys_report) sys.exit(0) - main() + main(cur_config) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 498872c5..2397393f 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -68,6 +68,7 @@ from config import appversion, appversion_nobuild, config, copyright from EDMCLogging import edmclogger, logger, logging from journal_lock import JournalLock, JournalLockResult from update import check_for_fdev_updates +from common_utils import log_locale, SERVER_RETRY if __name__ == '__main__': # noqa: C901 # Command-line arguments @@ -416,8 +417,6 @@ from monitor import monitor from theme import theme from ttkHyperlinkLabel import HyperlinkLabel, SHIPYARD_HTML_TEMPLATE -SERVER_RETRY = 5 # retry pause for Companion servers [s] - class AppWindow: """Define the main application window.""" @@ -837,8 +836,7 @@ class AppWindow: r' if downgrading between major versions with significant changes.\r\n\r\n' 'Do you want to open GitHub to download the latest release?' ) - update_msg = update_msg.replace('\\n', '\n') - update_msg = update_msg.replace('\\r', '\r') + update_msg = update_msg.replace('\\n', '\n').replace('\\r', '\r') stable_popup = tk.messagebox.askyesno(title=title, message=update_msg) if stable_popup: webbrowser.open("https://github.com/EDCD/eDMarketConnector/releases/latest") @@ -1979,17 +1977,6 @@ def test_logging() -> None: logger.debug('Test from EDMarketConnector.py top-level test_logging()') -def log_locale(prefix: str) -> None: - """Log all of the current local settings.""" - 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)}''' - ) - - def setup_killswitches(filename: str | None): """Download and setup the main killswitch list.""" logger.debug('fetching killswitches...') @@ -2061,17 +2048,15 @@ def validate_providers(): # LANG: Popup-text about Reset Providers popup_text = tr.tl(r'One or more of your URL Providers were invalid, and have been reset:\r\n\r\n') - for provider in reset_providers: + for provider, (old_prov, new_prov) in reset_providers.items(): # LANG: Text About What Provider Was Reset - popup_text += tr.tl(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n') - popup_text = popup_text.format( + popup_text += tr.tl(r'{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n').format( PROVIDER=provider, - OLDPROV=reset_providers[provider][0], - NEWPROV=reset_providers[provider][1] + OLDPROV=old_prov, + NEWPROV=new_prov ) # 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') + popup_text = popup_text.replace('\\n', '\n').replace('\\r', '\r') tk.messagebox.showinfo( # LANG: Popup window title for Reset Providers @@ -2243,8 +2228,7 @@ sys.path: {sys.path}''' 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') + detail = detail.replace('\\n', '\n').replace('\\r', '\r') msg = tk.messagebox.askyesno( title=title, message=message, detail=detail, icon=tkinter.messagebox.ERROR, type=tkinter.messagebox.YESNO, parent=root @@ -2275,8 +2259,7 @@ sys.path: {sys.path}''' 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') + popup_text = popup_text.replace('\\n', '\n').replace('\\r', '\r') tk.messagebox.showinfo( # LANG: Popup window title for list of 'broken' plugins that failed to load @@ -2306,8 +2289,7 @@ sys.path: {sys.path}''' 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') + popup_text = popup_text.replace('\\n', '\n').replace('\\r', '\r') tk.messagebox.showinfo( # LANG: Popup window title for list of 'enabled' plugins that don't work with Python 3.x diff --git a/PLUGINS.md b/PLUGINS.md index da7303f4..a120985e 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -153,6 +153,16 @@ See [#1327 - ModuleNotFound when creating a new plugin.](https://github.com/EDCD for some discussion. +## Common Resources + +Some plugins may wish to use resources available in a different plugin, or use +common assets across plugins. This is possible, however care must be taken to +ensure that the plugins do not attempt to load non-existent data or create +circular imports. + +For an example of how this is done, look at the code in `plugins/common_coreutils.py` +and the usage of these functions in other core plugins. + --- ## Logging diff --git a/build.py b/build.py index d14b4f62..e17c0979 100644 --- a/build.py +++ b/build.py @@ -121,6 +121,7 @@ def build() -> None: "plugins/edsy.py", "plugins/inara.py", "plugins/spansh_core.py", + "plugins/common_coreutils.py" ] options: dict = { "py2exe": { diff --git a/common_utils.py b/common_utils.py new file mode 100644 index 00000000..7e3a361e --- /dev/null +++ b/common_utils.py @@ -0,0 +1,61 @@ +""" +common_utils.py - Common functions and modules. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. +""" +from __future__ import annotations +import sys +import locale +from typing import TYPE_CHECKING +from EDMCLogging import get_main_logger + +if TYPE_CHECKING: + import tkinter as tk +logger = get_main_logger() + +SERVER_RETRY = 5 # retry pause for Companion servers [s] + +if sys.platform == 'win32': + import ctypes + from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL + import win32gui + try: + CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition + CalculatePopupWindowPosition.argtypes = [ + ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) + ] + CalculatePopupWindowPosition.restype = BOOL + except Exception: # Not supported under Wine 4.0 + CalculatePopupWindowPosition = None # type: ignore + + +def ensure_on_screen(self, parent: tk.Tk): + """ + Ensure a pop-up window is on the printable screen area. + + :param self: The calling class instance of tk.TopLevel + :param parent: The parent window + """ + # Ensure fully on-screen + if sys.platform == 'win32' and CalculatePopupWindowPosition: + position = RECT() + win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) + if CalculatePopupWindowPosition( + POINT(parent.winfo_rootx(), parent.winfo_rooty()), + SIZE(position.right - position.left, position.bottom - position.top), # type: ignore + 0x10000, None, position + ): + self.geometry(f"+{position.left}+{position.top}") + + +def log_locale(prefix: str) -> None: + """Log all of the current local settings.""" + 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)}''' + ) diff --git a/plugins/common_coreutils.py b/plugins/common_coreutils.py new file mode 100644 index 00000000..6b90d4dc --- /dev/null +++ b/plugins/common_coreutils.py @@ -0,0 +1,190 @@ +""" +common_coreutils.py - Common Plugin Functions. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. + +This is an EDMC 'core' plugin. + +All EDMC plugins are *dynamically* loaded at run-time. + +We build for Windows using `py2exe`. +`py2exe` can't possibly know about anything in the dynamically loaded core plugins. + +Thus, you **MUST** check if any imports you add in this file are only +referenced in this file (or only in any other core plugin), and if so... + + YOU MUST ENSURE THAT PERTINENT ADJUSTMENTS ARE MADE IN + `build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT + IN AN END-USER INSTALLATION ON WINDOWS. +""" +# pylint: disable=import-error +from __future__ import annotations + +from typing import Any, Mapping, cast +import tkinter as tk +import base64 +import gzip +import io +import json +import os +import myNotebook as nb # noqa: N813 +from EDMCLogging import get_main_logger +from companion import CAPIData +from l10n import translations as tr + +logger = get_main_logger() +if not os.getenv('EDMC_NO_UI'): # Functions using show_password_var MUST have TK set up + show_password_var = tk.BooleanVar() + +# Global Padding Preferences +PADX = 10 +BUTTONX = 12 # indent Checkbuttons and Radiobuttons +PADY = 1 # close spacing +BOXY = 2 # box spacing +SEPY = 10 # seperator line spacing +STATION_UNDOCKED = '×' # "Station" name to display when not docked = U+00D7 + + +def plugin_start3(plugin_dir: str) -> str: + """ + Start the plugin. + + :param plugin_dir: NAme of directory this was loaded from. + :return: Identifier string for this plugin. + """ + return 'CommonCoreUtils' + + +def api_keys_label_common(this, cur_row: int, frame: nb.Frame): + """ + Prepare the box for API Key Loading. This is an EDMC Common Function. + + :param this: The module global from the calling module. + :param cur_row: The current row in the calling module's config page. + :param frame: The current frame in the calling module's config page. + :return: The updated module global from the calling module. + """ + # LANG: EDSM API key label + this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) + this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) + this.apikey = nb.EntryMenu(frame, show="*", width=50) + this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) + return this + + +def show_pwd_var_common(frame: nb.Frame, cur_row: int, this): + """ + Allow unmasking of the API Key. This is an EDMC Common Function. + + :param cur_row: The current row in the calling module's config page. + :param frame: The current frame in the calling module's config page. + """ + show_password_var.set(False) # Password is initially masked + + show_password_checkbox = nb.Checkbutton( + frame, + text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key + variable=show_password_var, + command=lambda: toggle_password_visibility_common(this) + ) + show_password_checkbox.grid(row=cur_row, columnspan=2, padx=BUTTONX, pady=PADY, sticky=tk.W) + + +# Return a URL for the current ship +def shipyard_url_common(loadout: Mapping[str, Any]) -> str: + """ + Construct a URL for ship loadout. This is an EDMC Common Function. + + :param loadout: The ship loadout data. + :return: The constructed URL for the ship loadout. + """ + # Convert loadout to JSON and gzip compress it + string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') + if not string: + return '' + + out = io.BytesIO() + with gzip.GzipFile(fileobj=out, mode='w') as f: + f.write(string) + + encoded_data = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D') + return encoded_data + + +def station_link_common(data: CAPIData, this): + """ + Set the Staion Name. This is an EDMC Common Function. + + :param data: A CAPI Data Entry. + :param this: The module global from the calling module. + """ + if data['commander']['docked'] or this.on_foot and this.station_name: + this.station_link['text'] = this.station_name + elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": + this.station_link['text'] = STATION_UNDOCKED + else: + this.station_link['text'] = '' + + +def this_format_common(this, state: Mapping[str, Any]): + """ + Gather Common 'This' Elements. This is an EDMC Common Function. + + :param this: The module global from the calling module. + :param state: `monitor.state`. + """ + this.system_address = state['SystemAddress'] + this.system_name = state['SystemName'] + this.system_population = state['SystemPopulation'] + this.station_name = state['StationName'] + this.station_marketid = state['MarketID'] + this.station_type = state['StationType'] + this.on_foot = state['OnFoot'] + + +def toggle_password_visibility_common(this): + """ + Toggle if the API Key is visible or not. This is an EDMC Common Function. + + :param this: The module global from the calling module. + """ + if show_password_var.get(): + this.apikey.config(show="") # type: ignore + else: + this.apikey.config(show="*") # type: ignore + + +def station_name_setter_common(this): + """ + Set the Station Name. This is an EDMC Common Function. + + :param this: The module global from the calling module. + """ + to_set: str = cast(str, this.station_name) + if not to_set: + if this.system_population is not None and this.system_population > 0: + to_set = STATION_UNDOCKED + else: + to_set = '' + + this.station_link['text'] = to_set + + +def cmdr_data_initial_common(this, data: CAPIData): + """ + Set the common CMDR Data. This is an EDMC Common Function. + + :param this: The module global from the calling module. + :param data: The latest merged CAPI data. + """ + # Always store initially, even if we're not the *current* system provider. + if not this.station_marketid and data['commander']['docked']: + this.station_marketid = data['lastStarport']['id'] + + # Only trust CAPI if these aren't yet set + if not this.system_name: + this.system_name = data['lastSystem']['name'] + if not this.station_name and data['commander']['docked']: + this.station_name = data['lastStarport']['name'] diff --git a/plugins/coriolis.py b/plugins/coriolis.py index a9eabced..89f128f7 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -19,12 +19,10 @@ referenced in this file (or only in any other core plugin), and if so... `build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER INSTALLATION ON WINDOWS. """ +# pylint: disable=import-error from __future__ import annotations -import base64 -import gzip -import io -import json +from typing import Any, Mapping import tkinter as tk from tkinter import ttk import myNotebook as nb # noqa: N813 # its not my fault. @@ -32,6 +30,7 @@ from EDMCLogging import get_main_logger from plug import show_error from config import config from l10n import translations as tr +from plugins.common_coreutils import PADX, PADY, BOXY, shipyard_url_common class CoriolisConfig: @@ -82,10 +81,6 @@ def plugin_start3(path: str) -> str: def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame: """Set up plugin preferences.""" - PADX = 10 # noqa: N806 - PADY = 1 # noqa: N806 - BOXY = 2 # noqa: N806 # box spacing - # Save the old text values for the override mode, so we can update them if the language is changed coriolis_config.override_text_old_auto = tr.tl('Auto') # LANG: Coriolis normal/beta selection - auto coriolis_config.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal @@ -207,14 +202,14 @@ def _get_target_url(is_beta: bool) -> str: return coriolis_config.normal_url -def shipyard_url(loadout, is_beta) -> str | bool: - """Return a URL for the current ship.""" - # most compact representation - string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') - if not string: - return False - out = io.BytesIO() - with gzip.GzipFile(fileobj=out, mode='w') as f: - f.write(string) - encoded = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D') - return _get_target_url(is_beta) + encoded +# Return a URL for the current ship +def shipyard_url(loadout: Mapping[str, Any], is_beta: bool) -> bool | str: + """ + Construct a URL for ship loadout. + + :param loadout: The ship loadout data. + :param is_beta: Whether the game is in beta. + :return: The constructed URL for the ship loadout. + """ + encoded_data = shipyard_url_common(loadout) + return _get_target_url(is_beta) + encoded_data if encoded_data else False diff --git a/plugins/eddn.py b/plugins/eddn.py index 01d0a46a..9bfab900 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -18,6 +18,7 @@ referenced in this file (or only in any other core plugin), and if so... `build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER INSTALLATION ON WINDOWS. """ +# pylint: disable=import-error from __future__ import annotations import http @@ -47,6 +48,7 @@ from prefs import prefsVersion from ttkHyperlinkLabel import HyperlinkLabel from util import text from l10n import translations as tr +from plugins.common_coreutils import PADX, PADY, BUTTONX, this_format_common logger = get_main_logger() @@ -2145,10 +2147,6 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: :param is_beta: `bool` - True if this is a beta version of the Game. :return: The tkinter frame we created. """ - PADX = 10 # noqa: N806 - BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons - PADY = 1 # noqa: N806 - if prefsVersion.shouldSetDefaults('0.0.0.0', not bool(config.get_int('output'))): output: int = config.OUT_EDDN_SEND_STATION_DATA | config.OUT_EDDN_SEND_NON_STATION # default settings @@ -2349,11 +2347,7 @@ def journal_entry( # noqa: C901, CCR001 this.body_id = state['BodyID'] this.body_type = state['BodyType'] this.coordinates = state['StarPos'] - this.system_address = state['SystemAddress'] - this.system_name = state['SystemName'] - this.station_name = state['StationName'] - this.station_type = state['StationType'] - this.station_marketid = state['MarketID'] + this_format_common(this, state) if event_name == 'docked': # Trigger a send/retry of pending EDDN messages diff --git a/plugins/edsm.py b/plugins/edsm.py index 1d9f8cb8..a2e5004f 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -18,6 +18,7 @@ referenced in this file (or only in any other core plugin), and if so... `build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER INSTALLATION ON WINDOWS. """ +# pylint: disable=import-error from __future__ import annotations import json @@ -40,6 +41,9 @@ from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT from EDMCLogging import get_main_logger from ttkHyperlinkLabel import HyperlinkLabel from l10n import translations as tr +from plugins.common_coreutils import (api_keys_label_common, PADX, PADY, BUTTONX, SEPY, BOXY, STATION_UNDOCKED, + show_pwd_var_common, station_link_common, this_format_common, + cmdr_data_initial_common) # TODO: @@ -118,9 +122,7 @@ class This: this = This() -show_password_var = tk.BooleanVar() -STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 __cleanup = str.maketrans({' ': None, '\n': None}) IMG_KNOWN_B64 = """ R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7r @@ -269,14 +271,6 @@ def plugin_stop() -> None: logger.debug('Done.') -def toggle_password_visibility(): - """Toggle if the API Key is visible or not.""" - if show_password_var.get(): - this.apikey.config(show="") # type: ignore - else: - this.apikey.config(show="*") # type: ignore - - def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame: """ Plugin preferences setup hook. @@ -289,12 +283,6 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr :param is_beta: Whether game beta was detected. :return: An instance of `myNotebook.Frame`. """ - PADX = 10 # noqa: N806 - BUTTONX = 12 # noqa: N806 - PADY = 1 # noqa: N806 - BOXY = 2 # noqa: N806 - SEPY = 10 # noqa: N806 - frame = nb.Frame(parent) frame.columnconfigure(1, weight=1) @@ -349,23 +337,10 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr cur_row += 1 # LANG: EDSM API key label - this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) - this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.apikey = nb.EntryMenu(frame, show="*", width=50) - this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) + api_keys_label_common(this, cur_row, frame) cur_row += 1 - prefs_cmdr_changed(cmdr, is_beta) - - show_password_var.set(False) # Password is initially masked - - show_password_checkbox = nb.Checkbutton( - frame, - text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key - variable=show_password_var, - command=toggle_password_visibility - ) - show_password_checkbox.grid(row=cur_row, columnspan=2, padx=BUTTONX, pady=PADY, sticky=tk.W) + show_pwd_var_common(frame, cur_row, this) return frame @@ -544,11 +519,7 @@ def journal_entry( # noqa: C901, CCR001 this.game_version = state['GameVersion'] this.game_build = state['GameBuild'] - this.system_address = state['SystemAddress'] - this.system_name = state['SystemName'] - this.system_population = state['SystemPopulation'] - this.station_name = state['StationName'] - this.station_marketid = state['MarketID'] + this_format_common(this, state) entry = new_entry @@ -658,7 +629,7 @@ Queueing: {entry!r}''' # Update system data -def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 +def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: """ Process new CAPI data. @@ -668,14 +639,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 """ system = data['lastSystem']['name'] - # Always store initially, even if we're not the *current* system provider. - if not this.station_marketid and data['commander']['docked']: - this.station_marketid = data['lastStarport']['id'] - # Only trust CAPI if these aren't yet set - if not this.system_name: - this.system_name = data['lastSystem']['name'] - if not this.station_name and data['commander']['docked']: - this.station_name = data['lastStarport']['name'] + cmdr_data_initial_common(this, data) # TODO: Fire off the EDSM API call to trigger the callback for the icons @@ -687,13 +651,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # noqa: CCR001 this.system_link.update_idletasks() if config.get_str('station_provider') == 'EDSM': if this.station_link: - if data['commander']['docked'] or this.on_foot and this.station_name: - this.station_link['text'] = this.station_name - elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": - this.station_link['text'] = STATION_UNDOCKED - else: - this.station_link['text'] = '' - + station_link_common(data, this) # Do *NOT* set 'url' here, as it's set to a function that will call # through correctly. We don't want a static string. this.station_link.update_idletasks() diff --git a/plugins/edsy.py b/plugins/edsy.py index a02d3424..b1bdbaf1 100644 --- a/plugins/edsy.py +++ b/plugins/edsy.py @@ -20,11 +20,8 @@ referenced in this file (or only in any other core plugin), and if so... """ from __future__ import annotations -import base64 -import gzip -import io -import json from typing import Any, Mapping +from plugins.common_coreutils import shipyard_url_common # pylint: disable=E0401 def plugin_start3(plugin_dir: str) -> str: @@ -46,17 +43,7 @@ def shipyard_url(loadout: Mapping[str, Any], is_beta: bool) -> bool | str: :param is_beta: Whether the game is in beta. :return: The constructed URL for the ship loadout. """ - # Convert loadout to JSON and gzip compress it - string = json.dumps(loadout, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') - if not string: - return False - - out = io.BytesIO() - with gzip.GzipFile(fileobj=out, mode='w') as f: - f.write(string) - + encoded_data = shipyard_url_common(loadout) # Construct the URL using the appropriate base URL based on is_beta base_url = 'https://edsy.org/beta/#/I=' if is_beta else 'https://edsy.org/#/I=' - encoded_data = base64.urlsafe_b64encode(out.getvalue()).decode().replace('=', '%3D') - - return base_url + encoded_data + return base_url + encoded_data if encoded_data else False diff --git a/plugins/inara.py b/plugins/inara.py index 6c243172..f6a336f9 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -18,6 +18,7 @@ referenced in this file (or only in any other core plugin), and if so... `build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER INSTALLATION ON WINDOWS. """ +# pylint: disable=import-error from __future__ import annotations import json @@ -43,6 +44,8 @@ from EDMCLogging import get_main_logger from monitor import monitor from ttkHyperlinkLabel import HyperlinkLabel from l10n import translations as tr +from plugins.common_coreutils import (api_keys_label_common, PADX, PADY, BUTTONX, SEPY, station_name_setter_common, + show_pwd_var_common, station_link_common, this_format_common) logger = get_main_logger() @@ -115,7 +118,7 @@ class This: self.system_address: str | None = None # type: ignore self.system_population: int | None = None self.station_link: tk.Widget = None # type: ignore - self.station = None + self.station_name = None self.station_marketid = None # Prefs UI @@ -144,15 +147,12 @@ class This: this = This() -show_password_var = tk.BooleanVar() # last time we updated, if unset in config this is 0, which means an instant update LAST_UPDATE_CONF_KEY = 'inara_last_update' EVENT_COLLECT_TIME = 31 # Minimum time to take collecting events before requesting a send WORKER_WAIT_TIME = 35 # Minimum time for worker to wait between sends -STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 - TARGET_URL = 'https://inara.cz/inapi/v1/' DEBUG = 'inara' in debug_senders @@ -187,9 +187,9 @@ def station_url(system_name: str, station_name: str) -> str: if system_name and station_name: return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={system_name}%20[{station_name}]') - if this.system_name and this.station: + if this.system_name and this.station_name: return requests.utils.requote_uri( - f'https://inara.cz/galaxy-station/?search={this.system_name}%20[{this.station}]') + f'https://inara.cz/galaxy-station/?search={this.system_name}%20[{this.station_name}]') if system_name: return system_url(system_name) @@ -233,21 +233,8 @@ def plugin_stop() -> None: logger.debug('Done.') -def toggle_password_visibility(): - """Toggle if the API Key is visible or not.""" - if show_password_var.get(): - this.apikey.config(show="") - else: - this.apikey.config(show="*") - - def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: """Plugin Preferences UI hook.""" - PADX = 10 # noqa: N806 - BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons - PADY = 1 # noqa: N806 # close spacing - BOXY = 2 # noqa: N806 # box spacing - SEPY = 10 # noqa: N806 # seperator line spacing cur_row = 0 frame = nb.Frame(parent) @@ -287,22 +274,10 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: cur_row += 1 # LANG: Inara API key label - this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) # Inara setting - this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.apikey = nb.EntryMenu(frame, show="*", width=50) - this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) + api_keys_label_common(this, cur_row, frame) cur_row += 1 - prefs_cmdr_changed(cmdr, is_beta) - - show_password_var.set(False) # Password is initially masked - show_password_checkbox = nb.Checkbutton( - frame, - text=tr.tl('Show API Key'), # LANG: Text Inara Show API key - variable=show_password_var, - command=toggle_password_visibility, - ) - show_password_checkbox.grid(row=cur_row, columnspan=2, padx=BUTTONX, pady=PADY, sticky=tk.W) + show_pwd_var_common(frame, cur_row, this) return frame @@ -411,15 +386,11 @@ def journal_entry( # noqa: C901, CCR001 # But then we update all the tracking copies before any other checks, # because they're relevant for URL providing even if *sending* isn't # appropriate. - this.on_foot = state['OnFoot'] event_name: str = entry['event'] this.cmdr = cmdr this.FID = state['FID'] this.multicrew = bool(state['Role']) - this.system_name = state['SystemName'] - this.system_address = state['SystemAddress'] - this.station = state['StationName'] - this.station_marketid = state['MarketID'] + this_format_common(this, state) if not monitor.is_live_galaxy(): # Since Update 14 on 2022-11-29 Inara only accepts Live data. @@ -613,7 +584,7 @@ def journal_entry( # noqa: C901, CCR001 elif event_name == 'Undocked': this.undocked = True - this.station = None + this.station_name = None elif event_name == 'SupercruiseEntry': this.undocked = False @@ -1368,14 +1339,7 @@ def journal_entry( # noqa: C901, CCR001 this.system_link.update_idletasks() if config.get_str('station_provider') == 'Inara': - to_set: str = cast(str, this.station) - if not to_set: - if this.system_population is not None and this.system_population > 0: - to_set = STATION_UNDOCKED - else: - to_set = '' - - this.station_link['text'] = to_set + station_name_setter_common(this) # Do *NOT* set 'url' here, as it's set to a function that will call # through correctly. We don't want a static string. this.station_link.update_idletasks() @@ -1383,7 +1347,7 @@ def journal_entry( # noqa: C901, CCR001 return '' # No error -def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later +def cmdr_data(data: CAPIData, is_beta): """CAPI event hook.""" this.cmdr = data['commander']['name'] @@ -1394,8 +1358,8 @@ def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later # Only trust CAPI if these aren't yet set this.system_name = this.system_name if this.system_name else data['lastSystem']['name'] - if not this.station and data['commander']['docked']: - this.station = data['lastStarport']['name'] + if not this.station_name and data['commander']['docked']: + this.station_name = data['lastStarport']['name'] # Override standard URL functions if config.get_str('system_provider') == 'Inara': @@ -1405,14 +1369,7 @@ def cmdr_data(data: CAPIData, is_beta): # noqa: CCR001, reanalyze me later this.system_link.update_idletasks() if config.get_str('station_provider') == 'Inara': - if data['commander']['docked'] or this.on_foot and this.station: - this.station_link['text'] = this.station - - elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": - this.station_link['text'] = STATION_UNDOCKED - - else: - this.station_link['text'] = '' + station_link_common(data, this) # Do *NOT* set 'url' here, as it's set to a function that will call # through correctly. We don't want a static string. diff --git a/plugins/spansh_core.py b/plugins/spansh_core.py index 77106da8..1b7536c5 100644 --- a/plugins/spansh_core.py +++ b/plugins/spansh_core.py @@ -18,14 +18,17 @@ referenced in this file (or only in any other core plugin), and if so... `build.py` TO ENSURE THE FILES ARE ACTUALLY PRESENT IN AN END-USER INSTALLATION ON WINDOWS. """ +# pylint: disable=import-error from __future__ import annotations import tkinter as tk -from typing import Any, cast +from typing import Any import requests from companion import CAPIData from config import appname, config from EDMCLogging import get_main_logger +from plugins.common_coreutils import (station_link_common, this_format_common, + cmdr_data_initial_common, station_name_setter_common) logger = get_main_logger() @@ -47,7 +50,6 @@ class This: this = This() -STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7 def plugin_start3(plugin_dir: str) -> str: @@ -91,12 +93,7 @@ def journal_entry( :param state: `monitor.state` :return: None if no error, else an error string. """ - this.on_foot = state['OnFoot'] - this.system_address = state['SystemAddress'] - this.system_name = state['SystemName'] - this.system_population = state['SystemPopulation'] - this.station_name = state['StationName'] - this.station_marketid = state['MarketID'] + this_format_common(this, state) # Only actually change URLs if we are current provider. if config.get_str('system_provider') == 'spansh': @@ -106,14 +103,7 @@ def journal_entry( this.system_link.update_idletasks() if config.get_str('station_provider') == 'spansh': - to_set: str = cast(str, this.station_name) - if not to_set: - if this.system_population is not None and this.system_population > 0: - to_set = STATION_UNDOCKED - else: - to_set = '' - - this.station_link['text'] = to_set + station_name_setter_common(this) # Do *NOT* set 'url' here, as it's set to a function that will call # through correctly. We don't want a static string. this.station_link.update_idletasks() @@ -129,15 +119,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: :param is_beta: Whether game beta was detected. :return: Optional error string. """ - # Always store initially, even if we're not the *current* system provider. - if not this.station_marketid and data['commander']['docked']: - this.station_marketid = data['lastStarport']['id'] - - # Only trust CAPI if these aren't yet set - if not this.system_name: - this.system_name = data['lastSystem']['name'] - if not this.station_name and data['commander']['docked']: - this.station_name = data['lastStarport']['name'] + cmdr_data_initial_common(this, data) # Override standard URL functions if config.get_str('system_provider') == 'spansh': @@ -146,12 +128,7 @@ def cmdr_data(data: CAPIData, is_beta: bool) -> str | None: # through correctly. We don't want a static string. this.system_link.update_idletasks() if config.get_str('station_provider') == 'spansh': - if data['commander']['docked'] or this.on_foot and this.station_name: - this.station_link['text'] = this.station_name - elif data['lastStarport']['name'] and data['lastStarport']['name'] != "": - this.station_link['text'] = STATION_UNDOCKED - else: - this.station_link['text'] = '' + station_link_common(data, this) # Do *NOT* set 'url' here, as it's set to a function that will call # through correctly. We don't want a static string. this.station_link.update_idletasks() diff --git a/prefs.py b/prefs.py index 436decb1..fb2c7747 100644 --- a/prefs.py +++ b/prefs.py @@ -24,6 +24,7 @@ from l10n import translations as tr from monitor import monitor from theme import theme from ttkHyperlinkLabel import HyperlinkLabel +from common_utils import ensure_on_screen logger = get_main_logger() @@ -187,7 +188,6 @@ if sys.platform == 'win32': import ctypes import winreg from ctypes.wintypes import LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT, BOOL - import win32gui import win32api is_wine = False try: @@ -307,15 +307,7 @@ class PreferencesDialog(tk.Toplevel): self.grab_set() # Ensure fully on-screen - if sys.platform == 'win32' and CalculatePopupWindowPosition: - position = RECT() - win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) - if CalculatePopupWindowPosition( - POINT(parent.winfo_rootx(), parent.winfo_rooty()), - SIZE(position.right - position.left, position.bottom - position.top), # type: ignore - 0x10000, None, position - ): - self.geometry(f"+{position.left}+{position.top}") + ensure_on_screen(self, parent) # Set Log Directory self.logfile_loc = Path(config.app_dir_path / 'logs') @@ -829,7 +821,8 @@ class PreferencesDialog(tk.Toplevel): appearance_frame, # LANG: Appearance - Help/hint text for UI scaling selection text=tr.tl('100 means Default{CR}Restart Required for{CR}changes to take effect!') - ).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row) + ) # E1111 + self.ui_scaling_defaultis.grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.E, row=cur_row) # Transparency slider ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid( @@ -1308,10 +1301,10 @@ class PreferencesDialog(tk.Toplevel): self._destroy() # Send to the Post Config if we updated the update branch or need to restart post_flags = { - 'Update': True if self.curr_update_track != self.update_paths.get() else False, + 'Update': self.curr_update_track != self.update_paths.get(), # Just needs bool not true if else false 'Track': self.update_paths.get(), 'Parent': self, - 'Restart_Req': True if self.req_restart else False + 'Restart_Req': self.req_restart # Sipmle Bool Needed } if self.callback: self.callback(**post_flags) diff --git a/scripts/find_localised_strings.py b/scripts/find_localised_strings.py index 8187f262..85df27be 100644 --- a/scripts/find_localised_strings.py +++ b/scripts/find_localised_strings.py @@ -64,9 +64,19 @@ COMMENT_SAME_LINE_RE = re.compile(r"^.*?(#.*)$") COMMENT_OWN_LINE_RE = re.compile(r"^\s*?(#.*)$") -def extract_comments( # noqa: CCR001 - call: ast.Call, lines: list[str], file: pathlib.Path -) -> str | None: +def _extract_lang_comment(line: str, pattern: re.Pattern, file: pathlib.Path, + lineno: int) -> tuple[str | None, str | None]: + """Attempt to extract a LANG comment from a line using a given regex pattern.""" + match = pattern.match(line) + if match: + comment = match.group(1).strip() + if comment.startswith("# LANG:"): + return comment.replace("# LANG:", "").strip(), None + return None, f"Unknown comment for {file}:{lineno} {line}" + return None, None + + +def extract_comments(call: ast.Call, lines: list[str], file: pathlib.Path) -> str | None: """ Extract comments from source code based on the given call. @@ -86,29 +96,13 @@ def extract_comments( # noqa: CCR001 above_comment: str | None = None current_line = lines[current].strip() current_comment: str | None = None - bad_comment: str | None = None - if above_line is not None: - match = COMMENT_OWN_LINE_RE.match(above_line) - if match: - above_comment = match.group(1).strip() - if not above_comment.startswith("# LANG:"): - bad_comment = f"Unknown comment for {file}:{call.lineno} {above_line}" - above_comment = None - else: - above_comment = above_comment.replace("# LANG:", "").strip() + if above_line: + above_comment, bad_comment = _extract_lang_comment(above_line, COMMENT_OWN_LINE_RE, file, call.lineno) - if current_line is not None: - match = COMMENT_SAME_LINE_RE.match(current_line) - if match: - current_comment = match.group(1).strip() - if not current_comment.startswith("# LANG:"): - bad_comment = f"Unknown comment for {file}:{call.lineno} {current_line}" - current_comment = None - - else: - current_comment = current_comment.replace("# LANG:", "").strip() + if current_line: + current_comment, bad_comment = _extract_lang_comment(current_line, COMMENT_SAME_LINE_RE, file, call.lineno) if current_comment is not None: out = current_comment diff --git a/stats.py b/stats.py index b0ba34b3..4c4b0e3d 100644 --- a/stats.py +++ b/stats.py @@ -20,25 +20,10 @@ from edmc_data import ship_name_map from hotkey import hotkeymgr from l10n import Locale, translations as tr from monitor import monitor +from common_utils import ensure_on_screen logger = EDMCLogging.get_main_logger() -if sys.platform == 'win32': - import ctypes - from ctypes.wintypes import POINT, RECT, SIZE, UINT, BOOL - import win32gui - - try: - CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition - CalculatePopupWindowPosition.argtypes = [ - ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT) - ] - CalculatePopupWindowPosition.restype = BOOL - - except Exception: # Not supported under Wine 4.0 - CalculatePopupWindowPosition = None # type: ignore - - CR_LINES_START = 1 CR_LINES_END = 3 RANK_LINES_START = 3 @@ -418,16 +403,7 @@ class StatsResults(tk.Toplevel): self.grab_set() # Ensure fully on-screen - if sys.platform == 'win32' and CalculatePopupWindowPosition: - position = RECT() - win32gui.GetWindowRect(win32gui.GetParent(self.winfo_id())) - if CalculatePopupWindowPosition( - POINT(parent.winfo_rootx(), parent.winfo_rooty()), - # - is evidently supported on the C side - SIZE(position.right - position.left, position.bottom - position.top), # type: ignore - 0x10000, None, position - ): - self.geometry(f"+{position.left}+{position.top}") + ensure_on_screen(self, parent) def addpage( self, parent, header: list[str] | None = None, align: str | None = None