Skip to content

Commit 1b6a5c8

Browse files
Merge pull request #334 from DataLab-Platform/feature/302-improve-plugin-ux
Feature/302 improve plugin ux
2 parents f25f64c + 242d1e7 commit 1b6a5c8

13 files changed

Lines changed: 1503 additions & 211 deletions

File tree

datalab/config.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ class MainSection(conf.Section, metaclass=conf.SectionMeta):
206206
current_tab = conf.Option()
207207
plugins_enabled = conf.Option()
208208
plugins_enabled_list = conf.Option() # List of enabled plugin names
209-
plugins_path = conf.Option()
209+
plugins_path = conf.Option() # Deprecated: single-directory string, kept for
210+
# backward compatibility. Use plugins_path_list instead.
211+
plugins_path_list = conf.Option() # List of extra plugin directories
210212
tour_enabled = conf.Option()
211213
v020_plugins_warning_ignore = conf.Option() # True: do not warn, False: warn
212214

@@ -525,6 +527,59 @@ class Conf(conf.Configuration, metaclass=conf.ConfMeta):
525527
ai = AISection()
526528

527529

530+
def normalize_plugin_paths(paths: list[str] | tuple[str, ...] | None) -> list[str]:
531+
"""Normalize a list of plugin directories and drop duplicates/empties."""
532+
normalized: list[str] = []
533+
seen: set[str] = set()
534+
for raw_path in paths or []:
535+
if not raw_path:
536+
continue
537+
path = osp.normpath(osp.abspath(osp.expanduser(raw_path)))
538+
if path in seen:
539+
continue
540+
seen.add(path)
541+
normalized.append(path)
542+
return normalized
543+
544+
545+
def get_user_plugin_paths() -> list[str]:
546+
"""Return user-configured extra plugin directories.
547+
548+
Reads from ``plugins_path_list`` (list of directories). For backward
549+
compatibility, the deprecated ``plugins_path`` single-directory string is
550+
also merged into ``plugins_path_list`` if this is empty.
551+
"""
552+
fixed_default = osp.normpath(Conf.get_path("plugins"))
553+
554+
# New list-based option (primary)
555+
path_list = Conf.main.plugins_path_list.get([])
556+
if path_list is None:
557+
path_list = []
558+
candidates = list(path_list)
559+
560+
# Migrate deprecated single-directory option into the list
561+
legacy_path = Conf.main.plugins_path.get("")
562+
if legacy_path and isinstance(legacy_path, str):
563+
norm_legacy = osp.normpath(osp.abspath(osp.expanduser(legacy_path)))
564+
if not candidates and norm_legacy != fixed_default:
565+
candidates.append(legacy_path)
566+
Conf.main.plugins_path_list.set(candidates)
567+
568+
normalized = normalize_plugin_paths(candidates)
569+
return [path for path in normalized if path != fixed_default]
570+
571+
572+
def set_user_plugin_paths(paths: list[str] | tuple[str, ...]) -> None:
573+
"""Persist user-configured extra plugin directories.
574+
575+
Writes to ``plugins_path_list``. The deprecated ``plugins_path`` is left
576+
untouched so that older DataLab versions can still find at least one
577+
user-configured directory.
578+
"""
579+
normalized = normalize_plugin_paths(list(paths))
580+
Conf.main.plugins_path_list.set(normalized)
581+
582+
528583
def get_old_log_fname(fname):
529584
"""Return old log fname from current log fname"""
530585
return osp.splitext(fname)[0] + ".1.log"
@@ -555,7 +610,8 @@ def initialize():
555610
Conf.main.plugins_enabled_list.get(
556611
None
557612
) # None = all enabled, [] = none, list = specific
558-
Conf.main.plugins_path.get(Conf.get_path("plugins"))
613+
Conf.main.plugins_path.get("") # Deprecated: kept for backward compat
614+
Conf.main.plugins_path_list.get([])
559615
Conf.main.tour_enabled.get(True)
560616
Conf.main.v020_plugins_warning_ignore.get(False)
561617
# Console section
Lines changed: 11 additions & 0 deletions
Loading
Lines changed: 8 additions & 0 deletions
Loading

datalab/gui/main.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import time
2929
import traceback
3030
import webbrowser
31+
from datetime import datetime
3132
from typing import TYPE_CHECKING
3233

3334
import guidata.dataset as gds
@@ -158,6 +159,8 @@ def __init__(self, console=None, hide_on_close=False): # pylint: disable=too-ma
158159
execenv.log(self, "Starting initialization")
159160

160161
self.ready_flag = True
162+
self.started_at = datetime.now().astimezone()
163+
self.plugins_last_load_at = self.started_at
161164

162165
self.hide_on_close = hide_on_close
163166
self.__old_size: tuple[int, int] | None = None
@@ -1051,6 +1054,10 @@ def __register_plugins(self) -> None:
10511054
# None = all plugins enabled (default), [] = no plugins, list = specific plugins
10521055
enabled_list = Conf.main.plugins_enabled_list.get(None)
10531056

1057+
if not Conf.main.plugins_enabled.get():
1058+
self.plugins_last_load_at = datetime.now().astimezone()
1059+
return
1060+
10541061
for plugin_class in PluginRegistry.get_plugin_classes():
10551062
try:
10561063
# Check if plugin is enabled before instantiation
@@ -1092,6 +1099,8 @@ def __register_plugins(self) -> None:
10921099
plugin_class.__name__, filepath or "", tb_text
10931100
)
10941101

1102+
self.plugins_last_load_at = datetime.now().astimezone()
1103+
10951104
def __flush_startup_errors(self) -> None:
10961105
"""Write any buffered startup errors to the internal console.
10971106
@@ -1129,6 +1138,11 @@ def __configure_plugins(self) -> None:
11291138
dialog = PluginConfigDialog(self)
11301139
dialog.exec()
11311140

1141+
def set_plugins_enabled(self, enabled: bool) -> None:
1142+
"""Apply the global third-party plugin enabled state."""
1143+
Conf.main.plugins_enabled.set(enabled)
1144+
self.__apply_plugins_enabled_setting()
1145+
11321146
def reload_plugins(self) -> None:
11331147
"""Reload third-party plugins at runtime.
11341148
@@ -1143,8 +1157,9 @@ def reload_plugins(self) -> None:
11431157
self,
11441158
_("Plugins"),
11451159
_(
1146-
"Third-party plugins are disabled. Enable them in the "
1147-
"Settings dialog to use this feature."
1160+
"Third-party plugins are disabled. Enable them again "
1161+
"from the plugin configuration dialog to use this "
1162+
"feature."
11481163
),
11491164
)
11501165
return
@@ -1220,6 +1235,7 @@ def reload_plugins(self) -> None:
12201235
# Update plugin status in the status bar
12211236
self.pluginstatus.update_status()
12221237
self.__update_plugins_availability()
1238+
self.plugins_last_load_at = datetime.now().astimezone()
12231239

12241240
def __configure_statusbar(self, console: bool) -> None:
12251241
"""Configure status bar
@@ -1255,12 +1271,11 @@ def __update_plugins_availability(self) -> None:
12551271
"""Update plugin-related UI according to third-party plugin setting."""
12561272
plugins_enabled = Conf.main.plugins_enabled.get()
12571273

1258-
if self.plugins_menu is not None:
1259-
self.plugins_menu.setEnabled(plugins_enabled)
1274+
if self.reload_plugins_action is not None:
1275+
self.reload_plugins_action.setEnabled(plugins_enabled)
12601276

1261-
for action in (self.reload_plugins_action, self.configure_plugins_action):
1262-
if action is not None:
1263-
action.setEnabled(plugins_enabled)
1277+
if self.configure_plugins_action is not None:
1278+
self.configure_plugins_action.setEnabled(True)
12641279

12651280
if hasattr(self, "pluginstatus") and self.pluginstatus is not None:
12661281
self.pluginstatus.update_status()
@@ -1277,11 +1292,6 @@ def __apply_plugins_enabled_setting(self) -> None:
12771292
for panel in (self.signalpanel, self.imagepanel):
12781293
panel.acthandler.clear_plugin_actions()
12791294

1280-
PluginRegistry.clear_plugin_classes()
1281-
PluginRegistry.clear_failed_plugins()
1282-
PluginRegistry.clear_discovery_errors()
1283-
self._startup_errors.clear()
1284-
12851295
self.__update_actions(update_other_data_panel=True)
12861296
self.__update_plugins_availability()
12871297

@@ -1867,8 +1877,6 @@ def __update_actions(self, update_other_data_panel: bool = False) -> None:
18671877
panel.selection_changed()
18681878
self.signalpanel_toolbar.setVisible(is_signal)
18691879
self.imagepanel_toolbar.setVisible(not is_signal)
1870-
if self.plugins_menu is not None:
1871-
self.plugins_menu.setEnabled(Conf.main.plugins_enabled.get())
18721880

18731881
def __tab_index_changed(self, index: int) -> None:
18741882
"""Switch from signal to image mode, or vice-versa"""
@@ -1898,12 +1906,11 @@ def __update_generic_menu(self, menu: QW.QMenu | None = None) -> None:
18981906
# no plugin has registered actions yet (so that new plugins can be
18991907
# discovered after they are added on disk).
19001908
if menu is self.plugins_menu:
1901-
if Conf.main.plugins_enabled.get():
1902-
actions = list(actions) + [
1903-
None,
1904-
self.configure_plugins_action,
1905-
self.reload_plugins_action,
1906-
]
1909+
actions = list(actions) + [
1910+
None,
1911+
self.configure_plugins_action,
1912+
self.reload_plugins_action,
1913+
]
19071914
add_actions(menu, actions)
19081915

19091916
def __update_file_menu(self) -> None:
@@ -2513,8 +2520,6 @@ def __edit_settings(self) -> None: # pylint: disable=too-many-branches,too-many
25132520
self.__update_color_mode()
25142521
if option == "show_console_on_error":
25152522
self.__update_console_show_mode()
2516-
if option == "plugins_enabled":
2517-
self.__apply_plugins_enabled_setting()
25182523
if option == "plot_toolbar_position":
25192524
for dock in self.docks.values():
25202525
widget = dock.widget()

0 commit comments

Comments
 (0)