mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2026-10-22 17:22:09 +03:00
619 lines
21 KiB
Python
619 lines
21 KiB
Python
"""
|
|
plugin_browser.py - EDMC's Official Plugin Browser.
|
|
|
|
Copyright (c) EDCD, All Rights Reserved
|
|
Licensed under the GNU General Public License v2 or later.
|
|
See LICENSE file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import os
|
|
import json
|
|
import sys
|
|
import webbrowser
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from PIL import Image, ImageTk
|
|
from typing import Callable, cast
|
|
import requests
|
|
from io import BytesIO
|
|
from datetime import datetime
|
|
import myNotebook as nb # noqa: N813
|
|
import plug
|
|
from config import appversion_nobuild
|
|
from EDMCLogging import get_main_logger
|
|
from l10n import translations as tr
|
|
from ttkHyperlinkLabel import HyperlinkLabel
|
|
import semantic_version
|
|
|
|
logger = get_main_logger()
|
|
|
|
# TODO in 6.2 or later: Install/Uninstall Plugin, notify user a plugin update is available.
|
|
|
|
|
|
def get_plugins():
|
|
"""DEV: Print Installed Non-Core Plugins."""
|
|
print("Installed Plugins")
|
|
for plugin in plug.PLUGINS:
|
|
if plugin.folder: # Exclude Default Plugins
|
|
print(vars(plugin))
|
|
print(plugin.get_version())
|
|
|
|
|
|
def read_plugin_list():
|
|
"""Read the Plugin List distributed by EDMC."""
|
|
with open("master_plugin_list.json", encoding="utf-8") as plugin_list_file:
|
|
plugin_list = json.load(plugin_list_file)
|
|
plugins_by_id = {p["pluginName"]: p for p in plugin_list}
|
|
return plugin_list, plugins_by_id
|
|
|
|
|
|
def open_plugin_main(plugin) -> None:
|
|
"""Open the Plugin's Main Repository."""
|
|
webbrowser.open(plugin.get("pluginMainLink"))
|
|
|
|
|
|
def report_plugin(plugin) -> None: # FIXME: Update to live repo.
|
|
"""Open the Webbrowser to the new plugin issue report."""
|
|
webbrowser.open(
|
|
f"https://github.com/Rixxan/EDMC_Plugin_Registry_Dev/issues/new?"
|
|
f"template=report_plugin.md&title=Plugin Report: "
|
|
f"{plugin.get('pluginName')} ({plugin.get('pluginHash')[:8]})"
|
|
)
|
|
|
|
|
|
class PluginBrowserMixIn:
|
|
"""Separated Class for the Plugin Browser."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the Plugin Browser."""
|
|
self.PADX = 10
|
|
self.BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
|
self.LISTX = 25 # indent listed items
|
|
self.PADY = 1 # close spacing
|
|
self.BOXY = 2 # box spacing
|
|
self.SEPY = 10 # separator line spacing
|
|
|
|
# Setup Plugin Browser Options
|
|
self.browser_plugins, self.browser_plugins_by_id = read_plugin_list()
|
|
self._plugin_sort_reverse = {
|
|
"pluginName": False,
|
|
"pluginLastUpdate": False,
|
|
"pluginStars": False,
|
|
}
|
|
self.selected_plugin: dict | None = None
|
|
self._plugin_last_tested_label: nb.Label | None = None
|
|
|
|
# def dev_install_plugin(self, plugin) -> None:
|
|
# """TEMP."""
|
|
# print("Install", plugin.get("pluginName"))
|
|
#
|
|
# def dev_uninstall_plugin(self, plugin) -> None:
|
|
# """TEMP."""
|
|
# print("Uninstall", plugin.get("pluginName"))
|
|
#
|
|
# def dev_update_plugin(self, plugin) -> None:
|
|
# """TEMP."""
|
|
# print("Update", plugin.get("pluginName"))
|
|
|
|
def setup_browser_tab(self, notebook: nb.Notebook, row) -> None:
|
|
"""Set up the Plugin Browser tab in Preferences."""
|
|
if not hasattr(self, "browser_plugins"):
|
|
self.browser_plugins, self.browser_plugins_by_id = read_plugin_list()
|
|
self._plugin_sort_reverse = {"pluginName": False, "pluginLastUpdate": False}
|
|
self.selected_plugin = None
|
|
|
|
categories = sorted(
|
|
{
|
|
cat
|
|
for plugin in self.browser_plugins
|
|
for cat in plugin.get("pluginCategory", [])
|
|
}
|
|
)
|
|
categories.insert(0, "All")
|
|
|
|
plugins_frame = nb.Frame(notebook)
|
|
HyperlinkLabel(
|
|
plugins_frame,
|
|
text="EDMC Plugin Browser",
|
|
background=nb.Label().cget("background"),
|
|
url="https://github.com/Rixxan/EDMC_Plugin_Registry_Dev", # Fixme
|
|
underline=True,
|
|
).grid(row=row.get(), padx=self.PADX, pady=(self.PADY, 1), sticky=tk.W)
|
|
|
|
# Add after the "Available Plugins" label
|
|
categories = sorted(
|
|
{
|
|
cat
|
|
for plugin in self.browser_plugins
|
|
for cat in plugin.get("pluginCategory", [])
|
|
}
|
|
)
|
|
categories.insert(0, "All") # default to show all
|
|
|
|
self.selected_category = tk.StringVar(value="All")
|
|
|
|
# Place the dropdown on its own row (separate from the tree)
|
|
# LANG: Filter Plugin Categories
|
|
category_label = nb.Label(plugins_frame, text=tr.tl("Filter by Category:"))
|
|
category_label.grid(
|
|
row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=(0, self.PADY)
|
|
)
|
|
|
|
category_dropdown = nb.OptionMenu(
|
|
plugins_frame,
|
|
self.selected_category,
|
|
"All",
|
|
*categories,
|
|
command=self.__on_category_selected, # callback
|
|
)
|
|
category_dropdown.config(width=25) # make the dropdown wider
|
|
category_dropdown.grid(
|
|
row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=(0, self.PADY)
|
|
)
|
|
|
|
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
|
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
|
)
|
|
tree_container = nb.Frame(plugins_frame) # type: ignore
|
|
tree_row = row.get()
|
|
tree_container.grid(row=tree_row, column=0, sticky=tk.NSEW, padx=self.PADX)
|
|
|
|
plugins_frame.columnconfigure(0, weight=1)
|
|
|
|
columns = ("name", "version", "updated", "category", "description", "stars")
|
|
|
|
self.plugins_tree = ttk.Treeview(
|
|
tree_container,
|
|
columns=columns,
|
|
show="headings",
|
|
selectmode="browse",
|
|
height=12,
|
|
)
|
|
|
|
self.plugins_tree.heading(
|
|
"name",
|
|
# LANG: Plugin Browser Plug Name
|
|
text=tr.tl("Name"),
|
|
command=lambda: self.__sort_plugins("pluginName"),
|
|
)
|
|
# LANG: Plugin Browser Plug Ver
|
|
self.plugins_tree.heading("version", text=tr.tl("Version"))
|
|
self.plugins_tree.heading(
|
|
"updated",
|
|
text=tr.tl("Last Updated"), # LANG: Plugin Browser Last Update
|
|
command=lambda: self.__sort_plugins("pluginLastUpdate"),
|
|
)
|
|
self.plugins_tree.heading(
|
|
"category",
|
|
text=tr.tl("Categories"), # LANG: Plugin Browser Category
|
|
)
|
|
self.plugins_tree.heading(
|
|
"description",
|
|
text=tr.tl("Description"), # LANG: Plugin Browser Description
|
|
)
|
|
self.plugins_tree.heading(
|
|
"stars",
|
|
# LANG: Plugin Browser Stars
|
|
text=tr.tl("Stars"),
|
|
command=lambda: self.__sort_plugins("pluginStars"),
|
|
)
|
|
|
|
self.plugins_tree.column("name", width=40)
|
|
self.plugins_tree.column("version", width=40, anchor=tk.CENTER)
|
|
self.plugins_tree.column("updated", width=40, anchor=tk.CENTER)
|
|
self.plugins_tree.column("category", width=120, anchor=tk.CENTER)
|
|
self.plugins_tree.column("description", width=240)
|
|
self.plugins_tree.column("stars", width=40)
|
|
|
|
scrollbar = ttk.Scrollbar(tree_container, command=self.plugins_tree.yview)
|
|
self.plugins_tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
self.plugins_tree.grid(row=0, column=0, sticky=tk.NSEW)
|
|
scrollbar.grid(row=0, column=1, sticky=tk.NS)
|
|
|
|
tree_container.columnconfigure(0, weight=1)
|
|
tree_container.rowconfigure(0, weight=0)
|
|
|
|
ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid(
|
|
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
|
|
)
|
|
|
|
self.plugin_details_frame = nb.Frame(plugins_frame) # type: ignore
|
|
details_row = row.get()
|
|
self.plugin_details_frame.grid(
|
|
row=details_row, column=0, sticky=tk.NW, padx=self.PADX
|
|
)
|
|
self.plugin_details_frame.columnconfigure(1, weight=1)
|
|
self.plugin_details_frame.grid_propagate(True)
|
|
|
|
self._plugin_icon_cache: dict[str, ImageTk.PhotoImage] = {}
|
|
self._plugin_icon_label = nb.Label(self.plugin_details_frame)
|
|
self._plugin_icon_label.grid(
|
|
row=0, column=0, rowspan=3, sticky=tk.NW, padx=(0, 12)
|
|
)
|
|
self._plugin_hyperlinks: list[HyperlinkLabel] = []
|
|
self._plugin_title = nb.Label(
|
|
self.plugin_details_frame,
|
|
font=("TkDefaultFont", 10, "bold"),
|
|
wraplength=600,
|
|
justify=tk.LEFT,
|
|
)
|
|
self._plugin_title.grid(row=0, column=1, sticky=tk.W)
|
|
|
|
self._plugin_meta = nb.Label(
|
|
self.plugin_details_frame,
|
|
justify=tk.LEFT,
|
|
)
|
|
self._plugin_meta.grid(row=1, column=1, sticky=tk.W, pady=(1, 2))
|
|
|
|
self._plugin_description = nb.Label(
|
|
self.plugin_details_frame,
|
|
wraplength=600,
|
|
justify=tk.LEFT,
|
|
)
|
|
self._plugin_description.grid(row=2, column=1, sticky=tk.W)
|
|
self._create_plugin_action_buttons()
|
|
self.plugins_tree.bind("<<TreeviewSelect>>", self.__on_plugin_selected)
|
|
self.__populate_plugins_tree()
|
|
|
|
notebook.add(
|
|
plugins_frame,
|
|
# LANG: Plugin Browser Title
|
|
text=tr.tl("Plugin Browser"),
|
|
)
|
|
|
|
def __get_filtered_plugins(self) -> list[dict]:
|
|
category = self.selected_category.get()
|
|
if category == "All":
|
|
return self.browser_plugins
|
|
return [
|
|
p for p in self.browser_plugins if category in p.get("pluginCategory", [])
|
|
]
|
|
|
|
def _run_plugin_action(self, handler) -> None:
|
|
if self.selected_plugin is None:
|
|
return
|
|
handler(self.selected_plugin)
|
|
|
|
def _create_plugin_action_buttons(self) -> None:
|
|
self.plugin_actions_frame = nb.Frame(self.plugin_details_frame) # type: ignore
|
|
self.plugin_actions_frame.grid(
|
|
row=0, column=2, rowspan=6, sticky=tk.N, padx=(12, 0)
|
|
)
|
|
|
|
self._plugin_action_buttons: list[nb.Button] = []
|
|
|
|
actions = [
|
|
(
|
|
# LANG: Open Plugin Repo
|
|
tr.tl("Open Plugin Repository"),
|
|
open_plugin_main,
|
|
),
|
|
# ("Install", self.dev_install_plugin), # Upcoming Feature
|
|
# ("Uninstall", self.dev_uninstall_plugin), # Upcoming Feature
|
|
# ("Update", self.dev_update_plugin), # Upcoming Feature
|
|
(
|
|
# LANG: Report a Malfunctioning Plugin
|
|
tr.tl("Report Plugin"),
|
|
report_plugin,
|
|
),
|
|
]
|
|
|
|
for idx, (label, handler) in enumerate(actions):
|
|
btn = nb.Button(
|
|
self.plugin_actions_frame,
|
|
text=label,
|
|
command=cast(
|
|
Callable[[], None], lambda h=handler: self._run_plugin_action(h)
|
|
),
|
|
)
|
|
btn.grid(
|
|
row=idx,
|
|
column=0,
|
|
padx=self.LISTX,
|
|
pady=(0 if idx == 0 else self.PADY),
|
|
sticky=tk.EW,
|
|
)
|
|
btn.state(["disabled"])
|
|
self._plugin_action_buttons.append(btn)
|
|
|
|
self.plugin_actions_frame.columnconfigure(0, weight=1)
|
|
self.plugin_actions_frame.grid_remove()
|
|
|
|
def __populate_plugins_tree(self, plugins: list[dict] | None = None) -> None:
|
|
self.plugins_tree.delete(*self.plugins_tree.get_children())
|
|
|
|
if plugins is None:
|
|
plugins = self.__get_filtered_plugins()
|
|
|
|
# Rebuild Treeview ID to plugin mapping
|
|
self.browser_plugins_by_id = {}
|
|
|
|
for index, plugin in enumerate(plugins):
|
|
plugin_id = f"plugin_{index}"
|
|
self.browser_plugins_by_id[plugin_id] = plugin
|
|
self.plugins_tree.insert(
|
|
"",
|
|
tk.END,
|
|
iid=plugin_id,
|
|
values=(
|
|
plugin.get("pluginName", ""),
|
|
plugin.get("pluginVer", ""),
|
|
plugin.get("pluginLastUpdate", ""),
|
|
", ".join(plugin.get("pluginCategory", [])),
|
|
plugin.get("pluginDesc", ""),
|
|
plugin.get("pluginStars", ""),
|
|
),
|
|
)
|
|
self._set_plugin_action_visibility(bool(plugins))
|
|
self.plugins_tree.selection_remove(self.plugins_tree.selection())
|
|
self.selected_plugin = None
|
|
self.__clear_plugin_details()
|
|
|
|
def __on_category_selected(self, _event=None):
|
|
# Simply repopulate Treeview with filtered plugins
|
|
self.__populate_plugins_tree()
|
|
|
|
def __sort_plugins(self, key: str) -> None:
|
|
# Initialize sort state if missing
|
|
if key not in self._plugin_sort_reverse:
|
|
self._plugin_sort_reverse[key] = False
|
|
|
|
reverse = self._plugin_sort_reverse[key]
|
|
self._plugin_sort_reverse[key] = not reverse
|
|
self._current_sort_key = key
|
|
|
|
if key == "pluginLastUpdate":
|
|
self.browser_plugins.sort(
|
|
key=lambda p: datetime.fromisoformat(
|
|
p.get("pluginLastUpdate") or "1900-01-01"
|
|
),
|
|
reverse=reverse,
|
|
)
|
|
|
|
elif key == "pluginStars":
|
|
self.browser_plugins.sort(
|
|
key=lambda p: int(p.get("pluginStars") or 0),
|
|
reverse=reverse,
|
|
)
|
|
|
|
else:
|
|
self.browser_plugins.sort(
|
|
key=lambda p: (p.get(key) or "").casefold(),
|
|
reverse=reverse,
|
|
)
|
|
|
|
# Repopulate with current category filter
|
|
self.__populate_plugins_tree()
|
|
|
|
def __on_plugin_selected(self, event) -> None:
|
|
self._clear_plugin_links()
|
|
|
|
selection = self.plugins_tree.selection()
|
|
if not selection:
|
|
self._clear_selection_state()
|
|
return
|
|
|
|
plugin_id = selection[0]
|
|
plugin = self.browser_plugins_by_id.get(plugin_id)
|
|
if not plugin:
|
|
self._clear_selection_state()
|
|
return
|
|
|
|
self.selected_plugin = plugin
|
|
self._set_plugin_action_state(True)
|
|
|
|
self._plugin_title.configure(text=plugin.get("pluginName", ""))
|
|
|
|
meta_lines = self._build_plugin_meta(plugin)
|
|
self._plugin_meta.configure(text="\n".join(meta_lines))
|
|
|
|
link_row = self._update_last_tested(plugin)
|
|
|
|
self._build_plugin_links(plugin, link_row)
|
|
|
|
self._plugin_description.configure(text=plugin.get("pluginDesc", ""))
|
|
|
|
self._update_plugin_icon(plugin_id, plugin)
|
|
|
|
self._set_plugin_action_visibility(bool(self.browser_plugins))
|
|
|
|
def _clear_selection_state(self) -> None:
|
|
self.selected_plugin = None
|
|
self.__clear_plugin_details()
|
|
|
|
def _build_plugin_meta(self, plugin: dict) -> list[str]:
|
|
fields = [
|
|
(tr.tl("Version"), "pluginVer"), # LANG: Plugin Browser Plug Ver
|
|
(
|
|
# LANG: Plugin Browser Last Update
|
|
tr.tl("Last Updated"),
|
|
"pluginLastUpdate",
|
|
),
|
|
(
|
|
# LANG: Plugin Browser Category
|
|
tr.tl("Categories"),
|
|
"pluginCategory",
|
|
lambda v: ", ".join(v),
|
|
),
|
|
(
|
|
# LANG: Plugin Browser Authors
|
|
tr.tl("Authors"),
|
|
"pluginAuthors",
|
|
lambda v: ", ".join(v),
|
|
),
|
|
(tr.tl("License"), "pluginLicense"), # LANG: Plugin Browser License
|
|
(tr.tl("Stars"), "pluginStars"), # LANG: Plugin Browser Stars
|
|
(
|
|
# LANG: Plugin Browser Reqs
|
|
tr.tl("Requirements"),
|
|
"pluginRequirements",
|
|
lambda v: ", ".join(v),
|
|
),
|
|
]
|
|
|
|
meta = []
|
|
for label, key, *formatter in fields:
|
|
value = plugin.get(key)
|
|
if value:
|
|
if formatter:
|
|
value = formatter[0](value) # type: ignore
|
|
meta.append(f"{label}: {value}")
|
|
|
|
return meta
|
|
|
|
def _update_last_tested(self, plugin: dict) -> int:
|
|
last_tested = plugin.get("pluginLastTestedEDMC")
|
|
if not last_tested:
|
|
if getattr(self, "_plugin_last_tested_label", None):
|
|
self._plugin_last_tested_label.grid_forget() # type: ignore
|
|
self._plugin_last_tested_label = None
|
|
return 3
|
|
|
|
tested = semantic_version.Version.coerce(last_tested)
|
|
current = appversion_nobuild()
|
|
|
|
major_diff = tested.major - current.major
|
|
minor_diff = tested.minor - current.minor
|
|
|
|
if major_diff == 0 and abs(minor_diff) <= 1:
|
|
color = "green"
|
|
elif major_diff == 0 and abs(minor_diff) <= 2:
|
|
color = "dark goldenrod"
|
|
else:
|
|
color = "red"
|
|
|
|
# LANG: Last Tested EDMC Version
|
|
text = tr.tl("Last Tested EDMC: {last_tested}").format(last_tested=last_tested)
|
|
|
|
if getattr(self, "_plugin_last_tested_label", None):
|
|
self._plugin_last_tested_label.configure(text=text, foreground=color) # type: ignore
|
|
else:
|
|
self._plugin_last_tested_label = nb.Label(
|
|
self.plugin_details_frame,
|
|
text=text,
|
|
foreground=color,
|
|
justify=tk.LEFT,
|
|
)
|
|
self._plugin_last_tested_label.grid(row=3, column=1, sticky=tk.W)
|
|
|
|
return 4
|
|
|
|
def _clear_plugin_links(self) -> None:
|
|
for hl in self._plugin_hyperlinks:
|
|
hl.destroy()
|
|
self._plugin_hyperlinks.clear()
|
|
|
|
def _build_plugin_links(self, plugin: dict, start_row: int) -> None:
|
|
links = [
|
|
(
|
|
# LANG: Plugin Project Link
|
|
tr.tl("Main Project Link"),
|
|
plugin.get("pluginMainLink"),
|
|
),
|
|
(tr.tl("VirusTotal Link"), plugin.get("pluginVT")), # LANG: Plugin VT Link
|
|
]
|
|
|
|
bg = nb.Label().cget("background")
|
|
row = start_row
|
|
|
|
for text, url in links:
|
|
if not url:
|
|
continue
|
|
|
|
hl = HyperlinkLabel(
|
|
self.plugin_details_frame,
|
|
text=text,
|
|
background=bg,
|
|
url=url,
|
|
underline=True,
|
|
)
|
|
hl.grid(row=row, column=1, padx=self.PADX, pady=self.PADY, sticky=tk.W)
|
|
self._plugin_hyperlinks.append(hl)
|
|
row += 1
|
|
|
|
def _update_plugin_icon(self, plugin_id: str, plugin: dict) -> None:
|
|
icon_url = plugin.get("pluginIcon")
|
|
icon_key = plugin_id if icon_url else "__fallback__"
|
|
url = icon_url or os.path.join(
|
|
os.path.dirname(sys.argv[0]),
|
|
"io.edcd.EDMarketConnector.png",
|
|
)
|
|
self.__load_plugin_icon(icon_key, url)
|
|
|
|
def __clear_plugin_details(self) -> None:
|
|
self._plugin_title.configure(text="")
|
|
self._plugin_meta.configure(text="")
|
|
self._plugin_description.configure(text="")
|
|
|
|
# Last Tested
|
|
if getattr(self, "_plugin_last_tested_label", None) is not None:
|
|
self._plugin_last_tested_label.grid_forget() # type: ignore
|
|
self._plugin_last_tested_label = None
|
|
|
|
# Plugin Icon
|
|
self._plugin_icon_label.configure(image="")
|
|
self._plugin_icon_label.image = None # type: ignore
|
|
|
|
# Hyperlinks
|
|
if hasattr(self, "_plugin_hyperlinks"):
|
|
for hl in self._plugin_hyperlinks:
|
|
hl.destroy()
|
|
self._plugin_hyperlinks = []
|
|
|
|
self._set_plugin_action_state(False)
|
|
self._set_plugin_action_visibility(False)
|
|
|
|
def __load_plugin_icon(self, plugin_id: str, url: str) -> None:
|
|
if plugin_id in self._plugin_icon_cache:
|
|
tk_image = self._plugin_icon_cache[plugin_id]
|
|
self._plugin_icon_label.configure(image=tk_image)
|
|
self._plugin_icon_label.image = tk_image # type: ignore
|
|
return
|
|
|
|
try:
|
|
# Open PIL image
|
|
pil_image = (
|
|
Image.open(url).convert("RGBA")
|
|
if os.path.isfile(url)
|
|
else Image.open(BytesIO(requests.get(url, timeout=5).content)).convert(
|
|
"RGBA"
|
|
)
|
|
)
|
|
|
|
# Resize while maintaining aspect ratio
|
|
pil_image.thumbnail((64, 64), Image.Resampling.LANCZOS)
|
|
|
|
# Paste onto fixed 64x64 transparent canvas
|
|
canvas = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
|
offset = ((64 - pil_image.width) // 2, (64 - pil_image.height) // 2)
|
|
canvas.paste(pil_image, offset)
|
|
|
|
# Convert to Tkinter PhotoImage
|
|
tk_image = ImageTk.PhotoImage(canvas, master=self.plugins_tree)
|
|
self._plugin_icon_cache[plugin_id] = tk_image
|
|
|
|
self._plugin_icon_label.configure(image=tk_image)
|
|
self._plugin_icon_label.image = tk_image # type: ignore
|
|
|
|
except Exception:
|
|
self._plugin_icon_label.configure(image="")
|
|
self._plugin_icon_label.image = None # type: ignore
|
|
|
|
def _set_plugin_action_visibility(self, visible: bool) -> None:
|
|
if not hasattr(self, "plugin_actions_frame"):
|
|
return
|
|
|
|
if visible:
|
|
self.plugin_actions_frame.grid()
|
|
else:
|
|
self.plugin_actions_frame.grid_remove()
|
|
|
|
def _set_plugin_action_state(self, enabled: bool) -> None:
|
|
for btn in self._plugin_action_buttons:
|
|
if enabled:
|
|
btn.state(["!disabled"])
|
|
else:
|
|
btn.state(["disabled"])
|