diff --git a/dronecan_gui_tool/main.py b/dronecan_gui_tool/main.py index 611ffc2..ef32054 100644 --- a/dronecan_gui_tool/main.py +++ b/dronecan_gui_tool/main.py @@ -15,6 +15,12 @@ assert sys.version[0] == '3' +def parse_load_modules(argv): + modules = [m.strip() for m in argv.split(",") if m.strip()] + if not modules: + return None + return modules + from argparse import ArgumentParser parser = ArgumentParser(description='DroneCAN GUI tool') @@ -28,6 +34,8 @@ parser.add_argument("--filtered", action='store_true', help="enable filtering of DroneCAN traffic") parser.add_argument("--target-system", help="set the targetted system", type=int, default=0) +parser.add_argument("--load-module", type=parse_load_modules, nargs=1, help="Comma-separated list of modules to load (e.g. mod1,mod2).") # exactly one argument required if flag is present + args = parser.parse_args() # @@ -95,7 +103,18 @@ from .widgets.can_adapter_control_panel import spawn_window as spawn_can_adapter_control_panel from .panels import PANELS +from .panels import import_panel +EXT_PLUGINS = [] +modules = args.load_module[0] if args.load_module else [] +if len(modules) > 0: + for module in modules: + try: + panel = import_panel(module) + EXT_PLUGINS.append(panel) + except Exception as ex: + print(f"Unable to load {module}: {ex}") + print(f"Loaded {len(EXT_PLUGINS)} plugin modules!") NODE_NAME = 'org.dronecan.gui_tool' @@ -204,6 +223,39 @@ def __init__(self, node, iface_name, iface_kwargs): action.triggered.connect(lambda state, panel=panel: panel.safe_spawn(self, self._node)) panels_menu.addAction(action) + # + # External Modules menu + # + def get_or_create_submenu(parent_menu, menu_name): + """ + Find a submenu with menu_name under parent_menu, or create it if not found. + """ + for action in parent_menu.actions(): + submenu = action.menu() + if submenu and submenu.title() == menu_name: + return submenu + # Not found, create new submenu + return parent_menu.addMenu(menu_name) + + if len(EXT_PLUGINS) > 0: + extern_modules_menu = self.menuBar().addMenu('P&lugins') + for idx, panel in enumerate(EXT_PLUGINS): + menu_path = getattr(panel, "menu_path", "") + path_parts = [p for p in menu_path.split("/") if p] + + current_menu = extern_modules_menu + for part in path_parts: + current_menu = get_or_create_submenu(current_menu, part) + + action = QAction(panel.name, self) + icon = panel.get_icon() + if icon: + action.setIcon(icon) + if idx < 9: + action.setShortcut(QKeySequence(f'Ctrl+Shift+[,{(idx + 1)}')) + action.triggered.connect(lambda state, panel=panel: panel.safe_spawn(self, self._node)) + current_menu.addAction(action) + # # Help menu # diff --git a/dronecan_gui_tool/panels/__init__.py b/dronecan_gui_tool/panels/__init__.py index b49fdf2..207d9e0 100644 --- a/dronecan_gui_tool/panels/__init__.py +++ b/dronecan_gui_tool/panels/__init__.py @@ -18,6 +18,8 @@ from . import hobbywing_esc from . import rc_panel +import importlib.util + class PanelDescriptor: def __init__(self, module): self.name = module.PANEL_NAME @@ -36,6 +38,23 @@ def safe_spawn(self, parent, node): except Exception as ex: show_error('Panel error', 'Could not spawn panel', ex) +def import_panel(name): + """Given a package name like 'foo.bar.quux', imports the package + and returns the desired module.""" + spec = importlib.util.find_spec(name) + mod = None + if spec is None: + raise Exception(f"Module '{name}' not found!") + else: + mod = importlib.import_module(name) + print(f"Successfully imported {name} from {mod.__file__}") + return PluginPanelDescriptor(mod) + +class PluginPanelDescriptor(PanelDescriptor): + def __init__(self, module): + super().__init__(module) + + self.menu_path = getattr(module, "MENU_PATH", "") PANELS = [ PanelDescriptor(esc_panel),