mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-19 02:17:38 +03:00
Merge pull request #1411 from A-UNDERSCORE-D/sys-plaform-mypy
Ensure that platform guards are recognised by mypy
This commit is contained in:
commit
5e612d56b3
@ -2,3 +2,4 @@
|
||||
follow_imports = skip
|
||||
ignore_missing_imports = True
|
||||
scripts_are_modules = True
|
||||
; platform = darwin
|
||||
|
@ -228,6 +228,7 @@ handy if you want to step through the testing code to be sure of anything.
|
||||
Otherwise, see the [pytest documentation](https://docs.pytest.org/en/stable/contents.html).
|
||||
|
||||
---
|
||||
|
||||
## Debugging network sends
|
||||
|
||||
Rather than risk sending bad data to a remote service, even if only through
|
||||
@ -484,6 +485,20 @@ Please be verbose here, more info about weird choices is always prefered over ma
|
||||
|
||||
Additionally, if your hack is over around 5 lines, please include a `# HACK END` or similar comment to indicate the end of the hack.
|
||||
|
||||
# Use `sys.platform` for platform guards
|
||||
|
||||
`mypy` (and `pylance`) understand platform guards and will show unreachable code / resolve imports correctly
|
||||
for platform specific things. However, this only works if you directly reference `sys.platform`, importantly
|
||||
the following does not work:
|
||||
|
||||
```py
|
||||
from sys import platform
|
||||
if platform == 'darwin':
|
||||
...
|
||||
```
|
||||
|
||||
It **MUST** be `if sys.platform`.
|
||||
|
||||
---
|
||||
|
||||
## Build process
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Entry point for the main GUI application."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
@ -14,7 +15,6 @@ import webbrowser
|
||||
from builtins import object, str
|
||||
from os import chdir, environ
|
||||
from os.path import dirname, join
|
||||
from sys import platform
|
||||
from time import localtime, strftime, time
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
# place for things like config.py reading .gitversion
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Under py2exe sys.path[0] is the executable name
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
chdir(dirname(sys.path[0]))
|
||||
# Allow executable to be invoked from any cwd
|
||||
environ['TCL_LIBRARY'] = join(dirname(sys.path[0]), 'lib', 'tcl')
|
||||
@ -234,7 +234,7 @@ if __name__ == '__main__': # noqa: C901
|
||||
"""Handle any edmc:// auth callback, else foreground existing window."""
|
||||
logger.trace_if('frontier-auth.windows', 'Begin...')
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
|
||||
# If *this* instance hasn't locked, then another already has and we
|
||||
# now need to do the edmc:// checks for auth callback
|
||||
@ -370,8 +370,9 @@ if __name__ == '__main__': # noqa: C901
|
||||
# isort: off
|
||||
if TYPE_CHECKING:
|
||||
from logging import TRACE # type: ignore # noqa: F401 # Needed to update mypy
|
||||
import update
|
||||
from infi.systray import SysTrayIcon
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from infi.systray import SysTrayIcon
|
||||
# isort: on
|
||||
|
||||
def _(x: str) -> str:
|
||||
@ -443,7 +444,7 @@ class AppWindow(object):
|
||||
|
||||
self.prefsdialog = None
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
from infi.systray import SysTrayIcon
|
||||
|
||||
def open_window(systray: 'SysTrayIcon') -> None:
|
||||
@ -456,8 +457,8 @@ class AppWindow(object):
|
||||
|
||||
plug.load_plugins(master)
|
||||
|
||||
if platform != 'darwin':
|
||||
if platform == 'win32':
|
||||
if sys.platform != 'darwin':
|
||||
if sys.platform == 'win32':
|
||||
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
|
||||
|
||||
else:
|
||||
@ -527,7 +528,7 @@ class AppWindow(object):
|
||||
|
||||
# LANG: Update button in main window
|
||||
self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED)
|
||||
self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED)
|
||||
self.theme_button = tk.Label(frame, width=32 if sys.platform == 'darwin' else 28, state=tk.DISABLED)
|
||||
self.status = tk.Label(frame, name='status', anchor=tk.W)
|
||||
|
||||
ui_row = frame.grid_size()[1]
|
||||
@ -540,14 +541,15 @@ class AppWindow(object):
|
||||
theme.button_bind(self.theme_button, self.capi_request_data)
|
||||
|
||||
for child in frame.winfo_children():
|
||||
child.grid_configure(padx=self.PADX, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
||||
child.grid_configure(padx=self.PADX, pady=(
|
||||
sys.platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
||||
|
||||
# The type needs defining for adding the menu entry, but won't be
|
||||
# properly set until later
|
||||
self.updater: update.Updater = None
|
||||
|
||||
self.menubar = tk.Menu()
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Can't handle (de)iconify if topmost is set, so suppress iconify button
|
||||
# http://wiki.tcl.tk/13428 and p15 of
|
||||
# https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf
|
||||
@ -603,7 +605,7 @@ class AppWindow(object):
|
||||
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
|
||||
|
||||
self.menubar.add_cascade(menu=self.help_menu)
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
# Must be added after at least one "real" menu entry
|
||||
self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop')))
|
||||
self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
|
||||
@ -674,11 +676,11 @@ class AppWindow(object):
|
||||
if config.get_str('geometry'):
|
||||
match = re.match(r'\+([\-\d]+)\+([\-\d]+)', config.get_str('geometry'))
|
||||
if match:
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if int(match.group(2)) >= 0:
|
||||
self.w.geometry(config.get_str('geometry'))
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
# Check that the titlebar will be at least partly on screen
|
||||
import ctypes
|
||||
from ctypes.wintypes import POINT
|
||||
@ -776,7 +778,7 @@ class AppWindow(object):
|
||||
self.suit_shown = True
|
||||
|
||||
if not self.suit_shown:
|
||||
if platform != 'win32':
|
||||
if sys.platform != 'win32':
|
||||
pady = 2
|
||||
|
||||
else:
|
||||
@ -826,7 +828,7 @@ class AppWindow(object):
|
||||
self.system_label['text'] = _('System') + ':' # LANG: Label for 'System' line in main UI
|
||||
self.station_label['text'] = _('Station') + ':' # LANG: Label for 'Station' line in main UI
|
||||
self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title on OSX
|
||||
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title on OSX
|
||||
self.menubar.entryconfigure(3, label=_('View')) # LANG: 'View' menu title on OSX
|
||||
@ -873,7 +875,7 @@ class AppWindow(object):
|
||||
|
||||
self.button['state'] = self.theme_button['state'] = tk.DISABLED
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status
|
||||
self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data
|
||||
|
||||
@ -887,7 +889,7 @@ class AppWindow(object):
|
||||
# LANG: Successfully authenticated with the Frontier website
|
||||
self.status['text'] = _('Authentication successful')
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
||||
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
||||
|
||||
@ -1211,7 +1213,7 @@ class AppWindow(object):
|
||||
companion.session.invalidate()
|
||||
self.login()
|
||||
|
||||
except companion.ServerConnectionError as e:
|
||||
except companion.ServerConnectionError as e: # TODO: unreachable (subclass of ServerLagging -- move to above)
|
||||
logger.warning(f'Exception while contacting server: {e}')
|
||||
err = self.status['text'] = str(e)
|
||||
play_bad = True
|
||||
@ -1429,7 +1431,7 @@ class AppWindow(object):
|
||||
companion.session.auth_callback()
|
||||
# LANG: Successfully authenticated with the Frontier website
|
||||
self.status['text'] = _('Authentication successful')
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
|
||||
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
|
||||
|
||||
@ -1570,11 +1572,11 @@ class AppWindow(object):
|
||||
|
||||
# position over parent
|
||||
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if platform != 'darwin' or parent.winfo_rooty() > 0:
|
||||
if sys.platform != 'darwin' or parent.winfo_rooty() > 0:
|
||||
self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}')
|
||||
|
||||
# remove decoration
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
self.resizable(tk.FALSE, tk.FALSE)
|
||||
@ -1651,7 +1653,7 @@ class AppWindow(object):
|
||||
"""
|
||||
default_extension: str = ''
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
default_extension = '.json'
|
||||
|
||||
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
|
||||
@ -1676,7 +1678,7 @@ class AppWindow(object):
|
||||
|
||||
def onexit(self, event=None) -> None:
|
||||
"""Application shutdown procedure."""
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
shutdown_thread = threading.Thread(target=self.systray.shutdown)
|
||||
shutdown_thread.setDaemon(True)
|
||||
shutdown_thread.start()
|
||||
@ -1684,7 +1686,7 @@ class AppWindow(object):
|
||||
config.set_shutdown() # Signal we're in shutdown now.
|
||||
|
||||
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if platform != 'darwin' or self.w.winfo_rooty() > 0:
|
||||
if sys.platform != 'darwin' or self.w.winfo_rooty() > 0:
|
||||
x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267'
|
||||
config.set('geometry', f'+{x}+{y}')
|
||||
|
||||
|
473
config/__init__.py
Normal file
473
config/__init__.py
Normal file
@ -0,0 +1,473 @@
|
||||
"""
|
||||
Code dealing with the configuration of the program.
|
||||
|
||||
Windows uses the Registry to store values in a flat manner.
|
||||
Linux uses a file, but for commonality it's still a flat data structure.
|
||||
macOS uses a 'defaults' object.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
# defined in the order they appear in the file
|
||||
'GITVERSION_FILE',
|
||||
'appname',
|
||||
'applongname',
|
||||
'appcmdname',
|
||||
'copyright',
|
||||
'update_feed',
|
||||
'update_interval',
|
||||
'debug_senders',
|
||||
'trace_on',
|
||||
'capi_pretend_down',
|
||||
'capi_debug_access_token',
|
||||
'logger',
|
||||
'git_shorthash_from_head',
|
||||
'appversion',
|
||||
'user_agent',
|
||||
'appversion_nobuild',
|
||||
'AbstractConfig',
|
||||
'config'
|
||||
]
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Callable, List, Optional, Type, TypeVar, Union
|
||||
|
||||
import semantic_version
|
||||
|
||||
from constants import GITVERSION_FILE, applongname, appname
|
||||
|
||||
# Any of these may be imported by plugins
|
||||
appcmdname = 'EDMC'
|
||||
# appversion **MUST** follow Semantic Versioning rules:
|
||||
# <https://semver.org/#semantic-versioning-specification-semver>
|
||||
# Major.Minor.Patch(-prerelease)(+buildmetadata)
|
||||
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
|
||||
_static_appversion = '5.3.0-beta4'
|
||||
_cached_version: Optional[semantic_version.Version] = None
|
||||
copyright = '© 2015-2019 Jonathan Harris, 2020-2022 EDCD'
|
||||
|
||||
update_feed = 'https://raw.githubusercontent.com/EDCD/EDMarketConnector/releases/edmarketconnector.xml'
|
||||
update_interval = 8*60*60
|
||||
# Providers marked to be in debug mode. Generally this is expected to switch to sending data to a log file
|
||||
debug_senders: List[str] = []
|
||||
# TRACE logging code that should actually be used. Means not spamming it
|
||||
# *all* if only interested in some things.
|
||||
trace_on: List[str] = []
|
||||
|
||||
capi_pretend_down: bool = False
|
||||
capi_debug_access_token: Optional[str] = None
|
||||
# This must be done here in order to avoid an import cycle with EDMCLogging.
|
||||
# Other code should use EDMCLogging.get_main_logger
|
||||
if os.getenv("EDMC_NO_UI"):
|
||||
logger = logging.getLogger(appcmdname)
|
||||
|
||||
else:
|
||||
logger = logging.getLogger(appname)
|
||||
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
###########################################################################
|
||||
def git_shorthash_from_head() -> str:
|
||||
"""
|
||||
Determine short hash for current git HEAD.
|
||||
|
||||
Includes `.DIRTY` if any changes have been made from HEAD
|
||||
|
||||
:return: str - None if we couldn't determine the short hash.
|
||||
"""
|
||||
shorthash: str = None # type: ignore
|
||||
|
||||
try:
|
||||
git_cmd = subprocess.Popen('git rev-parse --short HEAD'.split(),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
out, err = git_cmd.communicate()
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Couldn't run git command for short hash: {e!r}")
|
||||
|
||||
else:
|
||||
shorthash = out.decode().rstrip('\n')
|
||||
if re.match(r'^[0-9a-f]{7,}$', shorthash) is None:
|
||||
logger.error(f"'{shorthash}' doesn't look like a valid git short hash, forcing to None")
|
||||
shorthash = None # type: ignore
|
||||
|
||||
if shorthash is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
result = subprocess.run('git diff --stat HEAD'.split(), capture_output=True)
|
||||
if len(result.stdout) > 0:
|
||||
shorthash += '.DIRTY'
|
||||
|
||||
if len(result.stderr) > 0:
|
||||
logger.warning(f'Data from git on stderr:\n{str(result.stderr)}')
|
||||
|
||||
return shorthash
|
||||
|
||||
|
||||
def appversion() -> semantic_version.Version:
|
||||
"""
|
||||
Determine app version including git short hash if possible.
|
||||
|
||||
:return: The augmented app version.
|
||||
"""
|
||||
global _cached_version
|
||||
if _cached_version is not None:
|
||||
return _cached_version
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running frozen, so we should have a .gitversion file
|
||||
# Yes, .parent because if frozen we're inside library.zip
|
||||
with open(pathlib.Path(sys.path[0]).parent / GITVERSION_FILE, 'r', encoding='utf-8') as gitv:
|
||||
shorthash = gitv.read()
|
||||
|
||||
else:
|
||||
# Running from source
|
||||
shorthash = git_shorthash_from_head()
|
||||
if shorthash is None:
|
||||
shorthash = 'UNKNOWN'
|
||||
|
||||
_cached_version = semantic_version.Version(f'{_static_appversion}+{shorthash}')
|
||||
return _cached_version
|
||||
|
||||
|
||||
user_agent = f'EDCD-{appname}-{appversion()}'
|
||||
|
||||
|
||||
def appversion_nobuild() -> semantic_version.Version:
|
||||
"""
|
||||
Determine app version without *any* build meta data.
|
||||
|
||||
This will not only strip any added git short hash, but also any trailing
|
||||
'+<string>' in _static_appversion.
|
||||
|
||||
:return: App version without any build meta data.
|
||||
"""
|
||||
return appversion().truncate('prerelease')
|
||||
###########################################################################
|
||||
|
||||
|
||||
class AbstractConfig(abc.ABC):
|
||||
"""Abstract root class of all platform specific Config implementations."""
|
||||
|
||||
OUT_MKT_EDDN = 1
|
||||
# OUT_MKT_BPC = 2 # No longer supported
|
||||
OUT_MKT_TD = 4
|
||||
OUT_MKT_CSV = 8
|
||||
OUT_SHIP = 16
|
||||
# OUT_SHIP_EDS = 16 # Replaced by OUT_SHIP
|
||||
# OUT_SYS_FILE = 32 # No longer supported
|
||||
# OUT_STAT = 64 # No longer available
|
||||
# OUT_SHIP_CORIOLIS = 128 # Replaced by OUT_SHIP
|
||||
OUT_STATION_ANY = OUT_MKT_EDDN | OUT_MKT_TD | OUT_MKT_CSV
|
||||
# OUT_SYS_EDSM = 256 # Now a plugin
|
||||
# OUT_SYS_AUTO = 512 # Now always automatic
|
||||
OUT_MKT_MANUAL = 1024
|
||||
OUT_SYS_EDDN = 2048
|
||||
OUT_SYS_DELAY = 4096
|
||||
|
||||
app_dir_path: pathlib.Path
|
||||
plugin_dir_path: pathlib.Path
|
||||
internal_plugin_dir_path: pathlib.Path
|
||||
respath_path: pathlib.Path
|
||||
home_path: pathlib.Path
|
||||
default_journal_dir_path: pathlib.Path
|
||||
|
||||
identifier: str
|
||||
|
||||
__in_shutdown = False # Is the application currently shutting down ?
|
||||
__auth_force_localserver = False # Should we use localhost for auth callback ?
|
||||
__auth_force_edmc_protocol = False # Should we force edmc:// protocol ?
|
||||
__eddn_url = None # Non-default EDDN URL
|
||||
__eddn_tracking_ui = False # Show EDDN tracking UI ?
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.home_path = pathlib.Path.home()
|
||||
|
||||
def set_shutdown(self):
|
||||
"""Set flag denoting we're in the shutdown sequence."""
|
||||
self.__in_shutdown = True
|
||||
|
||||
@property
|
||||
def shutting_down(self) -> bool:
|
||||
"""
|
||||
Determine if we're in the shutdown sequence.
|
||||
|
||||
:return: bool - True if in shutdown sequence.
|
||||
"""
|
||||
return self.__in_shutdown
|
||||
|
||||
def set_auth_force_localserver(self):
|
||||
"""Set flag to force use of localhost web server for Frontier Auth callback."""
|
||||
self.__auth_force_localserver = True
|
||||
|
||||
@property
|
||||
def auth_force_localserver(self) -> bool:
|
||||
"""
|
||||
Determine if use of localhost is forced for Frontier Auth callback.
|
||||
|
||||
:return: bool - True if we should use localhost web server.
|
||||
"""
|
||||
return self.__auth_force_localserver
|
||||
|
||||
def set_auth_force_edmc_protocol(self):
|
||||
"""Set flag to force use of localhost web server for Frontier Auth callback."""
|
||||
self.__auth_force_edmc_protocol = True
|
||||
|
||||
@property
|
||||
def auth_force_edmc_protocol(self) -> bool:
|
||||
"""
|
||||
Determine if use of localhost is forced for Frontier Auth callback.
|
||||
|
||||
:return: bool - True if we should use localhost web server.
|
||||
"""
|
||||
return self.__auth_force_edmc_protocol
|
||||
|
||||
def set_eddn_url(self, eddn_url: str):
|
||||
"""Set the specified eddn URL."""
|
||||
self.__eddn_url = eddn_url
|
||||
|
||||
@property
|
||||
def eddn_url(self) -> Optional[str]:
|
||||
"""
|
||||
Provide the custom EDDN URL.
|
||||
|
||||
:return: str - Custom EDDN URL to use.
|
||||
"""
|
||||
return self.__eddn_url
|
||||
|
||||
def set_eddn_tracking_ui(self):
|
||||
"""Activate EDDN tracking UI."""
|
||||
self.__eddn_tracking_ui = True
|
||||
|
||||
@property
|
||||
def eddn_tracking_ui(self) -> bool:
|
||||
"""
|
||||
Determine if the EDDN tracking UI be shown.
|
||||
|
||||
:return: bool - Should tracking UI be active?
|
||||
"""
|
||||
return self.__eddn_tracking_ui
|
||||
|
||||
@property
|
||||
def app_dir(self) -> str:
|
||||
"""Return a string version of app_dir."""
|
||||
return str(self.app_dir_path)
|
||||
|
||||
@property
|
||||
def plugin_dir(self) -> str:
|
||||
"""Return a string version of plugin_dir."""
|
||||
return str(self.plugin_dir_path)
|
||||
|
||||
@property
|
||||
def internal_plugin_dir(self) -> str:
|
||||
"""Return a string version of internal_plugin_dir."""
|
||||
return str(self.internal_plugin_dir_path)
|
||||
|
||||
@property
|
||||
def respath(self) -> str:
|
||||
"""Return a string version of respath."""
|
||||
return str(self.respath_path)
|
||||
|
||||
@property
|
||||
def home(self) -> str:
|
||||
"""Return a string version of home."""
|
||||
return str(self.home_path)
|
||||
|
||||
@property
|
||||
def default_journal_dir(self) -> str:
|
||||
"""Return a string version of default_journal_dir."""
|
||||
return str(self.default_journal_dir_path)
|
||||
|
||||
@staticmethod
|
||||
def _suppress_call(
|
||||
func: Callable[..., _T], exceptions: Union[Type[BaseException], List[Type[BaseException]]] = Exception,
|
||||
*args: Any, **kwargs: Any
|
||||
) -> Optional[_T]:
|
||||
if exceptions is None:
|
||||
exceptions = [Exception]
|
||||
|
||||
if not isinstance(exceptions, list):
|
||||
exceptions = [exceptions]
|
||||
|
||||
with contextlib.suppress(*exceptions): # type: ignore # it works fine, mypy
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def get(self, key: str, default: Union[list, str, bool, int] = None) -> Union[list, str, bool, int]:
|
||||
"""
|
||||
Return the data for the requested key, or a default.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: The default to return if the key does not exist, defaults to None.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The data or the default.
|
||||
"""
|
||||
warnings.warn(DeprecationWarning('get is Deprecated. use the specific getter for your type'))
|
||||
logger.debug('Attempt to use Deprecated get() method\n' + ''.join(traceback.format_stack()))
|
||||
|
||||
if (l := self._suppress_call(self.get_list, ValueError, key, default=None)) is not None:
|
||||
return l
|
||||
|
||||
elif (s := self._suppress_call(self.get_str, ValueError, key, default=None)) is not None:
|
||||
return s
|
||||
|
||||
elif (b := self._suppress_call(self.get_bool, ValueError, key, default=None)) is not None:
|
||||
return b
|
||||
|
||||
elif (i := self._suppress_call(self.get_int, ValueError, key, default=None)) is not None:
|
||||
return i
|
||||
|
||||
return default # type: ignore
|
||||
|
||||
@abstractmethod
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: Default to return if the key does not exist, defaults to None.
|
||||
:raises ValueError: If an internal error occurs getting or converting a value.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The requested data or the default.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: Default to return if the key does not exist, defaults to None
|
||||
:raises ValueError: If an internal error occurs getting or converting a value
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The requested data or the default
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def getint(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Getint is a Deprecated getter method.
|
||||
|
||||
See get_int for its replacement.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
"""
|
||||
warnings.warn(DeprecationWarning('getint is Deprecated. Use get_int instead'))
|
||||
logger.debug('Attempt to use Deprecated getint() method\n' + ''.join(traceback.format_stack()))
|
||||
|
||||
return self.get_int(key, default=default)
|
||||
|
||||
@abstractmethod
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
For legacy reasons, the default is 0 and not None.
|
||||
|
||||
:param key: The key data is being requested for.
|
||||
:param default: Default to return if the key does not exist, defaults to 0.
|
||||
:raises ValueError: If the internal representation of this key cannot be converted to an int.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
:return: The requested data or the default.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
:param key: The key to set the value on.
|
||||
:param val: The value to set the key's data to.
|
||||
:raises ValueError: On an invalid type.
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
:param key: The key to delete.
|
||||
:param suppress: bool - Whether to suppress any errors. Useful in case
|
||||
code to migrate settings is blindly removing an old key.
|
||||
:raises OSError: On Windows, if a registry error occurs.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration.
|
||||
|
||||
:raises OSError: On Windows, if a Registry error occurs.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""Close this config and release any associated resources."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_password(self, account: str) -> None:
|
||||
"""Legacy password retrieval."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning)
|
||||
|
||||
def set_password(self, account: str, password: str) -> None:
|
||||
"""Legacy password setting."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning)
|
||||
|
||||
def delete_password(self, account: str) -> None:
|
||||
"""Legacy password deletion."""
|
||||
warnings.warn("password subsystem is no longer supported", DeprecationWarning)
|
||||
|
||||
|
||||
def get_config(*args, **kwargs) -> AbstractConfig:
|
||||
"""
|
||||
Get the appropriate config class for the current platform.
|
||||
|
||||
:param args: Args to be passed through to implementation.
|
||||
:param kwargs: Args to be passed through to implementation.
|
||||
:return: Instance of the implementation.
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
from .darwin import MacConfig
|
||||
return MacConfig(*args, **kwargs)
|
||||
|
||||
elif sys.platform == "win32":
|
||||
from .windows import WinConfig
|
||||
return WinConfig(*args, **kwargs)
|
||||
|
||||
elif sys.platform == "linux":
|
||||
from .linux import LinuxConfig
|
||||
return LinuxConfig(*args, **kwargs)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unknown platform: {sys.platform=}')
|
||||
|
||||
|
||||
config = get_config()
|
184
config/darwin.py
Normal file
184
config/darwin.py
Normal file
@ -0,0 +1,184 @@
|
||||
import pathlib
|
||||
import sys
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from Foundation import ( # type: ignore
|
||||
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults,
|
||||
NSUserDomainMask
|
||||
)
|
||||
|
||||
from config import AbstractConfig, appname, logger
|
||||
|
||||
assert sys.platform == 'darwin'
|
||||
|
||||
|
||||
class MacConfig(AbstractConfig):
|
||||
"""MacConfig is the implementation of AbstractConfig for Darwin based OSes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
support_path = pathlib.Path(
|
||||
NSSearchPathForDirectoriesInDomains(
|
||||
NSApplicationSupportDirectory, NSUserDomainMask, True
|
||||
)[0]
|
||||
)
|
||||
|
||||
self.app_dir_path = support_path / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
# Bundle IDs identify a singled app though out a system
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
exe_dir = pathlib.Path(sys.executable).parent
|
||||
self.internal_plugin_dir_path = exe_dir.parent / 'Library' / 'plugins'
|
||||
self.respath_path = exe_dir.parent / 'Resources'
|
||||
self.identifier = NSBundle.mainBundle().bundleIdentifier()
|
||||
|
||||
else:
|
||||
file_dir = pathlib.Path(__file__).parent.parent
|
||||
self.internal_plugin_dir_path = file_dir / 'plugins'
|
||||
self.respath_path = file_dir
|
||||
|
||||
self.identifier = f'uk.org.marginal.{appname.lower()}'
|
||||
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier
|
||||
|
||||
self.default_journal_dir_path = support_path / 'Frontier Developments' / 'Elite Dangerous'
|
||||
self._defaults: Any = NSUserDefaults.standardUserDefaults()
|
||||
self._settings: Dict[str, Union[int, str, list]] = dict(
|
||||
self._defaults.persistentDomainForName_(self.identifier) or {}
|
||||
) # make writeable
|
||||
|
||||
if (out_dir := self.get_str('out_dir')) is None or not pathlib.Path(out_dir).exists():
|
||||
self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0])
|
||||
|
||||
def __raw_get(self, key: str) -> Union[None, list, str, int]:
|
||||
"""
|
||||
Retrieve the raw data for the given key.
|
||||
|
||||
:param str: str - The key data is being requested for.
|
||||
:return: The requested data.
|
||||
"""
|
||||
res = self._settings.get(key)
|
||||
# On MacOS Catalina, with python.org python 3.9.2 any 'list'
|
||||
# has type __NSCFArray so a simple `isinstance(res, list)` is
|
||||
# False. So, check it's not-None, and not the other types.
|
||||
#
|
||||
# If we can find where to import the definition of NSCFArray
|
||||
# then we could possibly test against that.
|
||||
if res is not None and not isinstance(res, str) and not isinstance(res, int):
|
||||
return list(res)
|
||||
|
||||
return res
|
||||
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_str`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
if not isinstance(res, str):
|
||||
raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}')
|
||||
|
||||
return res
|
||||
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, list):
|
||||
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
|
||||
|
||||
return res
|
||||
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_int`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default
|
||||
|
||||
elif not isinstance(res, (str, int)):
|
||||
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
|
||||
|
||||
try:
|
||||
return int(res)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f'__raw_get returned {res!r} which cannot be parsed to an int: {e}')
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_bool`.
|
||||
"""
|
||||
res = self.__raw_get(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, bool):
|
||||
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
|
||||
|
||||
return res
|
||||
|
||||
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
Implements :meth:`AbstractConfig.set`.
|
||||
"""
|
||||
if self._settings is None:
|
||||
raise ValueError('attempt to use a closed _settings')
|
||||
|
||||
if not isinstance(val, (bool, str, int, list)):
|
||||
raise ValueError(f'Unexpected type for value {type(val)=}')
|
||||
|
||||
self._settings[key] = val
|
||||
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.delete`.
|
||||
"""
|
||||
try:
|
||||
del self._settings[key]
|
||||
|
||||
except Exception:
|
||||
if suppress:
|
||||
pass
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration.
|
||||
|
||||
Implements :meth:`AbstractConfig.save`.
|
||||
"""
|
||||
self._defaults.setPersistentDomain_forName_(self._settings, self.identifier)
|
||||
self._defaults.synchronize()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close this config and release any associated resources.
|
||||
|
||||
Implements :meth:`AbstractConfig.close`.
|
||||
"""
|
||||
self.save()
|
||||
self._defaults = None
|
245
config/linux.py
Normal file
245
config/linux.py
Normal file
@ -0,0 +1,245 @@
|
||||
"""Linux config implementation."""
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
from config import AbstractConfig, appname, logger
|
||||
|
||||
assert sys.platform == 'linux'
|
||||
|
||||
|
||||
class LinuxConfig(AbstractConfig):
|
||||
"""Linux implementation of AbstractConfig."""
|
||||
|
||||
SECTION = 'config'
|
||||
# TODO: I dislike this, would rather use a sane config file format. But here we are.
|
||||
__unescape_lut = {'\\': '\\', 'n': '\n', ';': ';', 'r': '\r', '#': '#'}
|
||||
__escape_lut = {'\\': '\\', '\n': 'n', ';': ';', '\r': 'r'}
|
||||
|
||||
def __init__(self, filename: Optional[str] = None) -> None:
|
||||
super().__init__()
|
||||
# http://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
xdg_data_home = pathlib.Path(os.getenv('XDG_DATA_HOME', default='~/.local/share')).expanduser()
|
||||
self.app_dir_path = xdg_data_home / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
self.default_journal_dir_path = None # type: ignore
|
||||
self.identifier = f'uk.org.marginal.{appname.lower()}' # TODO: Unused?
|
||||
|
||||
config_home = pathlib.Path(os.getenv('XDG_CONFIG_HOME', default='~/.config')).expanduser()
|
||||
|
||||
self.filename = config_home / appname / f'{appname}.ini'
|
||||
if filename is not None:
|
||||
self.filename = pathlib.Path(filename)
|
||||
|
||||
self.filename.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
self.config: Optional[ConfigParser] = ConfigParser(comment_prefixes=('#',), interpolation=None)
|
||||
self.config.read(self.filename) # read() ignores files that dont exist
|
||||
|
||||
# Ensure that our section exists. This is here because configparser will happily create files for us, but it
|
||||
# does not magically create sections
|
||||
try:
|
||||
self.config[self.SECTION].get("this_does_not_exist", fallback=None)
|
||||
except KeyError:
|
||||
logger.info("Config section not found. Backing up existing file (if any) and readding a section header")
|
||||
if self.filename.exists():
|
||||
(self.filename.parent / f'{appname}.ini.backup').write_bytes(self.filename.read_bytes())
|
||||
|
||||
self.config.add_section(self.SECTION)
|
||||
|
||||
if (outdir := self.get_str('outdir')) is None or not pathlib.Path(outdir).is_dir():
|
||||
self.set('outdir', self.home)
|
||||
|
||||
def __escape(self, s: str) -> str:
|
||||
"""
|
||||
Escape a string using self.__escape_lut.
|
||||
|
||||
This does NOT support multi-character escapes.
|
||||
|
||||
:param s: str - String to be escaped.
|
||||
:return: str - The escaped string.
|
||||
"""
|
||||
out = ""
|
||||
for c in s:
|
||||
if c not in self.__escape_lut:
|
||||
out += c
|
||||
continue
|
||||
|
||||
out += '\\' + self.__escape_lut[c]
|
||||
|
||||
return out
|
||||
|
||||
def __unescape(self, s: str) -> str:
|
||||
"""
|
||||
Unescape a string.
|
||||
|
||||
:param s: str - The string to unescape.
|
||||
:return: str - The unescaped string.
|
||||
"""
|
||||
out: List[str] = []
|
||||
i = 0
|
||||
while i < len(s):
|
||||
c = s[i]
|
||||
if c != '\\':
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# We have a backslash, check what its escaping
|
||||
if i == len(s)-1:
|
||||
raise ValueError('Escaped string has unescaped trailer')
|
||||
|
||||
unescaped = self.__unescape_lut.get(s[i+1])
|
||||
if unescaped is None:
|
||||
raise ValueError(f'Unknown escape: \\ {s[i+1]}')
|
||||
|
||||
out.append(unescaped)
|
||||
i += 2
|
||||
|
||||
return "".join(out)
|
||||
|
||||
def __raw_get(self, key: str) -> Optional[str]:
|
||||
"""
|
||||
Get a raw data value from the config file.
|
||||
|
||||
:param key: str - The key data is being requested for.
|
||||
:return: str - The raw data, if found.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('Attempt to use a closed config')
|
||||
|
||||
return self.config[self.SECTION].get(key)
|
||||
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_str`.
|
||||
"""
|
||||
data = self.__raw_get(key)
|
||||
if data is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
if '\n' in data:
|
||||
raise ValueError('asked for string, got list')
|
||||
|
||||
return self.__unescape(data)
|
||||
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
data = self.__raw_get(key)
|
||||
|
||||
if data is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
split = data.split('\n')
|
||||
if split[-1] != ';':
|
||||
raise ValueError('Encoded list does not have trailer sentinel')
|
||||
|
||||
return list(map(self.__unescape, split[:-1]))
|
||||
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_int`.
|
||||
"""
|
||||
data = self.__raw_get(key)
|
||||
|
||||
if data is None:
|
||||
return default
|
||||
|
||||
try:
|
||||
return int(data)
|
||||
|
||||
except ValueError as e:
|
||||
raise ValueError(f'requested {key=} as int cannot be converted to int') from e
|
||||
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_bool`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
data = self.__raw_get(key)
|
||||
if data is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
return bool(int(data))
|
||||
|
||||
def set(self, key: str, val: Union[int, str, List[str]]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
Implements :meth:`AbstractConfig.set`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
to_set: Optional[str] = None
|
||||
if isinstance(val, bool):
|
||||
to_set = str(int(val))
|
||||
|
||||
elif isinstance(val, str):
|
||||
to_set = self.__escape(val)
|
||||
|
||||
elif isinstance(val, int):
|
||||
to_set = str(val)
|
||||
|
||||
elif isinstance(val, list):
|
||||
to_set = '\n'.join([self.__escape(s) for s in val] + [';'])
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unexpected type for value {type(val)=}')
|
||||
|
||||
self.config.set(self.SECTION, key, to_set)
|
||||
self.save()
|
||||
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.delete`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
self.config.remove_option(self.SECTION, key)
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration.
|
||||
|
||||
Implements :meth:`AbstractConfig.save`.
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError('attempt to use a closed config')
|
||||
|
||||
with open(self.filename, 'w', encoding='utf-8') as f:
|
||||
self.config.write(f)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close this config and release any associated resources.
|
||||
|
||||
Implements :meth:`AbstractConfig.close`.
|
||||
"""
|
||||
self.save()
|
||||
self.config = None
|
259
config/windows.py
Normal file
259
config/windows.py
Normal file
@ -0,0 +1,259 @@
|
||||
"""Windows config implementation."""
|
||||
|
||||
# spell-checker: words folderid deps hkey edcd
|
||||
import ctypes
|
||||
import functools
|
||||
import pathlib
|
||||
import sys
|
||||
import uuid
|
||||
import winreg
|
||||
from ctypes.wintypes import DWORD, HANDLE
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from config import AbstractConfig, applongname, appname, logger, update_interval
|
||||
|
||||
assert sys.platform == 'win32'
|
||||
|
||||
REG_RESERVED_ALWAYS_ZERO = 0
|
||||
|
||||
# This is the only way to do this from python without external deps (which do this anyway).
|
||||
FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}')
|
||||
FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}')
|
||||
FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}')
|
||||
FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}')
|
||||
|
||||
SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath
|
||||
SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)]
|
||||
|
||||
CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree
|
||||
CoTaskMemFree.argtypes = [ctypes.c_void_p]
|
||||
|
||||
|
||||
def known_folder_path(guid: uuid.UUID) -> Optional[str]:
|
||||
"""Look up a Windows GUID to actual folder path name."""
|
||||
buf = ctypes.c_wchar_p()
|
||||
if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)):
|
||||
return None
|
||||
retval = buf.value # copy data
|
||||
CoTaskMemFree(buf) # and free original
|
||||
return retval
|
||||
|
||||
|
||||
class WinConfig(AbstractConfig):
|
||||
"""Implementation of AbstractConfig for Windows."""
|
||||
|
||||
def __init__(self, do_winsparkle=True) -> None:
|
||||
self.app_dir_path = pathlib.Path(str(known_folder_path(FOLDERID_LocalAppData))) / appname
|
||||
self.app_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
self.plugin_dir_path = self.app_dir_path / 'plugins'
|
||||
self.plugin_dir_path.mkdir(exist_ok=True)
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
self.respath_path = pathlib.Path(sys.executable).parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
|
||||
else:
|
||||
self.respath_path = pathlib.Path(__file__).parent.parent
|
||||
self.internal_plugin_dir_path = self.respath_path / 'plugins'
|
||||
|
||||
self.home_path = pathlib.Path.home()
|
||||
|
||||
journal_dir_str = known_folder_path(FOLDERID_SavedGames)
|
||||
journaldir = pathlib.Path(journal_dir_str) if journal_dir_str is not None else None
|
||||
self.default_journal_dir_path = None # type: ignore
|
||||
if journaldir is not None:
|
||||
self.default_journal_dir_path = journaldir / 'Frontier Developments' / 'Elite Dangerous'
|
||||
|
||||
create_key_defaults = functools.partial(
|
||||
winreg.CreateKeyEx,
|
||||
key=winreg.HKEY_CURRENT_USER,
|
||||
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
|
||||
)
|
||||
|
||||
try:
|
||||
self.__reg_handle: winreg.HKEYType = create_key_defaults(
|
||||
sub_key=r'Software\Marginal\EDMarketConnector'
|
||||
)
|
||||
if do_winsparkle:
|
||||
self.__setup_winsparkle()
|
||||
|
||||
except OSError:
|
||||
logger.exception('could not create required registry keys')
|
||||
raise
|
||||
|
||||
self.identifier = applongname
|
||||
if (outdir_str := self.get_str('outdir')) is None or not pathlib.Path(outdir_str).is_dir():
|
||||
docs = known_folder_path(FOLDERID_Documents)
|
||||
self.set('outdir', docs if docs is not None else self.home)
|
||||
|
||||
def __setup_winsparkle(self):
|
||||
"""Ensure the necessary Registry keys for WinSparkle are present."""
|
||||
create_key_defaults = functools.partial(
|
||||
winreg.CreateKeyEx,
|
||||
key=winreg.HKEY_CURRENT_USER,
|
||||
access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY,
|
||||
)
|
||||
try:
|
||||
edcd_handle: winreg.HKEYType = create_key_defaults(sub_key=r'Software\EDCD\EDMarketConnector')
|
||||
winsparkle_reg: winreg.HKEYType = winreg.CreateKeyEx(
|
||||
edcd_handle, sub_key='WinSparkle', access=winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY
|
||||
)
|
||||
|
||||
except OSError:
|
||||
logger.exception('could not open WinSparkle handle')
|
||||
raise
|
||||
|
||||
# set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings
|
||||
winreg.SetValueEx(
|
||||
winsparkle_reg, 'UpdateInterval', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, str(update_interval)
|
||||
)
|
||||
|
||||
try:
|
||||
winreg.QueryValueEx(winsparkle_reg, 'CheckForUpdates')
|
||||
|
||||
except FileNotFoundError:
|
||||
# Key doesn't exist, set it to a default
|
||||
winreg.SetValueEx(winsparkle_reg, 'CheckForUpdates', REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, '1')
|
||||
|
||||
winsparkle_reg.Close()
|
||||
edcd_handle.Close()
|
||||
|
||||
def __get_regentry(self, key: str) -> Union[None, list, str, int]:
|
||||
"""Access the Registry for the raw entry."""
|
||||
try:
|
||||
value, _type = winreg.QueryValueEx(self.__reg_handle, key)
|
||||
except FileNotFoundError:
|
||||
# Key doesn't exist
|
||||
return None
|
||||
|
||||
# The type returned is actually as we'd expect for each of these. The casts are here for type checkers and
|
||||
# For programmers who want to actually know what is going on
|
||||
if _type == winreg.REG_SZ:
|
||||
return str(value)
|
||||
|
||||
elif _type == winreg.REG_DWORD:
|
||||
return int(value)
|
||||
|
||||
elif _type == winreg.REG_MULTI_SZ:
|
||||
return list(value)
|
||||
|
||||
else:
|
||||
logger.warning(f'registry key {key=} returned unknown type {_type=} {value=}')
|
||||
return None
|
||||
|
||||
def get_str(self, key: str, *, default: str = None) -> str:
|
||||
"""
|
||||
Return the string referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_str`.
|
||||
"""
|
||||
res = self.__get_regentry(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, str):
|
||||
raise ValueError(f'Data from registry is not a string: {type(res)=} {res=}')
|
||||
|
||||
return res
|
||||
|
||||
def get_list(self, key: str, *, default: list = None) -> list:
|
||||
"""
|
||||
Return the list referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_list`.
|
||||
"""
|
||||
res = self.__get_regentry(key)
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
elif not isinstance(res, list):
|
||||
raise ValueError(f'Data from registry is not a list: {type(res)=} {res}')
|
||||
|
||||
return res
|
||||
|
||||
def get_int(self, key: str, *, default: int = 0) -> int:
|
||||
"""
|
||||
Return the int referred to by key if it exists in the config.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_int`.
|
||||
"""
|
||||
res = self.__get_regentry(key)
|
||||
if res is None:
|
||||
return default
|
||||
|
||||
if not isinstance(res, int):
|
||||
raise ValueError(f'Data from registry is not an int: {type(res)=} {res}')
|
||||
|
||||
return res
|
||||
|
||||
def get_bool(self, key: str, *, default: bool = None) -> bool:
|
||||
"""
|
||||
Return the bool referred to by the given key if it exists, or the default.
|
||||
|
||||
Implements :meth:`AbstractConfig.get_bool`.
|
||||
"""
|
||||
res = self.get_int(key, default=default) # type: ignore
|
||||
if res is None:
|
||||
return default # type: ignore # Yes it could be None, but we're _assuming_ that people gave us a default
|
||||
|
||||
return bool(res)
|
||||
|
||||
def set(self, key: str, val: Union[int, str, List[str], bool]) -> None:
|
||||
"""
|
||||
Set the given key's data to the given value.
|
||||
|
||||
Implements :meth:`AbstractConfig.set`.
|
||||
"""
|
||||
reg_type = None
|
||||
if isinstance(val, str):
|
||||
reg_type = winreg.REG_SZ
|
||||
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, winreg.REG_SZ, val)
|
||||
|
||||
elif isinstance(val, int): # The original code checked for numbers.Integral, I dont think that is needed.
|
||||
reg_type = winreg.REG_DWORD
|
||||
|
||||
elif isinstance(val, list):
|
||||
reg_type = winreg.REG_MULTI_SZ
|
||||
|
||||
elif isinstance(val, bool):
|
||||
reg_type = winreg.REG_DWORD
|
||||
val = int(val)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unexpected type for value {type(val)=}')
|
||||
|
||||
# Its complaining about the list, it works, tested on windows, ignored.
|
||||
winreg.SetValueEx(self.__reg_handle, key, REG_RESERVED_ALWAYS_ZERO, reg_type, val) # type: ignore
|
||||
|
||||
def delete(self, key: str, *, suppress=False) -> None:
|
||||
"""
|
||||
Delete the given key from the config.
|
||||
|
||||
'key' is relative to the base Registry path we use.
|
||||
|
||||
Implements :meth:`AbstractConfig.delete`.
|
||||
"""
|
||||
try:
|
||||
winreg.DeleteValue(self.__reg_handle, key)
|
||||
except OSError:
|
||||
if suppress:
|
||||
return
|
||||
|
||||
raise
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the configuration.
|
||||
|
||||
Not required for WinConfig as Registry keys are flushed on write.
|
||||
"""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close this config and release any associated resources.
|
||||
|
||||
Implements :meth:`AbstractConfig.close`.
|
||||
"""
|
||||
self.__reg_handle.Close()
|
15
dashboard.py
15
dashboard.py
@ -2,11 +2,11 @@
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
import tkinter as tk
|
||||
from calendar import timegm
|
||||
from os.path import getsize, isdir, isfile
|
||||
from sys import platform
|
||||
from typing import Any, Dict
|
||||
|
||||
from config import config
|
||||
@ -14,11 +14,11 @@ from EDMCLogging import get_main_logger
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
@ -71,26 +71,25 @@ class Dashboard(FileSystemEventHandler):
|
||||
# File system events are unreliable/non-existent over network drives on Linux.
|
||||
# We can't easily tell whether a path points to a network drive, so assume
|
||||
# any non-standard logdir might be on a network drive and poll instead.
|
||||
polling = platform != 'win32'
|
||||
if not polling and not self.observer:
|
||||
if not (sys.platform != 'win32') and not self.observer:
|
||||
logger.debug('Setting up observer...')
|
||||
self.observer = Observer()
|
||||
self.observer.daemon = True
|
||||
self.observer.start()
|
||||
logger.debug('Done')
|
||||
|
||||
elif polling and self.observer:
|
||||
elif (sys.platform != 'win32') and self.observer:
|
||||
logger.debug('Using polling, stopping observer...')
|
||||
self.observer.stop()
|
||||
self.observer = None # type: ignore
|
||||
logger.debug('Done')
|
||||
|
||||
if not self.observed and not polling:
|
||||
if not self.observed and not (sys.platform != 'win32'):
|
||||
logger.debug('Starting observer...')
|
||||
self.observed = self.observer.schedule(self, self.currentdir)
|
||||
logger.debug('Done')
|
||||
|
||||
logger.info(f'{polling and "Polling" or "Monitoring"} Dashboard "{self.currentdir}"')
|
||||
logger.info(f'{(sys.platform != "win32") and "Polling" or "Monitoring"} Dashboard "{self.currentdir}"')
|
||||
|
||||
# Even if we're not intending to poll, poll at least once to process pre-existing
|
||||
# data and to check whether the watchdog thread has crashed due to events not
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Implements locking of Journal directory."""
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from enum import Enum
|
||||
from os import getpid as os_getpid
|
||||
from sys import platform
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
@ -94,7 +94,7 @@ class JournalLock:
|
||||
|
||||
:return: LockResult - See the class Enum definition
|
||||
"""
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
logger.trace_if('journal-lock', 'win32, using msvcrt')
|
||||
# win32 doesn't have fcntl, so we have to use msvcrt
|
||||
import msvcrt
|
||||
@ -143,7 +143,7 @@ class JournalLock:
|
||||
return True # We weren't locked, and still aren't
|
||||
|
||||
unlocked = False
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
logger.trace_if('journal-lock', 'win32, using msvcrt')
|
||||
# win32 doesn't have fcntl, so we have to use msvcrt
|
||||
import msvcrt
|
||||
@ -206,10 +206,10 @@ class JournalLock:
|
||||
self.title(_('Journal directory already locked'))
|
||||
|
||||
# remove decoration
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
elif platform == 'darwin':
|
||||
elif sys.platform == 'darwin':
|
||||
# http://wiki.tcl.tk/13428
|
||||
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
|
||||
|
||||
|
21
l10n.py
21
l10n.py
@ -12,7 +12,6 @@ import warnings
|
||||
from collections import OrderedDict
|
||||
from contextlib import suppress
|
||||
from os.path import basename, dirname, isdir, isfile, join
|
||||
from sys import platform
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional, Set, TextIO, Union, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -37,12 +36,12 @@ LANGUAGE_ID = '!Language'
|
||||
LOCALISATION_DIR = 'L10n'
|
||||
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from Foundation import ( # type: ignore # exists on Darwin
|
||||
NSLocale, NSNumberFormatter, NSNumberFormatterDecimalStyle
|
||||
)
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL, DWORD, LPCVOID, LPCWSTR, LPWSTR
|
||||
if TYPE_CHECKING:
|
||||
@ -176,7 +175,7 @@ class _Translations:
|
||||
def available(self) -> Set[str]:
|
||||
"""Return a list of available language codes."""
|
||||
path = self.respath()
|
||||
if getattr(sys, 'frozen', False) and platform == 'darwin':
|
||||
if getattr(sys, 'frozen', False) and sys.platform == 'darwin':
|
||||
available = {
|
||||
x[:-len('.lproj')] for x in os.listdir(path)
|
||||
if x.endswith('.lproj') and isfile(join(x, 'Localizable.strings'))
|
||||
@ -204,7 +203,7 @@ class _Translations:
|
||||
def respath(self) -> pathlib.Path:
|
||||
"""Path to localisation files."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
return (pathlib.Path(sys.executable).parents[0] / os.pardir / 'Resources').resolve()
|
||||
|
||||
return pathlib.Path(dirname(sys.executable)) / LOCALISATION_DIR
|
||||
@ -233,7 +232,7 @@ class _Translations:
|
||||
except OSError:
|
||||
logger.exception(f'could not open {f}')
|
||||
|
||||
elif getattr(sys, 'frozen', False) and platform == 'darwin':
|
||||
elif getattr(sys, 'frozen', False) and sys.platform == 'darwin':
|
||||
return (self.respath() / f'{lang}.lproj' / 'Localizable.strings').open('r', encoding='utf-16')
|
||||
|
||||
return (self.respath() / f'{lang}.strings').open('r', encoding='utf-8')
|
||||
@ -243,7 +242,7 @@ class _Locale:
|
||||
"""Locale holds a few utility methods to convert data to and from localized versions."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.int_formatter = NSNumberFormatter.alloc().init()
|
||||
self.int_formatter.setNumberStyle_(NSNumberFormatterDecimalStyle)
|
||||
self.float_formatter = NSNumberFormatter.alloc().init()
|
||||
@ -276,7 +275,7 @@ class _Locale:
|
||||
if decimals == 0 and not isinstance(number, numbers.Integral):
|
||||
number = int(round(number))
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
if not decimals and isinstance(number, numbers.Integral):
|
||||
return self.int_formatter.stringFromNumber_(number)
|
||||
|
||||
@ -298,7 +297,7 @@ class _Locale:
|
||||
:param string: The string to convert
|
||||
:return: None if the string cannot be parsed, otherwise an int or float dependant on input data.
|
||||
"""
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
return self.float_formatter.numberFromString_(string)
|
||||
|
||||
with suppress(ValueError):
|
||||
@ -321,10 +320,10 @@ class _Locale:
|
||||
:return: The preferred language list
|
||||
"""
|
||||
languages: Iterable[str]
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
languages = NSLocale.preferredLanguages()
|
||||
|
||||
elif platform != 'win32':
|
||||
elif sys.platform != 'win32':
|
||||
# POSIX
|
||||
lang = locale.getlocale()[0]
|
||||
languages = lang and [lang.replace('_', '-')] or []
|
||||
|
34
monitor.py
34
monitor.py
@ -1,15 +1,18 @@
|
||||
"""Monitor for new Journal files and contents of latest."""
|
||||
# v [sic]
|
||||
# spell-checker: words onfoot unforseen relog fsdjump suitloadoutid slotid suitid loadoutid fauto Intimidator
|
||||
# spell-checker: words joinacrew quitacrew sellshiponrebuy newbal navroute npccrewpaidwage sauto
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from calendar import timegm
|
||||
from collections import OrderedDict, defaultdict
|
||||
from os import SEEK_END, SEEK_SET, listdir
|
||||
from os.path import basename, expanduser, isdir, join
|
||||
from sys import platform
|
||||
from time import gmtime, localtime, mktime, sleep, strftime, strptime, time
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, List, MutableMapping, Optional
|
||||
from typing import OrderedDict as OrderedDictT
|
||||
@ -33,7 +36,7 @@ if TYPE_CHECKING:
|
||||
def _(x: str) -> str:
|
||||
return x
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from fcntl import fcntl
|
||||
|
||||
from AppKit import NSWorkspace
|
||||
@ -41,7 +44,7 @@ if platform == 'darwin':
|
||||
from watchdog.observers import Observer
|
||||
F_GLOBAL_NOCACHE = 55
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR
|
||||
|
||||
@ -62,6 +65,10 @@ elif platform == 'win32':
|
||||
else:
|
||||
# Linux's inotify doesn't work over CIFS or NFS, so poll
|
||||
FileSystemEventHandler = object # dummy
|
||||
if TYPE_CHECKING:
|
||||
# this isn't ever used, but this will make type checking happy
|
||||
from watchdog.events import FileCreatedEvent
|
||||
from watchdog.observers import Observer
|
||||
|
||||
|
||||
# Journal handler
|
||||
@ -225,7 +232,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
# File system events are unreliable/non-existent over network drives on Linux.
|
||||
# We can't easily tell whether a path points to a network drive, so assume
|
||||
# any non-standard logdir might be on a network drive and poll instead.
|
||||
polling = bool(config.get_str('journaldir')) and platform != 'win32'
|
||||
polling = bool(config.get_str('journaldir')) and sys.platform != 'win32'
|
||||
if not polling and not self.observer:
|
||||
logger.debug('Not polling, no observer, starting an observer...')
|
||||
self.observer = Observer()
|
||||
@ -282,6 +289,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
if self.observed:
|
||||
logger.debug('self.observed: Calling unschedule_all()')
|
||||
self.observed = None
|
||||
assert self.observer is not None, 'Observer was none but it is in use?'
|
||||
self.observer.unschedule_all()
|
||||
logger.debug('Done')
|
||||
|
||||
@ -341,7 +349,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
logfile = self.logfile
|
||||
if logfile:
|
||||
loghandle: BinaryIO = open(logfile, 'rb', 0) # unbuffered
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB
|
||||
|
||||
self.catching_up = True
|
||||
@ -394,9 +402,13 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
self.event_queue.put(None)
|
||||
self.live = False
|
||||
|
||||
emitter = None
|
||||
# Watchdog thread -- there is a way to get this by using self.observer.emitters and checking for an attribute:
|
||||
# watch, but that may have unforseen differences in behaviour.
|
||||
emitter = self.observed and self.observer._emitter_for_watch[self.observed] # Note: Uses undocumented attribute
|
||||
if self.observed:
|
||||
assert self.observer is not None, 'self.observer is None but also in use?'
|
||||
# Note: Uses undocumented attribute
|
||||
emitter = self.observed and self.observer._emitter_for_watch[self.observed]
|
||||
|
||||
logger.debug('Entering loop...')
|
||||
while True:
|
||||
@ -452,7 +464,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
if logfile:
|
||||
loghandle = open(logfile, 'rb', 0) # unbuffered
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB
|
||||
|
||||
log_pos = 0
|
||||
@ -699,7 +711,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
# This event is logged when a player (on foot) gets into a ship or SRV
|
||||
# Parameters:
|
||||
# • SRV: true if getting into SRV, false if getting into a ship
|
||||
# • Taxi: true when boarding a taxi transposrt ship
|
||||
# • Taxi: true when boarding a taxi transport ship
|
||||
# • Multicrew: true when boarding another player’s vessel
|
||||
# • ID: player’s ship ID (if players own vessel)
|
||||
# • StarSystem
|
||||
@ -727,7 +739,7 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
#
|
||||
# Parameters:
|
||||
# • SRV: true if getting out of SRV, false if getting out of a ship
|
||||
# • Taxi: true when getting out of a taxi transposrt ship
|
||||
# • Taxi: true when getting out of a taxi transport ship
|
||||
# • Multicrew: true when getting out of another player’s vessel
|
||||
# • ID: player’s ship ID (if players own vessel)
|
||||
# • StarSystem
|
||||
@ -1942,12 +1954,12 @@ class EDLogs(FileSystemEventHandler): # type: ignore # See below
|
||||
|
||||
:return: bool - True if the game is running.
|
||||
"""
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
for app in NSWorkspace.sharedWorkspace().runningApplications():
|
||||
if app.bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
|
||||
return True
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
def WindowTitle(h): # noqa: N802 # type: ignore
|
||||
if h:
|
||||
length = GetWindowTextLength(h) + 1
|
||||
|
@ -4,22 +4,21 @@
|
||||
# - OSX: page background should be a darker gray than systemWindowBody
|
||||
# selected tab foreground should be White when the window is active
|
||||
#
|
||||
|
||||
from sys import platform
|
||||
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
# Entire file may be imported by plugins
|
||||
|
||||
# Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from platform import mac_ver
|
||||
PAGEFG = 'systemButtonText'
|
||||
PAGEBG = 'systemButtonActiveDarkShadow'
|
||||
elif platform == 'win32':
|
||||
|
||||
elif sys.platform == 'win32':
|
||||
PAGEFG = 'SystemWindowText'
|
||||
PAGEBG = 'SystemWindow' # typically white
|
||||
PAGEBG = 'SystemWindow' # typically white
|
||||
|
||||
|
||||
class Notebook(ttk.Notebook):
|
||||
@ -29,14 +28,14 @@ class Notebook(ttk.Notebook):
|
||||
ttk.Notebook.__init__(self, master, **kw)
|
||||
style = ttk.Style()
|
||||
|
||||
if platform=='darwin':
|
||||
if list(map(int, mac_ver()[0].split('.'))) >= [10,10]:
|
||||
if sys.platform == 'darwin':
|
||||
if list(map(int, mac_ver()[0].split('.'))) >= [10, 10]:
|
||||
# Hack for tab appearance with 8.5 on Yosemite & El Capitan. For proper fix see
|
||||
# https://github.com/tcltk/tk/commit/55c4dfca9353bbd69bbcec5d63bf1c8dfb461e25
|
||||
style.configure('TNotebook.Tab', padding=(12,10,12,2))
|
||||
style.configure('TNotebook.Tab', padding=(12, 10, 12, 2))
|
||||
style.map('TNotebook.Tab', foreground=[('selected', '!background', 'systemWhite')])
|
||||
self.grid(sticky=tk.NSEW) # Already padded apropriately
|
||||
elif platform == 'win32':
|
||||
self.grid(sticky=tk.NSEW) # Already padded apropriately
|
||||
elif sys.platform == 'win32':
|
||||
style.configure('nb.TFrame', background=PAGEBG)
|
||||
style.configure('nb.TButton', background=PAGEBG)
|
||||
style.configure('nb.TCheckbutton', foreground=PAGEFG, background=PAGEBG)
|
||||
@ -47,56 +46,60 @@ class Notebook(ttk.Notebook):
|
||||
self.grid(padx=10, pady=10, sticky=tk.NSEW)
|
||||
|
||||
|
||||
class Frame(platform == 'darwin' and tk.Frame or ttk.Frame):
|
||||
class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
tk.Frame.__init__(self, master, **kw)
|
||||
tk.Frame(self).grid(pady=5)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Frame.__init__(self, master, style='nb.TFrame', **kw)
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
else:
|
||||
ttk.Frame.__init__(self, master, **kw)
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
self.configure(takefocus = 1) # let the frame take focus so that no particular child is focused
|
||||
ttk.Frame(self).grid(pady=5) # top spacer
|
||||
self.configure(takefocus=1) # let the frame take focus so that no particular child is focused
|
||||
|
||||
|
||||
class Label(tk.Label):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform in ['darwin', 'win32']:
|
||||
if sys.platform in ['darwin', 'win32']:
|
||||
kw['foreground'] = kw.pop('foreground', PAGEFG)
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
else:
|
||||
kw['foreground'] = kw.pop('foreground', ttk.Style().lookup('TLabel', 'foreground'))
|
||||
kw['background'] = kw.pop('background', ttk.Style().lookup('TLabel', 'background'))
|
||||
tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms
|
||||
tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms
|
||||
|
||||
class Entry(platform == 'darwin' and tk.Entry or ttk.Entry):
|
||||
|
||||
class Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG)
|
||||
tk.Entry.__init__(self, master, **kw)
|
||||
else:
|
||||
ttk.Entry.__init__(self, master, **kw)
|
||||
|
||||
class Button(platform == 'darwin' and tk.Button or ttk.Button):
|
||||
|
||||
class Button(sys.platform == 'darwin' and tk.Button or ttk.Button):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG)
|
||||
tk.Button.__init__(self, master, **kw)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Button.__init__(self, master, style='nb.TButton', **kw)
|
||||
else:
|
||||
ttk.Button.__init__(self, master, **kw)
|
||||
|
||||
class ColoredButton(platform == 'darwin' and tk.Label or tk.Button):
|
||||
|
||||
class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Can't set Button background on OSX, so use a Label instead
|
||||
kw['relief'] = kw.pop('relief', tk.RAISED)
|
||||
self._command = kw.pop('command', None)
|
||||
@ -105,52 +108,55 @@ class ColoredButton(platform == 'darwin' and tk.Label or tk.Button):
|
||||
else:
|
||||
tk.Button.__init__(self, master, **kw)
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
def _press(self, event):
|
||||
self._command()
|
||||
|
||||
class Checkbutton(platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton):
|
||||
|
||||
class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['foreground'] = kw.pop('foreground', PAGEFG)
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
tk.Checkbutton.__init__(self, master, **kw)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Checkbutton.__init__(self, master, style='nb.TCheckbutton', **kw)
|
||||
else:
|
||||
ttk.Checkbutton.__init__(self, master, **kw)
|
||||
|
||||
class Radiobutton(platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton):
|
||||
|
||||
class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
kw['foreground'] = kw.pop('foreground', PAGEFG)
|
||||
kw['background'] = kw.pop('background', PAGEBG)
|
||||
tk.Radiobutton.__init__(self, master, **kw)
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
ttk.Radiobutton.__init__(self, master, style='nb.TRadiobutton', **kw)
|
||||
else:
|
||||
ttk.Radiobutton.__init__(self, master, **kw)
|
||||
|
||||
class OptionMenu(platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu):
|
||||
|
||||
class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu):
|
||||
|
||||
def __init__(self, master, variable, default=None, *values, **kw):
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
variable.set(default)
|
||||
bg = kw.pop('background', PAGEBG)
|
||||
tk.OptionMenu.__init__(self, master, variable, *values, **kw)
|
||||
self['background'] = bg
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
# OptionMenu derives from Menubutton at the Python level, so uses Menubutton's style
|
||||
ttk.OptionMenu.__init__(self, master, variable, default, *values, style='nb.TMenubutton', **kw)
|
||||
self['menu'].configure(background = PAGEBG)
|
||||
self['menu'].configure(background=PAGEBG)
|
||||
# Workaround for https://bugs.python.org/issue25684
|
||||
for i in range(0, self['menu'].index('end')+1):
|
||||
self['menu'].entryconfig(i, variable=variable)
|
||||
else:
|
||||
ttk.OptionMenu.__init__(self, master, variable, default, *values, **kw)
|
||||
self['menu'].configure(background = ttk.Style().lookup('TMenu', 'background'))
|
||||
self['menu'].configure(background=ttk.Style().lookup('TMenu', 'background'))
|
||||
# Workaround for https://bugs.python.org/issue25684
|
||||
for i in range(0, self['menu'].index('end')+1):
|
||||
self['menu'].entryconfig(i, variable=variable)
|
||||
|
48
prefs.py
48
prefs.py
@ -3,10 +3,10 @@
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from os.path import expanduser, expandvars, join, normpath
|
||||
from sys import platform
|
||||
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
|
||||
from tkinter import ttk
|
||||
from types import TracebackType
|
||||
@ -154,7 +154,7 @@ class AutoInc(contextlib.AbstractContextManager):
|
||||
return None
|
||||
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
import objc # type: ignore
|
||||
from Foundation import NSFileManager # type: ignore
|
||||
try:
|
||||
@ -179,7 +179,7 @@ if platform == 'darwin':
|
||||
|
||||
was_accessible_at_launch = AXIsProcessTrusted() # type: ignore
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
import winreg
|
||||
from ctypes.wintypes import HINSTANCE, HWND, LPARAM, LPCWSTR, LPVOID, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT
|
||||
@ -246,7 +246,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
self.parent = parent
|
||||
self.callback = callback
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# LANG: File > Preferences menu entry for macOS
|
||||
self.title(_('Preferences'))
|
||||
|
||||
@ -258,15 +258,15 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.transient(parent)
|
||||
|
||||
# position over parent
|
||||
if platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
# TODO this is fixed supposedly.
|
||||
self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}')
|
||||
|
||||
# remove decoration
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
elif platform == 'darwin':
|
||||
elif sys.platform == 'darwin':
|
||||
# http://wiki.tcl.tk/13428
|
||||
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
|
||||
|
||||
@ -294,7 +294,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.__setup_appearance_tab(notebook)
|
||||
self.__setup_plugin_tab(notebook)
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes
|
||||
|
||||
else:
|
||||
@ -322,7 +322,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.grab_set()
|
||||
|
||||
# Ensure fully on-screen
|
||||
if platform == 'win32' and CalculatePopupWindowPosition:
|
||||
if sys.platform == 'win32' and CalculatePopupWindowPosition:
|
||||
position = RECT()
|
||||
GetWindowRect(GetParent(self.winfo_id()), position)
|
||||
if CalculatePopupWindowPosition(
|
||||
@ -396,7 +396,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.outdir_entry = nb.Entry(output_frame, takefocus=False)
|
||||
self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=(0, self.PADY), sticky=tk.EW, row=row.get())
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
|
||||
|
||||
else:
|
||||
@ -446,7 +446,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=(0, self.PADY), sticky=tk.EW, row=row.get())
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
|
||||
|
||||
else:
|
||||
@ -470,7 +470,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
state=tk.NORMAL if config.get_str('journaldir') else tk.DISABLED
|
||||
).grid(column=2, pady=self.PADY, sticky=tk.EW, row=row.get())
|
||||
|
||||
if platform in ('darwin', 'win32'):
|
||||
if sys.platform in ('darwin', 'win32'):
|
||||
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
|
||||
columnspan=4, padx=self.PADX, pady=self.PADY*4, sticky=tk.EW, row=row.get()
|
||||
)
|
||||
@ -482,11 +482,11 @@ class PreferencesDialog(tk.Toplevel):
|
||||
nb.Label(
|
||||
config_frame,
|
||||
text=_('Keyboard shortcut') if # LANG: Hotkey/Shortcut settings prompt on OSX
|
||||
platform == 'darwin' else
|
||||
sys.platform == 'darwin' else
|
||||
_('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows
|
||||
).grid(padx=self.PADX, sticky=tk.W, row=row.get())
|
||||
|
||||
if platform == 'darwin' and not was_accessible_at_launch:
|
||||
if sys.platform == 'darwin' and not was_accessible_at_launch:
|
||||
if AXIsProcessTrusted():
|
||||
# Shortcut settings prompt on OSX
|
||||
nb.Label(
|
||||
@ -511,7 +511,8 @@ class PreferencesDialog(tk.Toplevel):
|
||||
)
|
||||
|
||||
else:
|
||||
self.hotkey_text = nb.Entry(config_frame, width=(20 if platform == 'darwin' else 30), justify=tk.CENTER)
|
||||
self.hotkey_text = nb.Entry(config_frame, width=(
|
||||
20 if sys.platform == 'darwin' else 30), justify=tk.CENTER)
|
||||
self.hotkey_text.insert(
|
||||
0,
|
||||
# No hotkey/shortcut currently defined
|
||||
@ -741,7 +742,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
appearance_frame, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged
|
||||
).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get())
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
nb.Radiobutton(
|
||||
appearance_frame,
|
||||
# LANG: Label for 'Transparent' theme radio button
|
||||
@ -870,7 +871,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
)
|
||||
self.ontop_button.grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
nb.Checkbutton(
|
||||
appearance_frame,
|
||||
# LANG: Appearance option for Windows "minimize to system tray"
|
||||
@ -997,7 +998,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
def tabchanged(self, event: tk.Event) -> None:
|
||||
"""Handle preferences active tab changing."""
|
||||
self.outvarchanged()
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Hack to recompute size so that buttons show up under Mojave
|
||||
notebook = event.widget
|
||||
frame = self.nametowidget(notebook.winfo_parent())
|
||||
@ -1027,9 +1028,8 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
# If encoding isn't UTF-8 we can't use the tkinter dialog
|
||||
current_locale = locale.getlocale(locale.LC_CTYPE)
|
||||
from sys import platform as sys_platform
|
||||
directory = None
|
||||
if sys_platform == 'win32' and current_locale[1] not in ('utf8', 'UTF8', 'utf-8', 'UTF-8'):
|
||||
if sys.platform == 'win32' and current_locale[1] not in ('utf8', 'UTF8', 'utf-8', 'UTF-8'):
|
||||
def browsecallback(hwnd, uMsg, lParam, lpData): # noqa: N803 # Windows API convention
|
||||
# set initial folder
|
||||
if uMsg == BFFM_INITIALIZED and lpData:
|
||||
@ -1075,7 +1075,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
# TODO: This is awful.
|
||||
entryfield['state'] = tk.NORMAL # must be writable to update
|
||||
entryfield.delete(0, tk.END)
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
start = len(config.home.split('\\')) if pathvar.get().lower().startswith(config.home.lower()) else 0
|
||||
display = []
|
||||
components = normpath(pathvar.get()).split('\\')
|
||||
@ -1096,7 +1096,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
entryfield.insert(0, '\\'.join(display))
|
||||
|
||||
# None if path doesn't exist
|
||||
elif platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()):
|
||||
elif sys.platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()):
|
||||
if pathvar.get().startswith(config.home):
|
||||
display = ['~'] + NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())[
|
||||
len(NSFileManager.defaultManager().componentsToDisplayForPath_(config.home)):
|
||||
@ -1236,7 +1236,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
else:
|
||||
config.set('journaldir', logdir)
|
||||
|
||||
if platform in ('darwin', 'win32'):
|
||||
if sys.platform in ('darwin', 'win32'):
|
||||
config.set('hotkey_code', self.hotkey_code)
|
||||
config.set('hotkey_mods', self.hotkey_mods)
|
||||
config.set('hotkey_always', int(not self.hotkey_only.get()))
|
||||
@ -1282,7 +1282,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
self.parent.wm_attributes('-topmost', 1 if config.get_int('always_ontop') else 0)
|
||||
self.destroy()
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
def enableshortcuts(self) -> None:
|
||||
"""Set up macOS preferences shortcut."""
|
||||
self.apply()
|
||||
|
@ -3,10 +3,13 @@ max_line_length = 120
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 5
|
||||
line_length = 119
|
||||
line_length = 119
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"] # Search for tests in tests/
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = ["venv/*"] # when running pytest --cov, dont report coverage in venv directories
|
||||
omit = ["venv/*"] # when running pytest --cov, dont report coverage in venv directories
|
||||
|
||||
[tool.pyright]
|
||||
# pythonPlatform = 'Darwin'
|
||||
|
16
stats.py
16
stats.py
@ -1,9 +1,9 @@
|
||||
"""CMDR Status information."""
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
import tkinter
|
||||
import tkinter as tk
|
||||
from sys import platform
|
||||
from tkinter import ttk
|
||||
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Dict, List, NamedTuple, Optional, Sequence, cast
|
||||
|
||||
@ -20,11 +20,9 @@ logger = EDMCLogging.get_main_logger()
|
||||
if TYPE_CHECKING:
|
||||
def _(x: str) -> str: ...
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT
|
||||
if TYPE_CHECKING:
|
||||
import ctypes.windll # type: ignore # Fake this into existing, its really a magic dll thing
|
||||
|
||||
try:
|
||||
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
|
||||
@ -372,15 +370,15 @@ class StatsResults(tk.Toplevel):
|
||||
self.transient(parent)
|
||||
|
||||
# position over parent
|
||||
if platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
|
||||
self.geometry(f"+{parent.winfo_rootx()}+{parent.winfo_rooty()}")
|
||||
|
||||
# remove decoration
|
||||
self.resizable(tk.FALSE, tk.FALSE)
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
self.attributes('-toolwindow', tk.TRUE)
|
||||
|
||||
elif platform == 'darwin':
|
||||
elif sys.platform == 'darwin':
|
||||
# http://wiki.tcl.tk/13428
|
||||
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
|
||||
|
||||
@ -421,7 +419,7 @@ class StatsResults(tk.Toplevel):
|
||||
ttk.Frame(page).grid(pady=5) # bottom spacer
|
||||
notebook.add(page, text=_('Ships')) # LANG: Status dialog title
|
||||
|
||||
if platform != 'darwin':
|
||||
if sys.platform != 'darwin':
|
||||
buttonframe = ttk.Frame(frame)
|
||||
buttonframe.grid(padx=10, pady=(0, 10), sticky=tk.NSEW) # type: ignore # the tuple is supported
|
||||
buttonframe.columnconfigure(0, weight=1)
|
||||
@ -433,7 +431,7 @@ class StatsResults(tk.Toplevel):
|
||||
self.grab_set()
|
||||
|
||||
# Ensure fully on-screen
|
||||
if platform == 'win32' and CalculatePopupWindowPosition:
|
||||
if sys.platform == 'win32' and CalculatePopupWindowPosition:
|
||||
position = RECT()
|
||||
GetWindowRect(GetParent(self.winfo_id()), position)
|
||||
if CalculatePopupWindowPosition(
|
||||
|
@ -4,7 +4,6 @@ import warnings
|
||||
from configparser import NoOptionError
|
||||
from os import getenv, makedirs, mkdir, pardir
|
||||
from os.path import dirname, expanduser, isdir, join, normpath
|
||||
from sys import platform
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from config import applongname, appname, update_interval
|
||||
@ -12,13 +11,13 @@ from EDMCLogging import get_main_logger
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from Foundation import ( # type: ignore
|
||||
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains,
|
||||
NSUserDefaults, NSUserDomainMask
|
||||
)
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
import ctypes
|
||||
import uuid
|
||||
from ctypes.wintypes import DWORD, HANDLE, HKEY, LONG, LPCVOID, LPCWSTR
|
||||
@ -90,7 +89,7 @@ elif platform == 'win32':
|
||||
CoTaskMemFree(buf) # and free original
|
||||
return retval
|
||||
|
||||
elif platform == 'linux':
|
||||
elif sys.platform == 'linux':
|
||||
import codecs
|
||||
from configparser import RawConfigParser
|
||||
|
||||
@ -114,7 +113,7 @@ class OldConfig():
|
||||
OUT_SYS_EDDN = 2048
|
||||
OUT_SYS_DELAY = 4096
|
||||
|
||||
if platform == 'darwin': # noqa: C901 # It's gating *all* the functions
|
||||
if sys.platform == 'darwin': # noqa: C901 # It's gating *all* the functions
|
||||
|
||||
def __init__(self):
|
||||
self.app_dir = join(
|
||||
@ -199,7 +198,7 @@ class OldConfig():
|
||||
self.save()
|
||||
self.defaults = None
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
|
||||
def __init__(self):
|
||||
self.app_dir = join(known_folder_path(FOLDERID_LocalAppData), appname) # type: ignore # Not going to change
|
||||
@ -362,7 +361,7 @@ class OldConfig():
|
||||
RegCloseKey(self.hkey)
|
||||
self.hkey = None
|
||||
|
||||
elif platform == 'linux':
|
||||
elif sys.platform == 'linux':
|
||||
SECTION = 'config'
|
||||
|
||||
def __init__(self):
|
||||
|
@ -1,4 +1,12 @@
|
||||
"""Test the config system."""
|
||||
"""
|
||||
Test the config system.
|
||||
|
||||
Note: These tests to arbitrary reads and writes to an existing config, including
|
||||
key deletions. Said modifications are to keys that are generated internally.
|
||||
|
||||
Most of these tests are parity tests with the "old" config, and likely one day can be
|
||||
entirely removed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
@ -19,7 +27,7 @@ print(sys.path)
|
||||
|
||||
from _old_config import old_config # noqa: E402
|
||||
|
||||
from config import LinuxConfig, config # noqa: E402
|
||||
from config import config # noqa: E402
|
||||
|
||||
|
||||
def _fuzz_list(length: int) -> List[str]:
|
||||
@ -77,6 +85,11 @@ class TestNewConfig:
|
||||
|
||||
def __update_linuxconfig(self) -> None:
|
||||
"""On linux config uses ConfigParser, which doesn't update from disk changes. Force the update here."""
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
|
||||
from config.linux import LinuxConfig
|
||||
|
||||
if isinstance(config, LinuxConfig) and config.config is not None:
|
||||
config.config.read(config.filename)
|
||||
|
||||
@ -163,6 +176,10 @@ class TestOldNewConfig:
|
||||
|
||||
def __update_linuxconfig(self) -> None:
|
||||
"""On linux config uses ConfigParser, which doesn't update from disk changes. Force the update here."""
|
||||
if sys.platform != 'linux':
|
||||
return
|
||||
|
||||
from config.linux import LinuxConfig
|
||||
if isinstance(config, LinuxConfig) and config.config is not None:
|
||||
config.config.read(config.filename)
|
||||
|
||||
|
206
theme.py
206
theme.py
@ -6,9 +6,9 @@
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from os.path import join
|
||||
from sys import platform
|
||||
from tkinter import font as tkFont
|
||||
from tkinter import ttk
|
||||
|
||||
@ -18,20 +18,20 @@ from ttkHyperlinkLabel import HyperlinkLabel
|
||||
if __debug__:
|
||||
from traceback import print_exc
|
||||
|
||||
if platform == "linux":
|
||||
if sys.platform == "linux":
|
||||
from ctypes import POINTER, Structure, byref, c_char_p, c_int, c_long, c_uint, c_ulong, c_void_p, cdll
|
||||
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR
|
||||
AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW
|
||||
AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID]
|
||||
FR_PRIVATE = 0x10
|
||||
FR_PRIVATE = 0x10
|
||||
FR_NOT_ENUM = 0x20
|
||||
AddFontResourceEx(join(config.respath, u'EUROCAPS.TTF'), FR_PRIVATE, 0)
|
||||
|
||||
elif platform == 'linux':
|
||||
elif sys.platform == 'linux':
|
||||
# pyright: reportUnboundVariable=false
|
||||
XID = c_ulong # from X.h: typedef unsigned long XID
|
||||
Window = XID
|
||||
@ -40,7 +40,7 @@ elif platform == 'linux':
|
||||
|
||||
PropModeReplace = 0
|
||||
PropModePrepend = 1
|
||||
PropModeAppend = 2
|
||||
PropModeAppend = 2
|
||||
|
||||
# From xprops.h
|
||||
MWM_HINTS_FUNCTIONS = 1 << 0
|
||||
@ -69,16 +69,17 @@ elif platform == 'linux':
|
||||
('input_mode', c_long),
|
||||
('status', c_ulong),
|
||||
]
|
||||
|
||||
|
||||
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
|
||||
if not os.getenv("EDMC_NO_UI") :
|
||||
if not os.getenv("EDMC_NO_UI"):
|
||||
try:
|
||||
xlib = cdll.LoadLibrary('libX11.so.6')
|
||||
XInternAtom = xlib.XInternAtom
|
||||
XInternAtom.argtypes = [POINTER(Display), c_char_p, c_int]
|
||||
XInternAtom.restype = Atom
|
||||
XChangeProperty = xlib.XChangeProperty
|
||||
XChangeProperty.argtypes = [POINTER(Display), Window, Atom, Atom, c_int, c_int, POINTER(MotifWmHints), c_int]
|
||||
XChangeProperty.argtypes = [POINTER(Display), Window, Atom, Atom, c_int,
|
||||
c_int, POINTER(MotifWmHints), c_int]
|
||||
XChangeProperty.restype = c_int
|
||||
XFlush = xlib.XFlush
|
||||
XFlush.argtypes = [POINTER(Display)]
|
||||
@ -87,29 +88,31 @@ elif platform == 'linux':
|
||||
XOpenDisplay.argtypes = [c_char_p]
|
||||
XOpenDisplay.restype = POINTER(Display)
|
||||
XQueryTree = xlib.XQueryTree
|
||||
XQueryTree.argtypes = [POINTER(Display), Window, POINTER(Window), POINTER(Window), POINTER(Window), POINTER(c_uint)]
|
||||
XQueryTree.argtypes = [POINTER(Display), Window, POINTER(
|
||||
Window), POINTER(Window), POINTER(Window), POINTER(c_uint)]
|
||||
XQueryTree.restype = c_int
|
||||
dpy = xlib.XOpenDisplay(None)
|
||||
if not dpy:
|
||||
raise Exception("Can't find your display, can't continue")
|
||||
|
||||
|
||||
motif_wm_hints_property = XInternAtom(dpy, b'_MOTIF_WM_HINTS', False)
|
||||
motif_wm_hints_normal = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS,
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE | MWM_DECOR_MENU | MWM_DECOR_MINIMIZE,
|
||||
0, 0)
|
||||
motif_wm_hints_dark = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS,
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
0, 0, 0)
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
MWM_DECOR_BORDER | MWM_DECOR_RESIZEH | MWM_DECOR_TITLE | MWM_DECOR_MENU | MWM_DECOR_MINIMIZE,
|
||||
0, 0)
|
||||
motif_wm_hints_dark = MotifWmHints(MWM_HINTS_FUNCTIONS | MWM_HINTS_DECORATIONS,
|
||||
MWM_FUNC_RESIZE | MWM_FUNC_MOVE | MWM_FUNC_MINIMIZE | MWM_FUNC_CLOSE,
|
||||
0, 0, 0)
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
if __debug__:
|
||||
print_exc()
|
||||
dpy = None
|
||||
|
||||
|
||||
class _Theme(object):
|
||||
|
||||
def __init__(self):
|
||||
self.active = None # Starts out with no theme
|
||||
self.active = None # Starts out with no theme
|
||||
self.minwidth = None
|
||||
self.widgets = {}
|
||||
self.widgets_pair = []
|
||||
@ -124,18 +127,18 @@ class _Theme(object):
|
||||
if not self.defaults:
|
||||
# Can't initialise this til window is created # Windows, MacOS
|
||||
self.defaults = {
|
||||
'fg' : tk.Label()['foreground'], # SystemButtonText, systemButtonText
|
||||
'bg' : tk.Label()['background'], # SystemButtonFace, White
|
||||
'font' : tk.Label()['font'], # TkDefaultFont
|
||||
'bitmapfg' : tk.BitmapImage()['foreground'], # '-foreground {} {} #000000 #000000'
|
||||
'bitmapbg' : tk.BitmapImage()['background'], # '-background {} {} {} {}'
|
||||
'entryfg' : tk.Entry()['foreground'], # SystemWindowText, Black
|
||||
'entrybg' : tk.Entry()['background'], # SystemWindow, systemWindowBody
|
||||
'entryfont' : tk.Entry()['font'], # TkTextFont
|
||||
'frame' : tk.Frame()['background'], # SystemButtonFace, systemWindowBody
|
||||
'menufg' : tk.Menu()['foreground'], # SystemMenuText,
|
||||
'menubg' : tk.Menu()['background'], # SystemMenu,
|
||||
'menufont' : tk.Menu()['font'], # TkTextFont
|
||||
'fg': tk.Label()['foreground'], # SystemButtonText, systemButtonText
|
||||
'bg': tk.Label()['background'], # SystemButtonFace, White
|
||||
'font': tk.Label()['font'], # TkDefaultFont
|
||||
'bitmapfg': tk.BitmapImage()['foreground'], # '-foreground {} {} #000000 #000000'
|
||||
'bitmapbg': tk.BitmapImage()['background'], # '-background {} {} {} {}'
|
||||
'entryfg': tk.Entry()['foreground'], # SystemWindowText, Black
|
||||
'entrybg': tk.Entry()['background'], # SystemWindow, systemWindowBody
|
||||
'entryfont': tk.Entry()['font'], # TkTextFont
|
||||
'frame': tk.Frame()['background'], # SystemButtonFace, systemWindowBody
|
||||
'menufg': tk.Menu()['foreground'], # SystemMenuText,
|
||||
'menubg': tk.Menu()['background'], # SystemMenu,
|
||||
'menufont': tk.Menu()['font'], # TkTextFont
|
||||
}
|
||||
|
||||
if widget not in self.widgets:
|
||||
@ -189,26 +192,27 @@ class _Theme(object):
|
||||
def _enter(self, event, image):
|
||||
widget = event.widget
|
||||
if widget and widget['state'] != tk.DISABLED:
|
||||
widget.configure(state = tk.ACTIVE)
|
||||
widget.configure(state=tk.ACTIVE)
|
||||
if image:
|
||||
image.configure(foreground = self.current['activeforeground'], background = self.current['activebackground'])
|
||||
image.configure(foreground=self.current['activeforeground'],
|
||||
background=self.current['activebackground'])
|
||||
|
||||
def _leave(self, event, image):
|
||||
widget = event.widget
|
||||
if widget and widget['state'] != tk.DISABLED:
|
||||
widget.configure(state = tk.NORMAL)
|
||||
widget.configure(state=tk.NORMAL)
|
||||
if image:
|
||||
image.configure(foreground = self.current['foreground'], background = self.current['background'])
|
||||
image.configure(foreground=self.current['foreground'], background=self.current['background'])
|
||||
|
||||
# Set up colors
|
||||
def _colors(self, root, theme):
|
||||
style = ttk.Style()
|
||||
if platform == 'linux':
|
||||
if sys.platform == 'linux':
|
||||
style.theme_use('clam')
|
||||
|
||||
# Default dark theme colors
|
||||
if not config.get_str('dark_text'):
|
||||
config.set('dark_text', '#ff8000') # "Tangerine" in OSX color picker
|
||||
config.set('dark_text', '#ff8000') # "Tangerine" in OSX color picker
|
||||
if not config.get_str('dark_highlight'):
|
||||
config.set('dark_highlight', 'white')
|
||||
|
||||
@ -216,40 +220,40 @@ class _Theme(object):
|
||||
# Dark
|
||||
(r, g, b) = root.winfo_rgb(config.get_str('dark_text'))
|
||||
self.current = {
|
||||
'background' : 'grey4', # OSX inactive dark titlebar color
|
||||
'foreground' : config.get_str('dark_text'),
|
||||
'activebackground' : config.get_str('dark_text'),
|
||||
'activeforeground' : 'grey4',
|
||||
'disabledforeground' : '#%02x%02x%02x' % (int(r/384), int(g/384), int(b/384)),
|
||||
'highlight' : config.get_str('dark_highlight'),
|
||||
'background': 'grey4', # OSX inactive dark titlebar color
|
||||
'foreground': config.get_str('dark_text'),
|
||||
'activebackground': config.get_str('dark_text'),
|
||||
'activeforeground': 'grey4',
|
||||
'disabledforeground': '#%02x%02x%02x' % (int(r/384), int(g/384), int(b/384)),
|
||||
'highlight': config.get_str('dark_highlight'),
|
||||
# Font only supports Latin 1 / Supplement / Extended, and a few General Punctuation and Mathematical Operators
|
||||
# LANG: Label for commander name in main window
|
||||
'font' : (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and
|
||||
tkFont.Font(family='Euro Caps', size=10, weight=tkFont.NORMAL) or
|
||||
'TkDefaultFont'),
|
||||
'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and
|
||||
tkFont.Font(family='Euro Caps', size=10, weight=tkFont.NORMAL) or
|
||||
'TkDefaultFont'),
|
||||
}
|
||||
else:
|
||||
# (Mostly) system colors
|
||||
style = ttk.Style()
|
||||
self.current = {
|
||||
'background' : (platform == 'darwin' and 'systemMovableModalBackground' or
|
||||
style.lookup('TLabel', 'background')),
|
||||
'foreground' : style.lookup('TLabel', 'foreground'),
|
||||
'activebackground' : (platform == 'win32' and 'SystemHighlight' or
|
||||
style.lookup('TLabel', 'background', ['active'])),
|
||||
'activeforeground' : (platform == 'win32' and 'SystemHighlightText' or
|
||||
style.lookup('TLabel', 'foreground', ['active'])),
|
||||
'disabledforeground' : style.lookup('TLabel', 'foreground', ['disabled']),
|
||||
'highlight' : 'blue',
|
||||
'font' : 'TkDefaultFont',
|
||||
'background': (sys.platform == 'darwin' and 'systemMovableModalBackground' or
|
||||
style.lookup('TLabel', 'background')),
|
||||
'foreground': style.lookup('TLabel', 'foreground'),
|
||||
'activebackground': (sys.platform == 'win32' and 'SystemHighlight' or
|
||||
style.lookup('TLabel', 'background', ['active'])),
|
||||
'activeforeground': (sys.platform == 'win32' and 'SystemHighlightText' or
|
||||
style.lookup('TLabel', 'foreground', ['active'])),
|
||||
'disabledforeground': style.lookup('TLabel', 'foreground', ['disabled']),
|
||||
'highlight': 'blue',
|
||||
'font': 'TkDefaultFont',
|
||||
}
|
||||
|
||||
|
||||
# Apply current theme to a widget and its children, and register it for future updates
|
||||
|
||||
def update(self, widget):
|
||||
assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget
|
||||
if not self.current:
|
||||
return # No need to call this for widgets created in plugin_app()
|
||||
return # No need to call this for widgets created in plugin_app()
|
||||
self.register(widget)
|
||||
self._update_widget(widget)
|
||||
if isinstance(widget, tk.Frame) or isinstance(widget, ttk.Frame):
|
||||
@ -258,56 +262,57 @@ class _Theme(object):
|
||||
|
||||
# Apply current theme to a single widget
|
||||
def _update_widget(self, widget):
|
||||
assert widget in self.widgets, '%s %s "%s"' %(widget.winfo_class(), widget, 'text' in widget.keys() and widget['text'])
|
||||
assert widget in self.widgets, '%s %s "%s"' % (
|
||||
widget.winfo_class(), widget, 'text' in widget.keys() and widget['text'])
|
||||
attribs = self.widgets.get(widget, [])
|
||||
|
||||
if isinstance(widget, tk.BitmapImage):
|
||||
# not a widget
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['foreground']),
|
||||
widget.configure(foreground=self.current['foreground']),
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'])
|
||||
widget.configure(background=self.current['background'])
|
||||
elif 'cursor' in widget.keys() and str(widget['cursor']) not in ['', 'arrow']:
|
||||
# Hack - highlight widgets like HyperlinkLabel with a non-default cursor
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['highlight']),
|
||||
if 'insertbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(insertbackground = self.current['foreground']),
|
||||
widget.configure(foreground=self.current['highlight']),
|
||||
if 'insertbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(insertbackground=self.current['foreground']),
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'])
|
||||
if 'highlightbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(highlightbackground = self.current['background'])
|
||||
widget.configure(background=self.current['background'])
|
||||
if 'highlightbackground' in widget.keys(): # tk.Entry
|
||||
widget.configure(highlightbackground=self.current['background'])
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font = self.current['font'])
|
||||
widget.configure(font=self.current['font'])
|
||||
elif 'activeforeground' in widget.keys():
|
||||
# e.g. tk.Button, tk.Label, tk.Menu
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['foreground'],
|
||||
activeforeground = self.current['activeforeground'],
|
||||
disabledforeground = self.current['disabledforeground'])
|
||||
widget.configure(foreground=self.current['foreground'],
|
||||
activeforeground=self.current['activeforeground'],
|
||||
disabledforeground=self.current['disabledforeground'])
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'],
|
||||
activebackground = self.current['activebackground'])
|
||||
if platform == 'darwin' and isinstance(widget, tk.Button):
|
||||
widget.configure(highlightbackground = self.current['background'])
|
||||
widget.configure(background=self.current['background'],
|
||||
activebackground=self.current['activebackground'])
|
||||
if sys.platform == 'darwin' and isinstance(widget, tk.Button):
|
||||
widget.configure(highlightbackground=self.current['background'])
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font = self.current['font'])
|
||||
widget.configure(font=self.current['font'])
|
||||
elif 'foreground' in widget.keys():
|
||||
# e.g. ttk.Label
|
||||
if 'fg' not in attribs:
|
||||
widget.configure(foreground = self.current['foreground']),
|
||||
widget.configure(foreground=self.current['foreground']),
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'])
|
||||
widget.configure(background=self.current['background'])
|
||||
if 'font' not in attribs:
|
||||
widget.configure(font = self.current['font'])
|
||||
widget.configure(font=self.current['font'])
|
||||
elif 'background' in widget.keys() or isinstance(widget, tk.Canvas):
|
||||
# e.g. Frame, Canvas
|
||||
if 'bg' not in attribs:
|
||||
widget.configure(background = self.current['background'],
|
||||
highlightbackground = self.current['disabledforeground'])
|
||||
|
||||
widget.configure(background=self.current['background'],
|
||||
highlightbackground=self.current['disabledforeground'])
|
||||
|
||||
# Apply configured theme
|
||||
|
||||
def apply(self, root):
|
||||
|
||||
theme = config.get_int('theme')
|
||||
@ -316,7 +321,7 @@ class _Theme(object):
|
||||
# Apply colors
|
||||
for widget in set(self.widgets):
|
||||
if isinstance(widget, tk.Widget) and not widget.winfo_exists():
|
||||
self.widgets.pop(widget) # has been destroyed
|
||||
self.widgets.pop(widget) # has been destroyed
|
||||
else:
|
||||
self._update_widget(widget)
|
||||
|
||||
@ -334,58 +339,61 @@ class _Theme(object):
|
||||
pair[theme].grid(**gridopts)
|
||||
|
||||
if self.active == theme:
|
||||
return # Don't need to mess with the window manager
|
||||
return # Don't need to mess with the window manager
|
||||
else:
|
||||
self.active = theme
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
from AppKit import NSAppearance, NSApplication, NSMiniaturizableWindowMask, NSResizableWindowMask
|
||||
root.update_idletasks() # need main window to be created
|
||||
root.update_idletasks() # need main window to be created
|
||||
appearance = NSAppearance.appearanceNamed_(theme and
|
||||
'NSAppearanceNameDarkAqua' or
|
||||
'NSAppearanceNameAqua')
|
||||
for window in NSApplication.sharedApplication().windows():
|
||||
window.setStyleMask_(window.styleMask() & ~(NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom
|
||||
window.setStyleMask_(window.styleMask() & ~(
|
||||
NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom
|
||||
window.setAppearance_(appearance)
|
||||
|
||||
elif platform == 'win32':
|
||||
elif sys.platform == 'win32':
|
||||
GWL_STYLE = -16
|
||||
WS_MAXIMIZEBOX = 0x00010000
|
||||
WS_MAXIMIZEBOX = 0x00010000
|
||||
# tk8.5.9/win/tkWinWm.c:342
|
||||
GWL_EXSTYLE = -20
|
||||
WS_EX_APPWINDOW = 0x00040000
|
||||
WS_EX_LAYERED = 0x00080000
|
||||
WS_EX_APPWINDOW = 0x00040000
|
||||
WS_EX_LAYERED = 0x00080000
|
||||
GetWindowLongW = ctypes.windll.user32.GetWindowLongW
|
||||
SetWindowLongW = ctypes.windll.user32.SetWindowLongW
|
||||
|
||||
root.overrideredirect(theme and 1 or 0)
|
||||
root.attributes("-transparentcolor", theme > 1 and 'grey4' or '')
|
||||
root.withdraw()
|
||||
root.update_idletasks() # Size and windows styles get recalculated here
|
||||
root.update_idletasks() # Size and windows styles get recalculated here
|
||||
hwnd = ctypes.windll.user32.GetParent(root.winfo_id())
|
||||
SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize
|
||||
SetWindowLongW(hwnd, GWL_EXSTYLE, theme > 1 and WS_EX_APPWINDOW|WS_EX_LAYERED or WS_EX_APPWINDOW) # Add to taskbar
|
||||
SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize
|
||||
SetWindowLongW(hwnd, GWL_EXSTYLE, theme > 1 and WS_EX_APPWINDOW |
|
||||
WS_EX_LAYERED or WS_EX_APPWINDOW) # Add to taskbar
|
||||
root.deiconify()
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
|
||||
else:
|
||||
root.withdraw()
|
||||
root.update_idletasks() # Size gets recalculated here
|
||||
root.update_idletasks() # Size gets recalculated here
|
||||
if dpy:
|
||||
xroot = Window()
|
||||
parent = Window()
|
||||
children = Window()
|
||||
nchildren = c_uint()
|
||||
XQueryTree(dpy, root.winfo_id(), byref(xroot), byref(parent), byref(children), byref(nchildren))
|
||||
XChangeProperty(dpy, parent, motif_wm_hints_property, motif_wm_hints_property, 32, PropModeReplace, theme and motif_wm_hints_dark or motif_wm_hints_normal, 5)
|
||||
XChangeProperty(dpy, parent, motif_wm_hints_property, motif_wm_hints_property, 32,
|
||||
PropModeReplace, theme and motif_wm_hints_dark or motif_wm_hints_normal, 5)
|
||||
XFlush(dpy)
|
||||
else:
|
||||
root.overrideredirect(theme and 1 or 0)
|
||||
root.deiconify()
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
root.wait_visibility() # need main window to be displayed before returning
|
||||
|
||||
if not self.minwidth:
|
||||
self.minwidth = root.winfo_width() # Minimum width = width on first creation
|
||||
self.minwidth = root.winfo_width() # Minimum width = width on first creation
|
||||
root.minsize(self.minwidth, -1)
|
||||
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import webbrowser
|
||||
from sys import platform
|
||||
from tkinter import font as tkFont
|
||||
from tkinter import ttk
|
||||
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
import subprocess
|
||||
from winreg import (
|
||||
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, OpenKeyEx, QueryValueEx
|
||||
)
|
||||
from winreg import HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, OpenKeyEx, QueryValueEx
|
||||
|
||||
# A clickable ttk Label
|
||||
#
|
||||
@ -18,19 +16,22 @@ if platform == 'win32':
|
||||
# popup_copy: Whether right-click on non-empty label text pops up a context menu with a 'Copy' option. Defaults to no context menu. If popup_copy is a function it will be called with the current label text and should return a boolean.
|
||||
#
|
||||
# May be imported by plugins
|
||||
class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
|
||||
class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
def __init__(self, master=None, **kw):
|
||||
self.url = 'url' in kw and kw.pop('url') or None
|
||||
self.popup_copy = kw.pop('popup_copy', False)
|
||||
self.underline = kw.pop('underline', None) # override ttk.Label's underline
|
||||
self.underline = kw.pop('underline', None) # override ttk.Label's underline
|
||||
self.foreground = kw.get('foreground') or 'blue'
|
||||
self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup('TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option
|
||||
self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup(
|
||||
'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option
|
||||
|
||||
if platform == 'darwin':
|
||||
if sys.platform == 'darwin':
|
||||
# Use tk.Label 'cos can't set ttk.Label background - http://www.tkdocs.com/tutorial/styles.html#whydifficult
|
||||
kw['background'] = kw.pop('background', 'systemDialogBackgroundActive')
|
||||
kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label
|
||||
kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label
|
||||
tk.Label.__init__(self, master, **kw)
|
||||
else:
|
||||
ttk.Label.__init__(self, master, **kw)
|
||||
@ -39,16 +40,16 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
self.menu = tk.Menu(None, tearoff=tk.FALSE)
|
||||
# LANG: Label for 'Copy' as in 'Copy and Paste'
|
||||
self.menu.add_command(label=_('Copy'), command = self.copy) # As in Copy and Paste
|
||||
self.bind(platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu)
|
||||
self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste
|
||||
self.bind(sys.platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu)
|
||||
|
||||
self.bind('<Enter>', self._enter)
|
||||
self.bind('<Leave>', self._leave)
|
||||
|
||||
# set up initial appearance
|
||||
self.configure(state = kw.get('state', tk.NORMAL),
|
||||
text = kw.get('text'),
|
||||
font = kw.get('font', ttk.Style().lookup('TLabel', 'font')))
|
||||
self.configure(state=kw.get('state', tk.NORMAL),
|
||||
text=kw.get('text'),
|
||||
font=kw.get('font', ttk.Style().lookup('TLabel', 'font')))
|
||||
|
||||
# Change cursor and appearance depending on state and text
|
||||
def configure(self, cnf=None, **kw):
|
||||
@ -70,17 +71,18 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
if 'font' in kw:
|
||||
self.font_n = kw['font']
|
||||
self.font_u = tkFont.Font(font = self.font_n)
|
||||
self.font_u.configure(underline = True)
|
||||
self.font_u = tkFont.Font(font=self.font_n)
|
||||
self.font_u.configure(underline=True)
|
||||
kw['font'] = self.underline is True and self.font_u or self.font_n
|
||||
|
||||
if 'cursor' not in kw:
|
||||
if (kw['state'] if 'state' in kw else str(self['state'])) == tk.DISABLED:
|
||||
kw['cursor'] = 'arrow' # System default
|
||||
kw['cursor'] = 'arrow' # System default
|
||||
elif self.url and (kw['text'] if 'text' in kw else self['text']):
|
||||
kw['cursor'] = platform=='darwin' and 'pointinghand' or 'hand2'
|
||||
kw['cursor'] = sys.platform == 'darwin' and 'pointinghand' or 'hand2'
|
||||
else:
|
||||
kw['cursor'] = (platform=='darwin' and 'notallowed') or (platform=='win32' and 'no') or 'circle'
|
||||
kw['cursor'] = (sys.platform == 'darwin' and 'notallowed') or (
|
||||
sys.platform == 'win32' and 'no') or 'circle'
|
||||
|
||||
super(HyperlinkLabel, self).configure(cnf, **kw)
|
||||
|
||||
@ -89,22 +91,22 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
def _enter(self, event):
|
||||
if self.url and self.underline is not False and str(self['state']) != tk.DISABLED:
|
||||
super(HyperlinkLabel, self).configure(font = self.font_u)
|
||||
super(HyperlinkLabel, self).configure(font=self.font_u)
|
||||
|
||||
def _leave(self, event):
|
||||
if not self.underline:
|
||||
super(HyperlinkLabel, self).configure(font = self.font_n)
|
||||
super(HyperlinkLabel, self).configure(font=self.font_n)
|
||||
|
||||
def _click(self, event):
|
||||
if self.url and self['text'] and str(self['state']) != tk.DISABLED:
|
||||
url = self.url(self['text']) if callable(self.url) else self.url
|
||||
if url:
|
||||
self._leave(event) # Remove underline before we change window to browser
|
||||
self._leave(event) # Remove underline before we change window to browser
|
||||
openurl(url)
|
||||
|
||||
def _contextmenu(self, event):
|
||||
if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy):
|
||||
self.menu.post(platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root)
|
||||
self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root)
|
||||
|
||||
def copy(self):
|
||||
self.clipboard_clear()
|
||||
@ -112,13 +114,14 @@ class HyperlinkLabel(platform == 'darwin' and tk.Label or ttk.Label, object):
|
||||
|
||||
|
||||
def openurl(url):
|
||||
if platform == 'win32':
|
||||
if sys.platform == 'win32':
|
||||
# On Windows webbrowser.open calls os.startfile which calls ShellExecute which can't handle long arguments,
|
||||
# so discover and launch the browser directly.
|
||||
# https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553
|
||||
|
||||
try:
|
||||
hkey = OpenKeyEx(HKEY_CURRENT_USER, r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice')
|
||||
hkey = OpenKeyEx(HKEY_CURRENT_USER,
|
||||
r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice')
|
||||
(value, typ) = QueryValueEx(hkey, 'ProgId')
|
||||
CloseKey(hkey)
|
||||
if value in ['IE.HTTP', 'AppXq0fevzme2pys62n3e0fbqa7peapykr8v']:
|
||||
@ -128,7 +131,7 @@ def openurl(url):
|
||||
else:
|
||||
cls = value
|
||||
except:
|
||||
cls = 'https'
|
||||
cls = 'https'
|
||||
|
||||
if cls:
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user