mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-06-15 06:42:15 +03:00
Support dynamic plugin install/enable/disable
This commit is contained in:
parent
2c4e409ca3
commit
f91454c56a
@ -441,6 +441,7 @@ class AppWindow:
|
|||||||
self.minimizing = False
|
self.minimizing = False
|
||||||
self.w.rowconfigure(0, weight=1)
|
self.w.rowconfigure(0, weight=1)
|
||||||
self.w.columnconfigure(0, weight=1)
|
self.w.columnconfigure(0, weight=1)
|
||||||
|
self.plugin_id = 0
|
||||||
|
|
||||||
# companion needs to be able to send <<CAPIResponse>> events
|
# companion needs to be able to send <<CAPIResponse>> events
|
||||||
companion.session.set_tk_master(self.w)
|
companion.session.set_tk_master(self.w)
|
||||||
@ -478,6 +479,7 @@ class AppWindow:
|
|||||||
frame = tk.Frame(self.w, name=appname.lower())
|
frame = tk.Frame(self.w, name=appname.lower())
|
||||||
frame.grid(sticky=tk.NSEW)
|
frame.grid(sticky=tk.NSEW)
|
||||||
frame.columnconfigure(1, weight=1)
|
frame.columnconfigure(1, weight=1)
|
||||||
|
self.frame = frame
|
||||||
|
|
||||||
self.cmdr_label = tk.Label(frame, name='cmdr_label')
|
self.cmdr_label = tk.Label(frame, name='cmdr_label')
|
||||||
self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr')
|
self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr')
|
||||||
@ -517,38 +519,11 @@ class AppWindow:
|
|||||||
self.station.grid(row=ui_row, column=1, sticky=tk.EW)
|
self.station.grid(row=ui_row, column=1, sticky=tk.EW)
|
||||||
ui_row += 1
|
ui_row += 1
|
||||||
|
|
||||||
plugin_no = 0
|
self.all_plugin_frame = tk.Frame(frame, name=f"all_plugin_frame")
|
||||||
|
self.all_plugin_frame.grid(row=ui_row, columnspan=2, sticky=tk.NSEW)
|
||||||
|
self.insert_no_plugins_label()
|
||||||
for plugin in plug.PLUGINS:
|
for plugin in plug.PLUGINS:
|
||||||
# Per plugin separator
|
self.show_plugin(plugin)
|
||||||
plugin_sep = tk.Frame(
|
|
||||||
frame, highlightthickness=1, name=f"plugin_hr_{plugin_no + 1}"
|
|
||||||
)
|
|
||||||
# Per plugin frame, for it to use as its parent for own widgets
|
|
||||||
plugin_frame = tk.Frame(
|
|
||||||
frame,
|
|
||||||
name=f"plugin_{plugin_no + 1}"
|
|
||||||
)
|
|
||||||
appitem = plugin.get_app(plugin_frame)
|
|
||||||
if appitem:
|
|
||||||
plugin_no += 1
|
|
||||||
plugin_sep.grid(columnspan=2, sticky=tk.EW)
|
|
||||||
ui_row = frame.grid_size()[1]
|
|
||||||
plugin_frame.grid(
|
|
||||||
row=ui_row, columnspan=2, sticky=tk.NSEW
|
|
||||||
)
|
|
||||||
plugin_frame.columnconfigure(1, weight=1)
|
|
||||||
if isinstance(appitem, tuple) and len(appitem) == 2:
|
|
||||||
ui_row = frame.grid_size()[1]
|
|
||||||
appitem[0].grid(row=ui_row, column=0, sticky=tk.W)
|
|
||||||
appitem[1].grid(row=ui_row, column=1, sticky=tk.EW)
|
|
||||||
|
|
||||||
else:
|
|
||||||
appitem.grid(columnspan=2, sticky=tk.EW)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# This plugin didn't provide any UI, so drop the frames
|
|
||||||
plugin_frame.destroy()
|
|
||||||
plugin_sep.destroy()
|
|
||||||
|
|
||||||
# LANG: Update button in main window
|
# LANG: Update button in main window
|
||||||
self.button = ttk.Button(
|
self.button = ttk.Button(
|
||||||
@ -599,7 +574,7 @@ class AppWindow:
|
|||||||
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
|
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
|
||||||
self.file_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status))
|
self.file_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status))
|
||||||
self.file_menu.add_command(command=self.save_raw)
|
self.file_menu.add_command(command=self.save_raw)
|
||||||
self.file_menu.add_command(command=lambda: prefs.PreferencesDialog(self.w, self.postprefs))
|
self.file_menu.add_command(command=lambda: prefs.PreferencesDialog(self, self.w, self.postprefs))
|
||||||
self.file_menu.add_separator()
|
self.file_menu.add_separator()
|
||||||
self.file_menu.add_command(command=self.onexit)
|
self.file_menu.add_command(command=self.onexit)
|
||||||
self.menubar.add_cascade(menu=self.file_menu)
|
self.menubar.add_cascade(menu=self.file_menu)
|
||||||
@ -753,6 +728,69 @@ class AppWindow:
|
|||||||
else:
|
else:
|
||||||
self.w.wm_iconify()
|
self.w.wm_iconify()
|
||||||
|
|
||||||
|
def show_plugin(self, plugin: plug.Plugin):
|
||||||
|
self.remove_no_plugins_label()
|
||||||
|
plugin_id = self.plugin_id + 1
|
||||||
|
# Per plugin separator
|
||||||
|
plugin_sep = tk.Frame(
|
||||||
|
self.all_plugin_frame, highlightthickness=1, name=f"plugin_hr_{plugin_id}"
|
||||||
|
)
|
||||||
|
# Per plugin frame, for it to use as its parent for own widgets
|
||||||
|
plugin_frame = tk.Frame(self.all_plugin_frame, name=f"plugin_{plugin_id}")
|
||||||
|
appitem = plugin.get_app(plugin_frame)
|
||||||
|
if appitem:
|
||||||
|
self.plugin_id = plugin_id
|
||||||
|
plugin.frame_id = plugin_id
|
||||||
|
ui_row = self.all_plugin_frame.grid_size()[1]
|
||||||
|
plugin_sep.grid(row=ui_row, columnspan=2, sticky=tk.EW)
|
||||||
|
ui_row = self.all_plugin_frame.grid_size()[1]
|
||||||
|
plugin_frame.grid(row=ui_row, columnspan=2, sticky=tk.NSEW)
|
||||||
|
plugin_frame.columnconfigure(1, weight=1)
|
||||||
|
if isinstance(appitem, tuple) and len(appitem) == 2:
|
||||||
|
ui_row = self.all_plugin_frame.grid_size()[1]
|
||||||
|
appitem[0].grid(row=ui_row, column=0, sticky=tk.W)
|
||||||
|
appitem[1].grid(row=ui_row, column=1, sticky=tk.EW)
|
||||||
|
else:
|
||||||
|
appitem.grid(row=ui_row, columnspan=2, sticky=tk.EW)
|
||||||
|
else:
|
||||||
|
# This plugin didn't provide any UI, so drop the frames
|
||||||
|
plugin_frame.destroy()
|
||||||
|
plugin_sep.destroy()
|
||||||
|
self.insert_no_plugins_label()
|
||||||
|
|
||||||
|
def hide_plugin(self, plugin: plug.Plugin):
|
||||||
|
if plugin and plugin.frame_id > 0:
|
||||||
|
if widget := self.all_plugin_frame.nametowidget(f"plugin_hr_{plugin.frame_id}"):
|
||||||
|
widget.grid_forget()
|
||||||
|
widget.destroy()
|
||||||
|
if widget := self.all_plugin_frame.nametowidget(f"plugin_{plugin.frame_id}"):
|
||||||
|
widget.grid_forget()
|
||||||
|
widget.destroy()
|
||||||
|
plugin.frame_id = 0
|
||||||
|
self.insert_no_plugins_label()
|
||||||
|
self.frame.pack()
|
||||||
|
|
||||||
|
def remove_no_plugins_label(self):
|
||||||
|
try:
|
||||||
|
if label := self.all_plugin_frame.nametowidget("no_plugins_label"):
|
||||||
|
label.grid_forget()
|
||||||
|
label.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_no_plugins_label(self):
|
||||||
|
try:
|
||||||
|
if self.all_plugin_frame.nametowidget("no_plugins_label"):
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
children = self.all_plugin_frame.winfo_children()
|
||||||
|
if len(children) == 0:
|
||||||
|
label = tk.Label(self.all_plugin_frame, text="No plugins", name="no_plugins_label", anchor=tk.CENTER)
|
||||||
|
pady = 2 if sys.platform != 'win32' else 0
|
||||||
|
label.grid(row=0, columnspan=2, sticky=tk.NSEW, padx=self.PADX, pady=pady)
|
||||||
|
return
|
||||||
|
|
||||||
def update_suit_text(self) -> None:
|
def update_suit_text(self) -> None:
|
||||||
"""Update the suit text for current type and loadout."""
|
"""Update the suit text for current type and loadout."""
|
||||||
if not monitor.state['Odyssey']:
|
if not monitor.state['Odyssey']:
|
||||||
@ -2261,7 +2299,7 @@ sys.path: {sys.path}'''
|
|||||||
|
|
||||||
def messagebox_broken_plugins():
|
def messagebox_broken_plugins():
|
||||||
"""Display message about 'broken' plugins that failed to load."""
|
"""Display message about 'broken' plugins that failed to load."""
|
||||||
if plug.PLUGINS_broken:
|
if [x for x in plug.PLUGINS if x.broken]:
|
||||||
# LANG: Popup-text about 'broken' plugins that failed to load
|
# LANG: Popup-text about 'broken' plugins that failed to load
|
||||||
popup_text = tr.tl(
|
popup_text = tr.tl(
|
||||||
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' "
|
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' "
|
||||||
@ -2288,38 +2326,6 @@ sys.path: {sys.path}'''
|
|||||||
parent=root
|
parent=root
|
||||||
)
|
)
|
||||||
|
|
||||||
def messagebox_not_py3():
|
|
||||||
"""Display message about plugins not updated for Python 3.x."""
|
|
||||||
plugins_not_py3_last = config.get_int('plugins_not_py3_last', default=0)
|
|
||||||
if (plugins_not_py3_last + 86400) < int(time()) and plug.PLUGINS_not_py3:
|
|
||||||
# LANG: Popup-text about 'active' plugins without Python 3.x support
|
|
||||||
popup_text = tr.tl(
|
|
||||||
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the "
|
|
||||||
"list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an "
|
|
||||||
"updated version available, else alert the developer that they need to update the code for "
|
|
||||||
r"Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on "
|
|
||||||
"the end of the name."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Substitute in the other words.
|
|
||||||
popup_text = popup_text.format(
|
|
||||||
PLUGINS=tr.tl('Plugins'), # LANG: Settings > Plugins tab
|
|
||||||
FILE=tr.tl('File'), # LANG: 'File' menu
|
|
||||||
SETTINGS=tr.tl('Settings'), # LANG: File > Settings
|
|
||||||
DISABLED='.disabled'
|
|
||||||
)
|
|
||||||
# And now we do need these to be actual \r\n
|
|
||||||
popup_text = popup_text.replace('\\n', '\n')
|
|
||||||
popup_text = popup_text.replace('\\r', '\r')
|
|
||||||
|
|
||||||
tk.messagebox.showinfo(
|
|
||||||
# LANG: Popup window title for list of 'enabled' plugins that don't work with Python 3.x
|
|
||||||
tr.tl('EDMC: Plugins Without Python 3.x Support'),
|
|
||||||
popup_text,
|
|
||||||
parent=root
|
|
||||||
)
|
|
||||||
config.set('plugins_not_py3_last', int(time()))
|
|
||||||
|
|
||||||
# UI Transparency
|
# UI Transparency
|
||||||
ui_transparency = config.get_int('ui_transparency')
|
ui_transparency = config.get_int('ui_transparency')
|
||||||
if ui_transparency == 0:
|
if ui_transparency == 0:
|
||||||
@ -2328,8 +2334,6 @@ sys.path: {sys.path}'''
|
|||||||
root.wm_attributes('-alpha', ui_transparency / 100)
|
root.wm_attributes('-alpha', ui_transparency / 100)
|
||||||
# Display message box about plugins that failed to load
|
# Display message box about plugins that failed to load
|
||||||
root.after(0, messagebox_broken_plugins)
|
root.after(0, messagebox_broken_plugins)
|
||||||
# Display message box about plugins without Python 3.x support
|
|
||||||
root.after(1, messagebox_not_py3)
|
|
||||||
# Show warning popup for killswitches matching current version
|
# Show warning popup for killswitches matching current version
|
||||||
root.after(2, show_killswitch_poppup, root)
|
root.after(2, show_killswitch_poppup, root)
|
||||||
# Start the main event loop
|
# Start the main event loop
|
||||||
|
120
plug.py
120
plug.py
@ -28,8 +28,6 @@ logger = get_main_logger()
|
|||||||
|
|
||||||
# List of loaded Plugins
|
# List of loaded Plugins
|
||||||
PLUGINS = []
|
PLUGINS = []
|
||||||
PLUGINS_not_py3 = []
|
|
||||||
PLUGINS_broken = []
|
|
||||||
|
|
||||||
|
|
||||||
# For asynchronous error display
|
# For asynchronous error display
|
||||||
@ -49,7 +47,7 @@ last_error = LastError()
|
|||||||
class Plugin:
|
class Plugin:
|
||||||
"""An EDMC plugin."""
|
"""An EDMC plugin."""
|
||||||
|
|
||||||
def __init__(self, name: str, loadfile: Path | None, plugin_logger: logging.Logger | None): # noqa: CCR001
|
def __init__(self, name: str, folder: str, loadfile: Path | None, plugin_logger: logging.Logger | None): # noqa: CCR001
|
||||||
"""
|
"""
|
||||||
Load a single plugin.
|
Load a single plugin.
|
||||||
|
|
||||||
@ -59,9 +57,11 @@ class Plugin:
|
|||||||
:raises Exception: Typically, ImportError or OSError
|
:raises Exception: Typically, ImportError or OSError
|
||||||
"""
|
"""
|
||||||
self.name: str = name # Display name.
|
self.name: str = name # Display name.
|
||||||
self.folder: str | None = name # basename of plugin folder. None for internal plugins.
|
self.folder: str | None = folder # basename of plugin folder. None for internal plugins.
|
||||||
self.module = None # None for disabled plugins.
|
self.module = None # None for disabled plugins.
|
||||||
self.logger: logging.Logger | None = plugin_logger
|
self.logger: logging.Logger | None = plugin_logger
|
||||||
|
self.frame_id = 0 # Not 0 is shown in main window
|
||||||
|
self.broken: str = None # Error message
|
||||||
|
|
||||||
if loadfile:
|
if loadfile:
|
||||||
logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"')
|
logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"')
|
||||||
@ -78,14 +78,18 @@ class Plugin:
|
|||||||
newname = module.plugin_start3(Path(loadfile).resolve().parent)
|
newname = module.plugin_start3(Path(loadfile).resolve().parent)
|
||||||
self.name = str(newname) if newname else self.name
|
self.name = str(newname) if newname else self.name
|
||||||
self.module = module
|
self.module = module
|
||||||
|
self.broken = None
|
||||||
elif getattr(module, 'plugin_start', None):
|
elif getattr(module, 'plugin_start', None):
|
||||||
|
self.broken = f'plugin {name} needs migrating'
|
||||||
logger.warning(f'plugin {name} needs migrating\n')
|
logger.warning(f'plugin {name} needs migrating\n')
|
||||||
PLUGINS_not_py3.append(self)
|
|
||||||
else:
|
else:
|
||||||
|
self.broken = 'needs python 3 migration'
|
||||||
logger.error(f'plugin {name} has no plugin_start3() function')
|
logger.error(f'plugin {name} has no plugin_start3() function')
|
||||||
else:
|
else:
|
||||||
|
self.broken = 'Failed to load from load.py'
|
||||||
logger.error(f'Failed to load Plugin "{name}" from file "{loadfile}"')
|
logger.error(f'Failed to load Plugin "{name}" from file "{loadfile}"')
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
self.broken = f'Error: {e}'
|
||||||
logger.exception(f': Failed for Plugin "{name}"')
|
logger.exception(f': Failed for Plugin "{name}"')
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
@ -164,8 +168,7 @@ def load_plugins(master: tk.Tk) -> None:
|
|||||||
# Add plugin folder to load path so packages can be loaded from plugin folder
|
# Add plugin folder to load path so packages can be loaded from plugin folder
|
||||||
sys.path.append(config.plugin_dir)
|
sys.path.append(config.plugin_dir)
|
||||||
|
|
||||||
found = _load_found_plugins()
|
sync_all_plugins()
|
||||||
PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower()))
|
|
||||||
|
|
||||||
|
|
||||||
def _load_internal_plugins():
|
def _load_internal_plugins():
|
||||||
@ -175,16 +178,42 @@ def _load_internal_plugins():
|
|||||||
try:
|
try:
|
||||||
plugin_name = name[:-3]
|
plugin_name = name[:-3]
|
||||||
plugin_path = config.internal_plugin_dir_path / name
|
plugin_path = config.internal_plugin_dir_path / name
|
||||||
plugin = Plugin(plugin_name, plugin_path, logger)
|
plugin = Plugin(plugin_name, None, plugin_path, logger)
|
||||||
plugin.folder = None
|
|
||||||
internal.append(plugin)
|
internal.append(plugin)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(f'Failure loading internal Plugin "{name}"')
|
logger.exception(f'Failure loading internal Plugin "{name}"')
|
||||||
return internal
|
return internal
|
||||||
|
|
||||||
|
|
||||||
def _load_found_plugins():
|
def load_plugin(folder: str):
|
||||||
found = []
|
if not (config.plugin_dir_path / folder).is_dir() or folder.startswith(('.', '_')):
|
||||||
|
return None
|
||||||
|
if folder.endswith('.disabled'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
disabled_list = config.get_list('disabled_plugins', default=[])
|
||||||
|
if disabled_list.count(folder) > 0:
|
||||||
|
plugin = Plugin(folder, folder, None, logger)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Add plugin's folder to load path in case plugin has internal package dependencies
|
||||||
|
sys.path.append(str(config.plugin_dir_path / folder))
|
||||||
|
import EDMCLogging
|
||||||
|
# Create a logger for this 'found' plugin. Must be before the load.py is loaded.
|
||||||
|
plugin_logger = EDMCLogging.get_plugin_logger(folder)
|
||||||
|
plugin = Plugin(folder, folder, config.plugin_dir_path / folder / 'load.py', plugin_logger)
|
||||||
|
except Exception as e:
|
||||||
|
plugin = Plugin(folder, folder, None, logger)
|
||||||
|
plugin.broken = f'Error: {e}'
|
||||||
|
logger.exception(f'Failure loading found Plugin "{folder}"')
|
||||||
|
old = get_plugin_by_folder(folder)
|
||||||
|
if old:
|
||||||
|
disable_plugin(old, False)
|
||||||
|
PLUGINS.remove(old)
|
||||||
|
PLUGINS.append(plugin)
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
def sync_all_plugins():
|
||||||
# Load any plugins that are also packages first, but note it's *still*
|
# Load any plugins that are also packages first, but note it's *still*
|
||||||
# 100% relying on there being a `load.py`, as only that will be loaded.
|
# 100% relying on there being a `load.py`, as only that will be loaded.
|
||||||
# The intent here is to e.g. have EDMC-Overlay load before any plugins
|
# The intent here is to e.g. have EDMC-Overlay load before any plugins
|
||||||
@ -194,27 +223,14 @@ def _load_found_plugins():
|
|||||||
not (p / '__init__.py').is_file(), p.name.lower()))
|
not (p / '__init__.py').is_file(), p.name.lower()))
|
||||||
|
|
||||||
for plugin_file in plugin_files:
|
for plugin_file in plugin_files:
|
||||||
name = plugin_file.name
|
if not get_plugin_by_folder(plugin_file.name):
|
||||||
if not (config.plugin_dir_path / name).is_dir() or name.startswith(('.', '_')):
|
load_plugin(plugin_file.name)
|
||||||
pass
|
|
||||||
elif name.endswith('.disabled'):
|
|
||||||
name, discard = name.rsplit('.', 1)
|
|
||||||
found.append(Plugin(name, None, logger))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
# Add plugin's folder to load path in case plugin has internal package dependencies
|
|
||||||
sys.path.append(str(config.plugin_dir_path / name))
|
|
||||||
|
|
||||||
import EDMCLogging
|
|
||||||
# Create a logger for this 'found' plugin. Must be before the load.py is loaded.
|
|
||||||
plugin_logger = EDMCLogging.get_plugin_logger(name)
|
|
||||||
found.append(Plugin(name, config.plugin_dir_path / name / 'load.py', plugin_logger))
|
|
||||||
except Exception:
|
|
||||||
PLUGINS_broken.append(Plugin(name, None, logger))
|
|
||||||
logger.exception(f'Failure loading found Plugin "{name}"')
|
|
||||||
pass
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
for plugin in list(PLUGINS):
|
||||||
|
if plugin.folder and not os.path.exists(os.path.join(config.plugin_dir_path, plugin.folder)):
|
||||||
|
disable_plugin(plugin, False)
|
||||||
|
plugin.broken = f'Missing folder: {plugin.folder}'
|
||||||
|
PLUGINS.remove(plugin)
|
||||||
|
|
||||||
def provides(fn_name: str) -> list[str]:
|
def provides(fn_name: str) -> list[str]:
|
||||||
"""
|
"""
|
||||||
@ -381,7 +397,7 @@ def notify_journal_entry_cqc(
|
|||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
def notify_dashboard_entry(cmdr: str, is_beta: bool, entry: MutableMapping[str, Any],) -> str | None:
|
def notify_dashboard_entry(cmdr: str, is_beta: bool, entry: MutableMapping[str, Any], ) -> str | None:
|
||||||
"""
|
"""
|
||||||
Send a status entry to each plugin.
|
Send a status entry to each plugin.
|
||||||
|
|
||||||
@ -468,3 +484,41 @@ def show_error(err: str) -> None:
|
|||||||
if err and last_error.root:
|
if err and last_error.root:
|
||||||
last_error.msg = str(err)
|
last_error.msg = str(err)
|
||||||
last_error.root.event_generate('<<PluginError>>', when="tail")
|
last_error.root.event_generate('<<PluginError>>', when="tail")
|
||||||
|
|
||||||
|
|
||||||
|
def enable_plugin(plugin: Plugin, add_to_config = True) -> Plugin:
|
||||||
|
if add_to_config:
|
||||||
|
disabled_list = config.get_list('disabled_plugins', default=[])
|
||||||
|
if disabled_list.count(plugin.folder) > 0:
|
||||||
|
disabled_list.remove(plugin.folder)
|
||||||
|
config.set('disabled_plugins', disabled_list)
|
||||||
|
if loaded := load_plugin(plugin.folder):
|
||||||
|
if PLUGINS.count(plugin):
|
||||||
|
PLUGINS.remove(plugin)
|
||||||
|
PLUGINS.append(loaded)
|
||||||
|
return loaded
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
|
def disable_plugin(plugin: Plugin, add_to_config = True) -> Plugin:
|
||||||
|
if add_to_config:
|
||||||
|
disabled_list = config.get_list('disabled_plugins', default=[])
|
||||||
|
if disabled_list.count(plugin.folder) == 0:
|
||||||
|
disabled_list.append(plugin.folder)
|
||||||
|
config.set('disabled_plugins', disabled_list)
|
||||||
|
if plugin.module:
|
||||||
|
plugin_stop = plugin._get_func('plugin_stop')
|
||||||
|
if plugin_stop:
|
||||||
|
try:
|
||||||
|
logger.info(f'Asking plugin "{plugin.name}" to stop...')
|
||||||
|
plugin_stop()
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f'Plugin "{plugin.name}" failed')
|
||||||
|
plugin.module = None
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
def get_plugin_by_folder(folder: str) -> Plugin | None:
|
||||||
|
for plugin in PLUGINS:
|
||||||
|
if plugin.folder == folder:
|
||||||
|
return plugin
|
||||||
|
return None
|
||||||
|
257
prefs.py
257
prefs.py
@ -4,6 +4,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
import os.path
|
||||||
|
import zipfile
|
||||||
from os.path import expandvars, join, normpath
|
from os.path import expandvars, join, normpath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -12,7 +14,7 @@ import tkinter as tk
|
|||||||
import warnings
|
import warnings
|
||||||
from os import system
|
from os import system
|
||||||
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
|
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
|
||||||
from tkinter import ttk
|
from tkinter import ttk, messagebox
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, Callable, Optional, Type
|
from typing import Any, Callable, Optional, Type
|
||||||
import myNotebook as nb # noqa: N813
|
import myNotebook as nb # noqa: N813
|
||||||
@ -228,9 +230,10 @@ if sys.platform == 'win32':
|
|||||||
class PreferencesDialog(tk.Toplevel):
|
class PreferencesDialog(tk.Toplevel):
|
||||||
"""The EDMC preferences dialog."""
|
"""The EDMC preferences dialog."""
|
||||||
|
|
||||||
def __init__(self, parent: tk.Tk, callback: Optional[Callable]):
|
def __init__(self, app, parent: tk.Tk, callback: Optional[Callable]):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.theApp = app
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.req_restart = False
|
self.req_restart = False
|
||||||
@ -265,6 +268,7 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
|
|
||||||
notebook: nb.Notebook = nb.Notebook(frame)
|
notebook: nb.Notebook = nb.Notebook(frame)
|
||||||
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
|
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
|
||||||
|
self.notebook = notebook
|
||||||
|
|
||||||
self.PADX = 10
|
self.PADX = 10
|
||||||
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||||||
@ -409,6 +413,7 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
for plugin in plug.PLUGINS:
|
for plugin in plug.PLUGINS:
|
||||||
plugin_frame = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta)
|
plugin_frame = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta)
|
||||||
if plugin_frame:
|
if plugin_frame:
|
||||||
|
plugin.notebook_frame = plugin_frame
|
||||||
notebook.add(plugin_frame, text=plugin.name)
|
notebook.add(plugin_frame, text=plugin.name)
|
||||||
|
|
||||||
def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001
|
def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001
|
||||||
@ -905,7 +910,10 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001
|
def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001
|
||||||
# Plugin settings and info
|
# Plugin settings and info
|
||||||
plugins_frame = nb.Frame(notebook)
|
plugins_frame = nb.Frame(notebook)
|
||||||
plugins_frame.columnconfigure(0, weight=1)
|
plugins_frame.columnconfigure(0, weight=2)
|
||||||
|
plugins_frame.columnconfigure(1, weight=1)
|
||||||
|
plugins_frame.columnconfigure(2, weight=1)
|
||||||
|
plugins_frame.columnconfigure(3, weight=1)
|
||||||
row = AutoInc(start=0)
|
row = AutoInc(start=0)
|
||||||
self.plugdir = tk.StringVar()
|
self.plugdir = tk.StringVar()
|
||||||
self.plugdir.set(str(config.get_str('plugin_dir')))
|
self.plugdir.set(str(config.get_str('plugin_dir')))
|
||||||
@ -916,13 +924,14 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
textvariable=self.plugdir) # Link StringVar to Entry widget
|
textvariable=self.plugdir) # Link StringVar to Entry widget
|
||||||
self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
|
self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
|
||||||
with row as cur_row:
|
with row as cur_row:
|
||||||
nb.Label(
|
# Install Plugin Button
|
||||||
|
self.install_plugin_btn = ttk.Button(
|
||||||
plugins_frame,
|
plugins_frame,
|
||||||
# Help text in settings
|
# LANG: Label on button used to Install Plugin
|
||||||
# LANG: Tip/label about how to disable plugins
|
text=tr.tl('Install Plugin'),
|
||||||
text=tr.tl(
|
command=lambda frame=plugins_frame: self.installbrowse(config.plugin_dir_path, frame)
|
||||||
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')
|
)
|
||||||
).grid(columnspan=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
self.install_plugin_btn.grid(column=0, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||||
|
|
||||||
# Open Plugin Folder Button
|
# Open Plugin Folder Button
|
||||||
self.open_plug_folder_btn = ttk.Button(
|
self.open_plug_folder_btn = ttk.Button(
|
||||||
@ -953,90 +962,110 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
state=tk.NORMAL if config.get_str('plugin_dir') else tk.DISABLED
|
state=tk.NORMAL if config.get_str('plugin_dir') else tk.DISABLED
|
||||||
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
).grid(column=3, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||||
|
|
||||||
enabled_plugins = list(filter(lambda x: x.folder and x.module, plug.PLUGINS))
|
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
||||||
if enabled_plugins:
|
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
||||||
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
)
|
||||||
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
nb.Label(
|
||||||
)
|
plugins_frame,
|
||||||
nb.Label(
|
# LANG: Label on list of enabled plugins
|
||||||
plugins_frame,
|
text=tr.tl('Enabled Plugins')+':' # List of plugins in settings
|
||||||
# LANG: Label on list of enabled plugins
|
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
||||||
text=tr.tl('Enabled Plugins')+':' # List of plugins in settings
|
|
||||||
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
|
||||||
|
|
||||||
for plugin in enabled_plugins:
|
self.sync_all_plugin_rows(plugins_frame)
|
||||||
if plugin.name == plugin.folder:
|
|
||||||
label = nb.Label(plugins_frame, text=plugin.name)
|
|
||||||
|
|
||||||
else:
|
|
||||||
label = nb.Label(plugins_frame, text=f'{plugin.folder} ({plugin.name})')
|
|
||||||
|
|
||||||
label.grid(columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get())
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# Show which plugins don't have Python 3.x support
|
|
||||||
############################################################
|
|
||||||
if plug.PLUGINS_not_py3:
|
|
||||||
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
|
||||||
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
|
||||||
)
|
|
||||||
# LANG: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x
|
|
||||||
nb.Label(plugins_frame, text=tr.tl('Plugins Without Python 3.x Support')+':').grid(
|
|
||||||
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
HyperlinkLabel(
|
|
||||||
# LANG: Plugins - Label on URL to documentation about migrating plugins from Python 2.7
|
|
||||||
plugins_frame, text=tr.tl('Information on migrating plugins'),
|
|
||||||
background=nb.Label().cget('background'),
|
|
||||||
url='https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#migration-from-python-27',
|
|
||||||
underline=True
|
|
||||||
).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
|
||||||
|
|
||||||
for plugin in plug.PLUGINS_not_py3:
|
|
||||||
if plugin.folder: # 'system' ones have this set to None to suppress listing in Plugins prefs tab
|
|
||||||
nb.Label(plugins_frame, text=plugin.name).grid(
|
|
||||||
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
|
|
||||||
)
|
|
||||||
############################################################
|
|
||||||
# Show disabled plugins
|
|
||||||
############################################################
|
|
||||||
disabled_plugins = list(filter(lambda x: x.folder and not x.module, plug.PLUGINS))
|
|
||||||
if disabled_plugins:
|
|
||||||
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
|
||||||
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
|
||||||
)
|
|
||||||
nb.Label(
|
|
||||||
plugins_frame,
|
|
||||||
# LANG: Label on list of user-disabled plugins
|
|
||||||
text=tr.tl('Disabled Plugins')+':' # List of plugins in settings
|
|
||||||
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
|
||||||
|
|
||||||
for plugin in disabled_plugins:
|
|
||||||
nb.Label(plugins_frame, text=plugin.name).grid(
|
|
||||||
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
|
|
||||||
)
|
|
||||||
############################################################
|
|
||||||
# Show plugins that failed to load
|
|
||||||
############################################################
|
|
||||||
if plug.PLUGINS_broken:
|
|
||||||
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
|
||||||
columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
|
||||||
)
|
|
||||||
# LANG: Plugins - Label for list of 'broken' plugins that failed to load
|
|
||||||
nb.Label(plugins_frame, text=tr.tl('Broken Plugins')+':').grid(
|
|
||||||
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
for plugin in plug.PLUGINS_broken:
|
|
||||||
if plugin.folder: # 'system' ones have this set to None to suppress listing in Plugins prefs tab
|
|
||||||
nb.Label(plugins_frame, text=plugin.name).grid(
|
|
||||||
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
# LANG: Label on Settings > Plugins tab
|
# LANG: Label on Settings > Plugins tab
|
||||||
notebook.add(plugins_frame, text=tr.tl('Plugins')) # Tab heading in settings
|
notebook.add(plugins_frame, text=tr.tl('Plugins')) # Tab heading in settings
|
||||||
|
|
||||||
|
def _cleanup_plugin_widgets(self, plugins_frame: tk.Frame):
|
||||||
|
# remove rows for plugins that were deleted or duplicated rows
|
||||||
|
configured = {}
|
||||||
|
for widget in list(plugins_frame.children.values()):
|
||||||
|
if hasattr(widget,"plugin_folder"):
|
||||||
|
folder = widget.plugin_folder
|
||||||
|
if not plug.get_plugin_by_folder(folder):
|
||||||
|
widget.grid_forget()
|
||||||
|
widget.destroy()
|
||||||
|
continue
|
||||||
|
#ui_row = widget.grid_info()["row"]
|
||||||
|
#if not configured.get(folder):
|
||||||
|
# configured[folder] = ui_row
|
||||||
|
# continue
|
||||||
|
#if configured.get(folder) == ui_row:
|
||||||
|
# continue
|
||||||
|
#widget.grid_forget()
|
||||||
|
#widget.destroy()
|
||||||
|
|
||||||
|
def sync_all_plugin_rows(self, plugins_frame: tk.Frame):
|
||||||
|
self._cleanup_plugin_widgets(plugins_frame)
|
||||||
|
user_plugins = list(filter(lambda p: p.folder, plug.PLUGINS))
|
||||||
|
for plugin in user_plugins:
|
||||||
|
found = False
|
||||||
|
for widget in plugins_frame.children.values():
|
||||||
|
if isinstance(widget, nb.Checkbutton) and hasattr(widget, "plugin_folder"):
|
||||||
|
cb: nb.Checkbutton = widget
|
||||||
|
if cb.plugin_folder == plugin.folder:
|
||||||
|
found = True
|
||||||
|
self._update_plugin_state(plugin, cb)
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
cb = nb.Checkbutton(plugins_frame)
|
||||||
|
cb.plugin_folder = plugin.folder
|
||||||
|
ui_row = plugins_frame.grid_size()[1]
|
||||||
|
cb.grid(column=0, row=ui_row, padx=self.PADX, pady=self.PADY, sticky=tk.W)
|
||||||
|
self._update_plugin_state(plugin, cb)
|
||||||
|
|
||||||
|
def _update_plugin_state(self, plugin: plug.Plugin, cb: nb.Checkbutton):
|
||||||
|
ui_row = cb.grid_info()["row"]
|
||||||
|
if not plugin.name == plugin.folder and not cb.master.grid_slaves(ui_row, 1):
|
||||||
|
label = tk.Label(cb.master, text=plugin.folder+"/")
|
||||||
|
label.grid(column=1, row=ui_row, padx=self.PADX, pady=self.PADY, sticky=tk.W)
|
||||||
|
label.plugin_folder = plugin.folder
|
||||||
|
status = next(iter(cb.master.grid_slaves(ui_row, 2)),None)
|
||||||
|
if not status:
|
||||||
|
status = tk.Label(cb.master)
|
||||||
|
status.grid(column=2, row=ui_row, padx=self.PADX, pady=self.PADY, sticky=tk.W)
|
||||||
|
status.plugin_folder = plugin.folder
|
||||||
|
state = tk.NORMAL
|
||||||
|
if plugin.folder:
|
||||||
|
if not hasattr(cb, "cb_var"):
|
||||||
|
cb.cb_var = tk.IntVar()
|
||||||
|
cb.configure(variable=cb.cb_var)
|
||||||
|
cb.cb_var.set(1 if plugin.module else 0)
|
||||||
|
if plugin.folder.endswith('.disabled'):
|
||||||
|
state = tk.DISABLED
|
||||||
|
if plugin.broken:
|
||||||
|
state = tk.DISABLED
|
||||||
|
cb.configure(text=plugin.name, state=state, command=None)
|
||||||
|
status.configure(text=plugin.broken, fg="red")
|
||||||
|
elif state == tk.NORMAL and plugin.module:
|
||||||
|
cb.configure(text=plugin.name, state=state, command=lambda p=plugin,c=cb: self._disable_plugin(p,c))
|
||||||
|
status.configure(text="running", fg="green")
|
||||||
|
else:
|
||||||
|
cb.configure(text=plugin.name, state=state, command=lambda p=plugin,c=cb: self._enable_plugin(p,c))
|
||||||
|
status.configure(text="disabled", fg="gray")
|
||||||
|
|
||||||
|
def _disable_plugin(self, plugin: plug.Plugin, cb: nb.Checkbutton|None, add_to_config:bool=True):
|
||||||
|
self.theApp.hide_plugin(plugin)
|
||||||
|
if plugin.module:
|
||||||
|
plugin = plug.disable_plugin(plugin, add_to_config)
|
||||||
|
if hasattr(plugin, "notebook_frame"):
|
||||||
|
self.notebook.forget(plugin.notebook_frame)
|
||||||
|
plugin.notebook_frame.destroy()
|
||||||
|
plugin.notebook_frame = None
|
||||||
|
if cb:
|
||||||
|
self._update_plugin_state(plugin, cb)
|
||||||
|
|
||||||
|
def _enable_plugin(self, plugin: plug.Plugin, cb: nb.Checkbutton|None, add_to_config:bool=True):
|
||||||
|
plugin = plug.enable_plugin(plugin,add_to_config)
|
||||||
|
self.theApp.show_plugin(plugin)
|
||||||
|
plugin_frame = plugin.get_prefs(self.notebook, monitor.cmdr, monitor.is_beta)
|
||||||
|
if plugin_frame:
|
||||||
|
plugin.notebook_frame = plugin_frame
|
||||||
|
self.notebook.add(plugin_frame, text=plugin.name)
|
||||||
|
if cb:
|
||||||
|
self._update_plugin_state(plugin, cb)
|
||||||
|
|
||||||
|
|
||||||
def cmdrchanged(self, event=None):
|
def cmdrchanged(self, event=None):
|
||||||
"""
|
"""
|
||||||
Notify plugins of cmdr change.
|
Notify plugins of cmdr change.
|
||||||
@ -1087,6 +1116,58 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
pathvar.set(directory)
|
pathvar.set(directory)
|
||||||
self.outvarchanged()
|
self.outvarchanged()
|
||||||
|
|
||||||
|
def installbrowse(self, plugin_dir_path: str, plugins_frame: nb.Frame):
|
||||||
|
import tkinter.filedialog
|
||||||
|
initialdir = Path(plugin_dir_path).expanduser()
|
||||||
|
file = tkinter.filedialog.askopenfilename(
|
||||||
|
parent=self,
|
||||||
|
initialdir=initialdir,
|
||||||
|
filetypes = (("Zip files", "*.zip"), ("All files", "*.*")),
|
||||||
|
title=tr.tl("Select plugin ZIP file")
|
||||||
|
)
|
||||||
|
user_plugins = list(filter(lambda p: p.folder, plug.PLUGINS))
|
||||||
|
disabled_list = config.get_list('disabled_plugins', default=[])
|
||||||
|
if file:
|
||||||
|
if not zipfile.is_zipfile(file):
|
||||||
|
messagebox.showwarning(
|
||||||
|
tr.tl('Error'), # LANG: Generic error prefix - following text is from Frontier auth service;
|
||||||
|
tr.tl('Invalid ZIP archive.'), # LANG: Invalid ZIP archive
|
||||||
|
parent=self.master
|
||||||
|
)
|
||||||
|
return
|
||||||
|
name = os.path.splitext(os.path.basename(file))[0]
|
||||||
|
with zipfile.ZipFile(file, 'r') as myzip:
|
||||||
|
has_load = "load.py" in myzip.NameToInfo
|
||||||
|
has_dir_load = name+"/load.py" in myzip.NameToInfo
|
||||||
|
if not (has_load or has_dir_load):
|
||||||
|
messagebox.showwarning(
|
||||||
|
tr.tl('Error'), # LANG: Generic error prefix - following text is from Frontier auth service;
|
||||||
|
tr.tl('Not a valid plugin ZIP archive'), # LANG: Not a valid plugin ZIP archive
|
||||||
|
parent=self.master
|
||||||
|
)
|
||||||
|
return
|
||||||
|
dest = os.path.join(initialdir, name)
|
||||||
|
old_plugin = plug.get_plugin_by_folder(name)
|
||||||
|
if old_plugin:
|
||||||
|
if not messagebox.askokcancel(
|
||||||
|
tr.tl('Error'), # LANG: Generic error prefix - following text is from Frontier auth service;
|
||||||
|
tr.tl('Plugin already installed. Upgrade?'), # LANG: Plugin already installed
|
||||||
|
parent=self.master
|
||||||
|
):
|
||||||
|
return
|
||||||
|
self._disable_plugin(old_plugin, None, False)
|
||||||
|
plug.PLUGINS.remove(old_plugin)
|
||||||
|
myzip.extractall(path=initialdir if has_dir_load else os.path.join(initialdir, name))
|
||||||
|
plug.sync_all_plugins()
|
||||||
|
for plugin in user_plugins:
|
||||||
|
if plugin.broken:
|
||||||
|
self._disable_plugin(plugin, None, False)
|
||||||
|
user_plugins = list(filter(lambda p: p.folder, plug.PLUGINS))
|
||||||
|
for plugin in user_plugins:
|
||||||
|
if not plugin.broken and plugin.frame_id == 0 and disabled_list.count(plugin.folder) == 0:
|
||||||
|
self._enable_plugin(plugin, None, False)
|
||||||
|
self.sync_all_plugin_rows(plugins_frame)
|
||||||
|
|
||||||
def displaypath(self, pathvar: tk.StringVar, entryfield: tk.Entry) -> None:
|
def displaypath(self, pathvar: tk.StringVar, entryfield: tk.Entry) -> None:
|
||||||
"""
|
"""
|
||||||
Display a path in a locked tk.Entry.
|
Display a path in a locked tk.Entry.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user