1
0
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:
Maxim Kizub 2025-04-06 16:39:15 +03:00 committed by mkizub
parent 2c4e409ca3
commit f91454c56a
3 changed files with 327 additions and 188 deletions

@ -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

@ -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

@ -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.