mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2026-10-25 01:06:05 +03:00
Merge pull request #2528 from HullSeals/enhancement/261/disable-plugins
Scrollable Notebook and Plugin Enable/Disable System
This commit is contained in:
5 files changed
+271
-10
No files matched your search
+12
-1
@@ -6,7 +6,7 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are
|
||||
in the source (not distributed with the Windows installer) for the
|
||||
currently used version.
|
||||
---
|
||||
Release 5.14.0
|
||||
Release 6.0.0
|
||||
===
|
||||
|
||||
This is the WORK IN PROGRESS changelog for upcoming release 5.14. All features are subject to change.
|
||||
@@ -22,6 +22,17 @@ This is the WORK IN PROGRESS changelog for upcoming release 5.14. All features a
|
||||
* Set new deprecation warnings using Python 3.13's new Deprecation decorator.
|
||||
* Fixed a bug where "en" would not be included as a valid language and raise a KeyError
|
||||
|
||||
Release 5.13.3
|
||||
===
|
||||
This release contains a bugfix for a race condition that could occur if plugin setup took too long for the journal monitor thread.
|
||||
|
||||
This often resulted in users seeing "Awaiting CMDR Login" even when already in the game.
|
||||
|
||||
This will likely be the last release before 6.0.0.
|
||||
|
||||
**Changes and Enhancements**
|
||||
* Fixes a race condition in journal reading.
|
||||
|
||||
**Plugin Developers**
|
||||
* nb.Entry is deprecated, and is slated for removal in 6.0 or later. Please migrate to nb.EntryMenu
|
||||
* nb.ColoredButton is deprecated, and is slated for removal in 6.0 or later. Please migrate to tk.Button
|
||||
|
||||
Executable → Regular
File mode changed.
@@ -498,6 +498,15 @@
|
||||
/* prefs.py: Catch & Record Profiler Errors; */
|
||||
"Error in System Profiler" = "Error in System Profiler";
|
||||
|
||||
/* prefs.py: Disabling a Plugin. */
|
||||
"Disabling plugin {PLUGIN}. This will cause a restart. Click OK to continue..." = "Disabling plugin {PLUGIN}. This will cause a restart. Click OK to continue...";
|
||||
|
||||
/* prefs.py: Enable a Disabled Plugin */
|
||||
"Enable" = "Enable";
|
||||
|
||||
/* prefs.py: Disable an Enabled Plugin */
|
||||
"Disable" = "Disable";
|
||||
|
||||
/* stats.py: Cmdr stats; In files: stats.py:58; */
|
||||
"Balance" = "Balance";
|
||||
|
||||
|
||||
+180
@@ -157,3 +157,183 @@ class OptionMenu(ttk.OptionMenu):
|
||||
# Workaround for https://bugs.python.org/issue25684
|
||||
for i in range(0, self['menu'].index('end') + 1):
|
||||
self['menu'].entryconfig(i, variable=variable)
|
||||
|
||||
|
||||
class ScrollableNotebook(Notebook):
|
||||
"""
|
||||
ScrollableNotebook – A tab bar that scrolls horizontally when there are too many tabs.
|
||||
|
||||
Based off ttkScrollableNotebook by @muhammeteminturgut.
|
||||
https://github.com/muhammeteminturgut/ttkScrollableNotebook
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master: ttk.Frame | None = None,
|
||||
tabmenu: bool = False,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None:
|
||||
super().__init__(master, **kwargs)
|
||||
|
||||
# Sliding state
|
||||
self.xLocation: int = 0
|
||||
self.timer: str | None = None
|
||||
self.menuSpace: int = 50 if tabmenu else 30
|
||||
self.contentsManaged: list[tk.Widget] = []
|
||||
|
||||
# Notebook that holds the actual content widgets
|
||||
self.notebookContent: ttk.Notebook = ttk.Notebook(self, **kwargs)
|
||||
self.notebookContent.pack(fill="both", expand=True)
|
||||
self.notebookContent.bind("<Configure>", self._reset_slide)
|
||||
|
||||
# Notebook that displays scrollable tabs
|
||||
self.notebookTab: ttk.Notebook = ttk.Notebook(self, **kwargs)
|
||||
self.notebookTab.place(x=0, y=0)
|
||||
self.notebookTab.bind("<<NotebookTabChanged>>", self._tab_changer)
|
||||
|
||||
# Sliding frame and controls
|
||||
slide_frame: ttk.Frame = ttk.Frame(self)
|
||||
slide_frame.place(relx=1.0, x=0, y=1, anchor=tk.NE)
|
||||
|
||||
if tabmenu:
|
||||
menu_btn = ttk.Label(slide_frame, text="\u2630")
|
||||
menu_btn.bind("<ButtonPress-1>", self._bottom_menu)
|
||||
menu_btn.pack(side=tk.RIGHT)
|
||||
|
||||
left_arrow = ttk.Label(slide_frame, text=" \u276e")
|
||||
left_arrow.bind("<ButtonPress-1>", self._left_slide_start)
|
||||
left_arrow.bind("<ButtonRelease-1>", self._slide_stop)
|
||||
left_arrow.pack(side=tk.LEFT)
|
||||
right_arrow = ttk.Label(slide_frame, text=" \u276f")
|
||||
right_arrow.bind("<ButtonPress-1>", self._right_slide_start)
|
||||
right_arrow.bind("<ButtonRelease-1>", self._slide_stop)
|
||||
right_arrow.pack(side=tk.RIGHT)
|
||||
|
||||
# Outer Notebook geometry
|
||||
self.grid(padx=10, pady=10, sticky=tk.NSEW)
|
||||
|
||||
def _bottom_menu(self, event: tk.Event) -> None:
|
||||
menu = tk.Menu(self, tearoff=0)
|
||||
for tab in self.notebookTab.tabs():
|
||||
label = self.notebookTab.tab(tab, option="text")
|
||||
menu.add_command(label=label, command=lambda t=tab: self.select(t)) # type: ignore
|
||||
try:
|
||||
menu.tk_popup(event.x_root, event.y_root)
|
||||
finally:
|
||||
menu.grab_release()
|
||||
|
||||
def _tab_changer(self, event: tk.Event) -> None:
|
||||
try:
|
||||
idx = self.notebookTab.index("current")
|
||||
self.notebookContent.select(idx)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _right_slide_start(self, event: tk.Event | None = None) -> None:
|
||||
if self._right_slide(event):
|
||||
self.timer = self.after(100, self._right_slide_start)
|
||||
|
||||
def _right_slide(self, event: tk.Event | None) -> bool:
|
||||
tabs_w = self.notebookTab.winfo_width()
|
||||
content_w = self.notebookContent.winfo_width()
|
||||
|
||||
if tabs_w > content_w - self.menuSpace:
|
||||
remaining = content_w - (tabs_w + self.notebookTab.winfo_x())
|
||||
if remaining <= self.menuSpace + 5:
|
||||
self.xLocation -= 20
|
||||
self.notebookTab.place(x=self.xLocation, y=0)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _left_slide_start(self, event: tk.Event | None = None) -> None:
|
||||
if self._left_slide(event):
|
||||
self.timer = self.after(100, self._left_slide_start)
|
||||
|
||||
def _left_slide(self, event: tk.Event | None) -> bool:
|
||||
if self.notebookTab.winfo_x() != 0:
|
||||
self.xLocation += 20
|
||||
self.notebookTab.place(x=self.xLocation, y=0)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _slide_stop(self, event: tk.Event) -> None:
|
||||
if self.timer is not None:
|
||||
self.after_cancel(self.timer)
|
||||
self.timer = None
|
||||
|
||||
def _reset_slide(self, event: tk.Event | None = None) -> None:
|
||||
self.notebookTab.place(x=0, y=0)
|
||||
self.xLocation = 0
|
||||
|
||||
def add(self, child: tk.Widget, **kwargs) -> None:
|
||||
"""Add content + visual tab."""
|
||||
has_tabs = len(self.notebookTab.tabs()) > 0
|
||||
|
||||
# Content tab properties
|
||||
content_kwargs = kwargs.copy()
|
||||
content_kwargs["text"] = "" # content tabs never show text
|
||||
|
||||
if has_tabs:
|
||||
content_kwargs["state"] = "hidden"
|
||||
else:
|
||||
content_kwargs.pop("state", None)
|
||||
|
||||
# Add content widget
|
||||
self.notebookContent.add(child, **content_kwargs)
|
||||
|
||||
# Add visible tab
|
||||
self.notebookTab.add(ttk.Frame(self.notebookTab), **kwargs)
|
||||
self.contentsManaged.append(child)
|
||||
|
||||
def __content_tab_id(self, tab_id: str) -> str:
|
||||
idx = self.notebookTab.tabs().index(tab_id)
|
||||
return self.notebookContent.tabs()[idx]
|
||||
|
||||
def forget(self, tab_id: str) -> None: # type: ignore
|
||||
"""Remove a tab from both the visible tab strip and the hidden content notebook."""
|
||||
idx = self.notebookTab.index(tab_id)
|
||||
content_tab = self.__content_tab_id(tab_id)
|
||||
|
||||
self.notebookContent.forget(content_tab)
|
||||
self.notebookTab.forget(tab_id)
|
||||
self.contentsManaged[idx].destroy()
|
||||
self.contentsManaged.pop(idx)
|
||||
|
||||
def hide(self, tab_id: str) -> None:
|
||||
"""Hide a tab without destroying it."""
|
||||
self.notebookContent.hide(self.__content_tab_id(tab_id))
|
||||
self.notebookTab.hide(tab_id)
|
||||
|
||||
def identify(self, x: int, y: int) -> str | None: # type: ignore
|
||||
"""Identify the visible tab at a given x/y coord."""
|
||||
return self.notebookTab.identify(x, y)
|
||||
|
||||
def index(self, tab_id: str) -> int:
|
||||
"""Return the index of a visible tab."""
|
||||
return self.notebookTab.index(tab_id)
|
||||
|
||||
def insert(self, pos: int, child: tk.Widget, **kwargs) -> None:
|
||||
"""Insert a new tab at a specified position."""
|
||||
self.notebookContent.insert(pos, child, **kwargs)
|
||||
self.notebookTab.insert(pos, ttk.Frame(self.notebookTab), **kwargs)
|
||||
|
||||
def select(self, tab_id: str) -> None: # type: ignore
|
||||
"""Select a tab."""
|
||||
self.notebookTab.select(tab_id)
|
||||
|
||||
def tab(self, tab_id: str, option: str | None = None, **kwargs):
|
||||
"""Get/Set Options for tabs."""
|
||||
c_kwargs = kwargs.copy()
|
||||
c_kwargs["text"] = "" # content tabs never show text
|
||||
self.notebookContent.tab(self.__content_tab_id(tab_id), **c_kwargs)
|
||||
return self.notebookTab.tab(tab_id, option=option, **kwargs)
|
||||
|
||||
def tabs(self) -> tuple[str, ...]:
|
||||
"""Tuple of all tabs."""
|
||||
return self.notebookTab.tabs()
|
||||
|
||||
def enable_traversal(self) -> None:
|
||||
"""Enable keyboard shortcuts for tabs."""
|
||||
self.notebookContent.enable_traversal()
|
||||
self.notebookTab.enable_traversal()
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import time
|
||||
from os.path import join, normpath
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
@@ -275,7 +276,7 @@ class PreferencesDialog(tk.Toplevel):
|
||||
frame.rowconfigure(0, weight=1)
|
||||
frame.rowconfigure(1, weight=0)
|
||||
|
||||
notebook: nb.Notebook = nb.Notebook(frame)
|
||||
notebook: nb.ScrollableNotebook = nb.ScrollableNotebook(frame, tabmenu=True)
|
||||
notebook.bind('<<NotebookTabChanged>>', self.tabchanged) # Recompute on tab change
|
||||
|
||||
self.PADX = 10
|
||||
@@ -324,12 +325,6 @@ class PreferencesDialog(tk.Toplevel):
|
||||
# Set Log Directory
|
||||
self.logfile_loc = Path(config.app_dir_path / 'logs')
|
||||
|
||||
# Set minimum size to prevent content cut-off
|
||||
self.update_idletasks() # Update "requested size" from geometry manager
|
||||
min_width = self.winfo_reqwidth()
|
||||
min_height = self.winfo_reqheight()
|
||||
self.wm_minsize(min_width, min_height)
|
||||
|
||||
def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None:
|
||||
output_frame = nb.Frame(root_notebook)
|
||||
output_frame.columnconfigure(0, weight=1)
|
||||
@@ -980,11 +975,19 @@ class PreferencesDialog(tk.Toplevel):
|
||||
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
||||
|
||||
for plugin in enabled_plugins:
|
||||
curr_row = row.get()
|
||||
label = nb.Label(plugins_frame,
|
||||
text=plugin.name if plugin.name == plugin.folder
|
||||
else f'{plugin.folder} ({plugin.name})')
|
||||
|
||||
label.grid(columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get())
|
||||
label.grid(column=0, columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=curr_row)
|
||||
|
||||
btn = ttk.Button(
|
||||
plugins_frame,
|
||||
text=tr.tl("Disable"), # LANG: Disable an Enabled Plugin
|
||||
command=lambda plugin=plugin: self.disable_plugin(plugin) # type: ignore
|
||||
)
|
||||
btn.grid(column=3, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=curr_row)
|
||||
|
||||
############################################################
|
||||
# Show which plugins don't have Python 3.x support
|
||||
@@ -1026,9 +1029,18 @@ class PreferencesDialog(tk.Toplevel):
|
||||
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get())
|
||||
|
||||
for plugin in disabled_plugins:
|
||||
curr_row = row.get()
|
||||
nb.Label(plugins_frame, text=plugin.name).grid(
|
||||
columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()
|
||||
column=0, columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=curr_row
|
||||
)
|
||||
|
||||
btn = ttk.Button(
|
||||
plugins_frame,
|
||||
text=tr.tl("Enable"), # LANG: Enable a Disabled Plugin
|
||||
command=lambda plugin=plugin: self.enable_plugin(plugin) # type: ignore
|
||||
)
|
||||
btn.grid(column=3, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=curr_row)
|
||||
|
||||
############################################################
|
||||
# Show plugins that failed to load
|
||||
############################################################
|
||||
@@ -1050,6 +1062,55 @@ class PreferencesDialog(tk.Toplevel):
|
||||
# LANG: Label on Settings > Plugins tab
|
||||
notebook.add(plugins_frame, text=tr.tl('Plugins')) # Tab heading in settings
|
||||
|
||||
def disable_plugin(self, plugin):
|
||||
"""Disable an existing plugin and restart."""
|
||||
logger.debug(f"Calling plugin_stop() for {plugin.name}")
|
||||
if plugin._get_func('plugin_stop'): # Try to stop cleanly. We're going regardless...
|
||||
plugin.module.plugin_stop()
|
||||
# LANG: Text of Notification Popup for EDMC Restart
|
||||
restart_msg = tr.tl( # LANG: Disabling a Plugin
|
||||
r"Disabling plugin {PLUGIN}. This will cause a restart. Click OK to continue..."
|
||||
).format(PLUGIN=plugin.name)
|
||||
restart_box = tk.messagebox.Message(
|
||||
title=tr.tl('Restart Required'), # LANG: Title of Notification Popup for EDMC Restart
|
||||
message=restart_msg,
|
||||
type=tk.messagebox.OK
|
||||
)
|
||||
restart_box.show()
|
||||
time.sleep(2) # Give plugin stop time to work.
|
||||
plug.PLUGINS.remove(plugin) # Remove PLugin from active list
|
||||
try:
|
||||
folder = Path(config.plugin_dir_path / plugin.folder)
|
||||
new_name = f"{folder}.disabled"
|
||||
folder.rename(new_name)
|
||||
logger.info(f"Disabled {plugin.name}.")
|
||||
except FileExistsError:
|
||||
logger.warning("Unable to disable the plugin!")
|
||||
return
|
||||
self.req_restart = True
|
||||
self.apply()
|
||||
|
||||
def enable_plugin(self, plugin):
|
||||
"""Enable an existing plugin and restart."""
|
||||
logger.debug(f"Attempting to enable {plugin.name}")
|
||||
localfolder = plugin.folder + ".disabled"
|
||||
folder = Path(config.plugin_dir_path) / localfolder
|
||||
if not folder.exists():
|
||||
logger.warning(f"Folder does not exist: {folder}")
|
||||
return
|
||||
new_folder = folder.with_name(plugin.folder.replace(".disabled", ""))
|
||||
if new_folder.exists():
|
||||
logger.warning(f"Target folder already exists: {new_folder}")
|
||||
return
|
||||
try:
|
||||
folder.rename(new_folder)
|
||||
logger.info(f"Enabled {plugin.name}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to enable plugin {plugin.name}: {e}")
|
||||
return
|
||||
self.req_restart = True
|
||||
self.apply()
|
||||
|
||||
def cmdrchanged(self, event=None):
|
||||
"""
|
||||
Notify plugins of cmdr change.
|
||||
|
||||
Reference in new issue
Block a user