mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-21 11:27:38 +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.w.rowconfigure(0, weight=1)
|
||||
self.w.columnconfigure(0, weight=1)
|
||||
self.plugin_id = 0
|
||||
|
||||
# companion needs to be able to send <<CAPIResponse>> events
|
||||
companion.session.set_tk_master(self.w)
|
||||
@ -478,6 +479,7 @@ class AppWindow:
|
||||
frame = tk.Frame(self.w, name=appname.lower())
|
||||
frame.grid(sticky=tk.NSEW)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
self.frame = frame
|
||||
|
||||
self.cmdr_label = tk.Label(frame, name='cmdr_label')
|
||||
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)
|
||||
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:
|
||||
# Per plugin separator
|
||||
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()
|
||||
self.show_plugin(plugin)
|
||||
|
||||
# LANG: Update button in main window
|
||||
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.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=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_command(command=self.onexit)
|
||||
self.menubar.add_cascade(menu=self.file_menu)
|
||||
@ -753,6 +728,69 @@ class AppWindow:
|
||||
else:
|
||||
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:
|
||||
"""Update the suit text for current type and loadout."""
|
||||
if not monitor.state['Odyssey']:
|
||||
@ -2261,7 +2299,7 @@ sys.path: {sys.path}'''
|
||||
|
||||
def messagebox_broken_plugins():
|
||||
"""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
|
||||
popup_text = tr.tl(
|
||||
"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
|
||||
)
|
||||
|
||||
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 = config.get_int('ui_transparency')
|
||||
if ui_transparency == 0:
|
||||
@ -2328,8 +2334,6 @@ sys.path: {sys.path}'''
|
||||
root.wm_attributes('-alpha', ui_transparency / 100)
|
||||
# Display message box about plugins that failed to load
|
||||
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
|
||||
root.after(2, show_killswitch_poppup, root)
|
||||
# Start the main event loop
|
||||
|
120
plug.py
120
plug.py
@ -28,8 +28,6 @@ logger = get_main_logger()
|
||||
|
||||
# List of loaded Plugins
|
||||
PLUGINS = []
|
||||
PLUGINS_not_py3 = []
|
||||
PLUGINS_broken = []
|
||||
|
||||
|
||||
# For asynchronous error display
|
||||
@ -49,7 +47,7 @@ last_error = LastError()
|
||||
class 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.
|
||||
|
||||
@ -59,9 +57,11 @@ class Plugin:
|
||||
:raises Exception: Typically, ImportError or OSError
|
||||
"""
|
||||
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.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:
|
||||
logger.info(f'loading plugin "{name.replace(".", "_")}" from "{loadfile}"')
|
||||
@ -78,14 +78,18 @@ class Plugin:
|
||||
newname = module.plugin_start3(Path(loadfile).resolve().parent)
|
||||
self.name = str(newname) if newname else self.name
|
||||
self.module = module
|
||||
self.broken = None
|
||||
elif getattr(module, 'plugin_start', None):
|
||||
self.broken = f'plugin {name} needs migrating'
|
||||
logger.warning(f'plugin {name} needs migrating\n')
|
||||
PLUGINS_not_py3.append(self)
|
||||
else:
|
||||
self.broken = 'needs python 3 migration'
|
||||
logger.error(f'plugin {name} has no plugin_start3() function')
|
||||
else:
|
||||
self.broken = 'Failed to load from load.py'
|
||||
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}"')
|
||||
raise
|
||||
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
|
||||
sys.path.append(config.plugin_dir)
|
||||
|
||||
found = _load_found_plugins()
|
||||
PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower()))
|
||||
sync_all_plugins()
|
||||
|
||||
|
||||
def _load_internal_plugins():
|
||||
@ -175,16 +178,42 @@ def _load_internal_plugins():
|
||||
try:
|
||||
plugin_name = name[:-3]
|
||||
plugin_path = config.internal_plugin_dir_path / name
|
||||
plugin = Plugin(plugin_name, plugin_path, logger)
|
||||
plugin.folder = None
|
||||
plugin = Plugin(plugin_name, None, plugin_path, logger)
|
||||
internal.append(plugin)
|
||||
except Exception:
|
||||
logger.exception(f'Failure loading internal Plugin "{name}"')
|
||||
return internal
|
||||
|
||||
|
||||
def _load_found_plugins():
|
||||
found = []
|
||||
def load_plugin(folder: str):
|
||||
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*
|
||||
# 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
|
||||
@ -194,27 +223,14 @@ def _load_found_plugins():
|
||||
not (p / '__init__.py').is_file(), p.name.lower()))
|
||||
|
||||
for plugin_file in plugin_files:
|
||||
name = plugin_file.name
|
||||
if not (config.plugin_dir_path / name).is_dir() or name.startswith(('.', '_')):
|
||||
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
|
||||
if not get_plugin_by_folder(plugin_file.name):
|
||||
load_plugin(plugin_file.name)
|
||||
|
||||
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]:
|
||||
"""
|
||||
@ -381,7 +397,7 @@ def notify_journal_entry_cqc(
|
||||
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.
|
||||
|
||||
@ -468,3 +484,41 @@ def show_error(err: str) -> None:
|
||||
if err and last_error.root:
|
||||
last_error.msg = str(err)
|
||||
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 logging
|
||||
import os.path
|
||||
import zipfile
|
||||
from os.path import expandvars, join, normpath
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
@ -12,7 +14,7 @@ import tkinter as tk
|
||||
import warnings
|
||||
from os import system
|
||||
from tkinter import colorchooser as tkColorChooser # type: ignore # noqa: N812
|
||||
from tkinter import ttk
|
||||
from tkinter import ttk, messagebox
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, Optional, Type
|
||||
import myNotebook as nb # noqa: N813
|
||||
@ -228,9 +230,10 @@ if sys.platform == 'win32':
|
||||
class PreferencesDialog(tk.Toplevel):
|
||||
"""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)
|
||||
|
||||
self.theApp = app
|
||||
self.parent = parent
|
||||
self.callback = callback
|
||||
self.req_restart = False
|
||||
@ -265,6 +268,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
|
||||
notebook: nb.Notebook = nb.Notebook(frame)
|
||||
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
|
||||
self.notebook = notebook
|
||||
|
||||
self.PADX = 10
|
||||
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||||
@ -409,6 +413,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
for plugin in plug.PLUGINS:
|
||||
plugin_frame = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta)
|
||||
if plugin_frame:
|
||||
plugin.notebook_frame = plugin_frame
|
||||
notebook.add(plugin_frame, text=plugin.name)
|
||||
|
||||
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
|
||||
# Plugin settings and info
|
||||
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)
|
||||
self.plugdir = tk.StringVar()
|
||||
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
|
||||
self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
|
||||
with row as cur_row:
|
||||
nb.Label(
|
||||
# Install Plugin Button
|
||||
self.install_plugin_btn = ttk.Button(
|
||||
plugins_frame,
|
||||
# Help text in settings
|
||||
# LANG: Tip/label about how to disable plugins
|
||||
text=tr.tl(
|
||||
"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)
|
||||
# LANG: Label on button used to Install Plugin
|
||||
text=tr.tl('Install Plugin'),
|
||||
command=lambda frame=plugins_frame: self.installbrowse(config.plugin_dir_path, frame)
|
||||
)
|
||||
self.install_plugin_btn.grid(column=0, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
|
||||
|
||||
# Open Plugin Folder 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
|
||||
).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))
|
||||
if enabled_plugins:
|
||||
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,
|
||||
# LANG: Label on list of enabled plugins
|
||||
text=tr.tl('Enabled Plugins')+':' # List of plugins in settings
|
||||
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, 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,
|
||||
# LANG: Label on list of enabled plugins
|
||||
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:
|
||||
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()
|
||||
)
|
||||
self.sync_all_plugin_rows(plugins_frame)
|
||||
|
||||
# LANG: Label on Settings > Plugins tab
|
||||
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):
|
||||
"""
|
||||
Notify plugins of cmdr change.
|
||||
@ -1087,6 +1116,58 @@ class PreferencesDialog(tk.Toplevel):
|
||||
pathvar.set(directory)
|
||||
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:
|
||||
"""
|
||||
Display a path in a locked tk.Entry.
|
||||
|
Loading…
x
Reference in New Issue
Block a user