Initial commit for profile Radian
This commit is contained in:
parent
0e2a56f4e1
commit
8b686ec80e
37
pyproject.toml
Normal file
37
pyproject.toml
Normal file
@ -0,0 +1,37 @@
|
||||
# pyproject.toml
|
||||
|
||||
# This section defines the build system requirements for the project.
|
||||
# setuptools is a standard choice.
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
# This section contains the core metadata about your project.
|
||||
[project]
|
||||
name = "radian-framework"
|
||||
version = "0.1.0"
|
||||
authors = [
|
||||
{ name = "Luca Vallongo", email = "luca.vallongo@gmail.com" },
|
||||
]
|
||||
description = "A framework for Radar Data Integration & Analysis."
|
||||
readme = "README.md" # Optional: if you have a README file
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License", # Choose a license
|
||||
"Operating System :: OS Independent",
|
||||
"Development Status :: 3 - Alpha",
|
||||
]
|
||||
|
||||
# This is the most important part for our use case.
|
||||
# It lists the external libraries that RADIAN depends on to run.
|
||||
dependencies = [
|
||||
"PyYAML", # For reading the .yaml configuration files
|
||||
]
|
||||
|
||||
# This section tells setuptools where to find your actual Python packages.
|
||||
[tool.setuptools]
|
||||
packages = ["radian"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
radian = ["py.typed"]
|
||||
@ -0,0 +1,121 @@
|
||||
# radian/components/base_component.py
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
import tkinter as tk
|
||||
|
||||
# A type alias for configuration dictionaries to improve code readability.
|
||||
ConfigType = Dict[str, Any]
|
||||
|
||||
class BaseComponent(ABC):
|
||||
"""
|
||||
Abstract Base Class for all RADIAN components.
|
||||
|
||||
This class defines a common interface (a "contract") that each external
|
||||
tool must implement via an adapter class to be managed by the RADIAN framework.
|
||||
It ensures that RADIAN can interact with any component in a standardized way.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Each component instance will manage its own configuration.
|
||||
self.config: ConfigType = self.get_default_config()
|
||||
|
||||
@abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""
|
||||
Returns the user-friendly name of the component.
|
||||
This name will be displayed in the RADIAN GUI.
|
||||
|
||||
Returns:
|
||||
The string name of the component.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_description(self) -> str:
|
||||
"""
|
||||
Returns a brief description of what the component does.
|
||||
This can be used for tooltips or help text in the GUI.
|
||||
|
||||
Returns:
|
||||
A short string description.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_default_config(self) -> ConfigType:
|
||||
"""
|
||||
Returns a dictionary with the default configuration parameters
|
||||
for the component. This is used to initialize the component's state.
|
||||
|
||||
Returns:
|
||||
A dictionary representing the default configuration.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_config(self, config: ConfigType) -> None:
|
||||
"""
|
||||
Applies a new configuration to the component. This method can be
|
||||
overridden if complex logic is needed when setting a configuration.
|
||||
|
||||
Args:
|
||||
config: A dictionary with the configuration to apply.
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def get_current_config(self) -> ConfigType:
|
||||
"""
|
||||
Returns the current configuration of the component.
|
||||
|
||||
Returns:
|
||||
The current configuration dictionary.
|
||||
"""
|
||||
return self.config
|
||||
|
||||
@abstractmethod
|
||||
def get_config_ui(self, parent: tk.Frame) -> Optional[tk.Frame]:
|
||||
"""
|
||||
Creates and returns a Tkinter Frame containing the component's
|
||||
specific configuration UI elements. RADIAN will place this frame
|
||||
in the main content area.
|
||||
|
||||
If a component has no configurable parameters, this method can
|
||||
return None.
|
||||
|
||||
Args:
|
||||
parent: The parent Tkinter widget (a Frame) where the UI
|
||||
should be built.
|
||||
|
||||
Returns:
|
||||
A Tkinter Frame with all the necessary configuration widgets,
|
||||
or None if no GUI configuration is needed.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run(self, input_data: Optional[Any] = None) -> Any:
|
||||
"""
|
||||
Executes the main logic of the component.
|
||||
This is the core function for workflow execution. It can accept data
|
||||
from a previous component and return data for the next one.
|
||||
|
||||
Args:
|
||||
input_data: Data passed from a previous component in a workflow.
|
||||
|
||||
Returns:
|
||||
The output of the component's processing, to be passed to the
|
||||
next component in a workflow, or None if there is no output.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_icon_path(self) -> Optional[str]:
|
||||
"""
|
||||
Returns the absolute path to the component's icon file (e.g., .png, .ico).
|
||||
The recommended size is 32x32 or 64x64.
|
||||
|
||||
If not implemented or returns None, a default icon will be used.
|
||||
|
||||
Returns:
|
||||
A string representing the absolute path to the icon, or None.
|
||||
"""
|
||||
return None
|
||||
125
radian/components/component_manager.py
Normal file
125
radian/components/component_manager.py
Normal file
@ -0,0 +1,125 @@
|
||||
# radian/components/component_manager.py
|
||||
|
||||
import sys
|
||||
import importlib.util
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
import yaml
|
||||
|
||||
from radian.utils import logger
|
||||
from radian.components.base_component import BaseComponent
|
||||
|
||||
log = logger.get_logger(__name__)
|
||||
|
||||
# The expected name of the file containing the adapter class within a component's project.
|
||||
ADAPTER_FILENAME = "adapter.py"
|
||||
# The expected name of the adapter class. This can be made more flexible later if needed.
|
||||
ADAPTER_CLASS_NAME = "RadianAdapter"
|
||||
|
||||
|
||||
class ComponentManager:
|
||||
"""
|
||||
Discovers, loads, and manages all RADIAN components (plugins).
|
||||
|
||||
It reads a configuration file to find components and dynamically loads
|
||||
them, making them available to the rest of the application.
|
||||
"""
|
||||
def __init__(self, config_path: Path):
|
||||
self.config_path = config_path
|
||||
self.components: Dict[str, BaseComponent] = {}
|
||||
log.info(f"ComponentManager initialized with config path: {config_path}")
|
||||
|
||||
def load_components(self):
|
||||
"""
|
||||
Loads all components specified in the configuration file.
|
||||
This is the main entry point for the manager.
|
||||
"""
|
||||
log.info("Starting component discovery and loading...")
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
log.error(f"Component configuration file not found at: {self.config_path}")
|
||||
return
|
||||
except yaml.YAMLError as e:
|
||||
log.error(f"Error parsing component YAML file: {e}")
|
||||
return
|
||||
|
||||
if not config or 'components' not in config:
|
||||
log.warning("Component configuration file is empty or missing 'components' key.")
|
||||
return
|
||||
|
||||
for name, comp_config in config['components'].items():
|
||||
if 'path' in comp_config:
|
||||
log.info(f"Found component '{name}' configured with a local path.")
|
||||
self._load_component_from_path(name, Path(comp_config['path']))
|
||||
else:
|
||||
log.warning(f"Component '{name}' is missing a 'path' key. Skipping.")
|
||||
|
||||
log.info(f"Component loading complete. {len(self.components)} components loaded.")
|
||||
|
||||
def _load_component_from_path(self, name: str, component_path: Path):
|
||||
"""Dynamically loads a single component from a local directory path."""
|
||||
adapter_file_path = component_path / ADAPTER_FILENAME
|
||||
|
||||
if not adapter_file_path.is_file():
|
||||
log.error(f"Component '{name}': Adapter file '{ADAPTER_FILENAME}' not found in {component_path}. Skipping.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Create a unique module name to avoid conflicts
|
||||
module_name = f"radian.external_components.{name}"
|
||||
|
||||
# Create a module spec from the file path
|
||||
spec = importlib.util.spec_from_file_location(module_name, adapter_file_path)
|
||||
if not spec or not spec.loader:
|
||||
log.error(f"Component '{name}': Could not create module spec from {adapter_file_path}.")
|
||||
return
|
||||
|
||||
# Create a new module object
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
|
||||
# Add the component's root directory to sys.path TEMPORARILY
|
||||
# so that its internal imports (e.g., from . import logic) work.
|
||||
sys.path.insert(0, str(component_path))
|
||||
|
||||
# Execute the module's code
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# IMPORTANT: Remove the path from sys.path to avoid side effects
|
||||
sys.path.pop(0)
|
||||
|
||||
# Find the adapter class within the loaded module
|
||||
AdapterClass = self._find_adapter_class(module)
|
||||
|
||||
if AdapterClass:
|
||||
instance = AdapterClass()
|
||||
self.components[name] = instance
|
||||
log.info(f"Successfully loaded and instantiated component: '{name}'")
|
||||
else:
|
||||
log.error(f"Component '{name}': No valid adapter class inheriting from BaseComponent found in {adapter_file_path}.")
|
||||
|
||||
except Exception as e:
|
||||
# Clean up path if an error occurred during exec_module
|
||||
if str(component_path) in sys.path:
|
||||
sys.path.remove(str(component_path))
|
||||
log.error(f"Failed to load component '{name}' from {component_path}. Error: {e}", exc_info=True)
|
||||
|
||||
def _find_adapter_class(self, module) -> Optional[Type[BaseComponent]]:
|
||||
"""Inspects a module to find a class that inherits from BaseComponent."""
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
# Check if it's a class defined in this module (not imported)
|
||||
# and if it's a subclass of BaseComponent (but not BaseComponent itself)
|
||||
if obj is not BaseComponent and issubclass(obj, BaseComponent):
|
||||
return obj
|
||||
return None
|
||||
|
||||
def get_component(self, name: str) -> Optional[BaseComponent]:
|
||||
"""Returns the loaded instance of a component by its name."""
|
||||
return self.components.get(name)
|
||||
|
||||
def get_all_components(self) -> Dict[str, BaseComponent]:
|
||||
"""Returns a dictionary of all loaded components."""
|
||||
return self.components
|
||||
16
radian/config/components.yaml
Normal file
16
radian/config/components.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
# config/components.yaml
|
||||
|
||||
components:
|
||||
test_component:
|
||||
# !!! IMPORTANT: Use the actual path to the 'test_component' FOLDER you created !!!
|
||||
path: "C:/src/____GitProjects/RADIAN/radian_plugins/test_component"
|
||||
|
||||
# Comment out the other components for now, as they don't have an adapter.py yet.
|
||||
#
|
||||
# radar_data_reader:
|
||||
# path: "C:/path/to/your/radar_data_reader_project"
|
||||
# git_repo: "https://github.com/your_username/radar_data_reader.git"
|
||||
#
|
||||
# flight_monitor:
|
||||
# path: "C:/path/to/your/FlightMonitor_project"
|
||||
# git_repo: "https://github.com/your_username/FlightMonitor.git"
|
||||
@ -1,8 +1,9 @@
|
||||
# radian/core/main_controller.py
|
||||
|
||||
from pathlib import Path
|
||||
from radian.gui.main_window import MainWindow
|
||||
from radian.utils import logger
|
||||
from radian.config.logging_config import LOGGING_CONFIG
|
||||
from radian.components.component_manager import ComponentManager
|
||||
|
||||
# Get a logger for this specific module
|
||||
log = logger.get_logger(__name__)
|
||||
@ -16,26 +17,49 @@ class MainController:
|
||||
"""
|
||||
def __init__(self):
|
||||
self.app: MainWindow | None = None
|
||||
self.component_manager: ComponentManager | None = None
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Initializes and starts the RADIAN application.
|
||||
"""
|
||||
# 1. Initialize the main window (the GUI)
|
||||
self.app = MainWindow()
|
||||
|
||||
# 2. Set up the centralized logging system
|
||||
self._setup_logging()
|
||||
|
||||
# 3. Set up a graceful shutdown procedure
|
||||
# 3. Initialize and run the component manager
|
||||
self._load_components()
|
||||
|
||||
# 4. Populate the GUI with the loaded components
|
||||
self._populate_gui_with_components()
|
||||
|
||||
# 5. Set up a graceful shutdown procedure
|
||||
self.app.protocol("WM_DELETE_WINDOW", self._on_shutdown)
|
||||
|
||||
log.info("RADIAN application started.")
|
||||
|
||||
log.info("RADIAN application started.")
|
||||
log.info("Welcome to the Radar Data Integration & Analysis Network.")
|
||||
|
||||
# 4. Start the Tkinter main event loop
|
||||
# --- TEST: Log i componenti caricati ---
|
||||
if self.component_manager:
|
||||
loaded_names = self.component_manager.get_all_components().keys()
|
||||
if loaded_names:
|
||||
log.info(f"Available components: {list(loaded_names)}")
|
||||
else:
|
||||
log.warning("No components were loaded. Check config/components.yaml and paths.")
|
||||
|
||||
# 5. Start the Tkinter main event loop
|
||||
self.app.mainloop()
|
||||
|
||||
def _load_components(self):
|
||||
"""Initializes the ComponentManager and loads all plugins."""
|
||||
log.info("Initializing Component Manager...")
|
||||
# Assume config file is in 'config/components.yaml' relative to the project root
|
||||
# This needs a robust way to find the project root. For now, let's assume...
|
||||
config_path = Path(__file__).parent.parent / "config" / "components.yaml"
|
||||
self.component_manager = ComponentManager(config_path.resolve())
|
||||
self.component_manager.load_components()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Initializes the basic logger and adds the Tkinter handler."""
|
||||
# Set up the queue, processor, and file/console handlers
|
||||
@ -64,3 +88,40 @@ class MainController:
|
||||
# Destroy the main window
|
||||
if self.app:
|
||||
self.app.destroy()
|
||||
|
||||
def _populate_gui_with_components(self):
|
||||
"""Gets components from the manager and adds them to the rich GUI list."""
|
||||
if not self.component_manager or not self.app:
|
||||
return
|
||||
|
||||
all_components = self.component_manager.get_all_components()
|
||||
|
||||
for comp_id, component in all_components.items():
|
||||
self.app.add_component_to_list(
|
||||
component_id=comp_id,
|
||||
name=component.get_name(),
|
||||
icon_path=component.get_icon_path(),
|
||||
callback=self._on_component_selected # Passiamo il metodo come callback
|
||||
)
|
||||
|
||||
def _on_component_selected(self, component_id: str): # Ora riceve l'id direttamente
|
||||
"""Called when a user clicks on a component widget in the list."""
|
||||
if not self.app or not self.component_manager:
|
||||
return
|
||||
|
||||
component = self.component_manager.get_component(component_id)
|
||||
if not component:
|
||||
return
|
||||
|
||||
log.info(f"Component '{component.get_name()}' selected.")
|
||||
|
||||
# Deseleziona gli altri (feedback visivo)
|
||||
# (Implementazione più avanzata in futuro)
|
||||
|
||||
component_ui_frame = component.get_config_ui(self.app.content_frame)
|
||||
|
||||
if component_ui_frame:
|
||||
self.app.display_component_ui(component_ui_frame)
|
||||
else:
|
||||
self.app.clear_content_area()
|
||||
log.info(f"Component '{component.get_name()}' has no configuration UI.")
|
||||
@ -2,71 +2,119 @@
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from PIL import Image, ImageTk # Assicurati di aver installato Pillow: pip install Pillow
|
||||
|
||||
class MainWindow(tk.Tk):
|
||||
"""
|
||||
The main window of the RADIAN application.
|
||||
|
||||
It sets up the main application frame, including the layout for
|
||||
the components' UI and the logging area.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# --- Basic Window Configuration ---
|
||||
self.title("RADIAN - Radar Data Integration & Analysis Network")
|
||||
self._set_fullscreen()
|
||||
|
||||
# --- Main Layout Structure ---
|
||||
# The main container frame
|
||||
# Store PhotoImage objects to prevent garbage collection
|
||||
self.photo_images: Dict[str, ImageTk.PhotoImage] = {}
|
||||
|
||||
main_frame = ttk.Frame(self)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Configure the grid layout (2 rows, 1 column)
|
||||
main_frame.rowconfigure(0, weight=3) # Content area takes 3/4 of the space
|
||||
main_frame.rowconfigure(1, weight=1) # Log area takes 1/4 of the space
|
||||
main_frame.columnconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(0, weight=1, minsize=250) # Colonna lista, peso minore ma con minsize
|
||||
main_frame.columnconfigure(1, weight=5)
|
||||
|
||||
# --- Create Widgets ---
|
||||
self._create_content_area(main_frame)
|
||||
self._create_log_area(main_frame)
|
||||
self._create_components_list_area(main_frame)
|
||||
|
||||
right_panel = ttk.Frame(main_frame)
|
||||
right_panel.grid(row=0, column=1, sticky="nsew")
|
||||
right_panel.rowconfigure(0, weight=3)
|
||||
right_panel.rowconfigure(1, weight=1)
|
||||
right_panel.columnconfigure(0, weight=1)
|
||||
|
||||
self._create_content_area(right_panel)
|
||||
self._create_log_area(right_panel)
|
||||
|
||||
def _set_fullscreen(self):
|
||||
"""Sets the window to a maximized state."""
|
||||
# 'zoomed' is the standard state for a maximized window in Tkinter.
|
||||
# This is generally more reliable across different systems than using attributes.
|
||||
self.state('zoomed')
|
||||
|
||||
def _create_components_list_area(self, parent: ttk.Frame):
|
||||
list_frame = ttk.LabelFrame(parent, text="Components", padding="10")
|
||||
list_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
list_frame.rowconfigure(0, weight=1)
|
||||
list_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# --- Nuovo approccio con Canvas e Frame scrollabile ---
|
||||
canvas = tk.Canvas(list_frame)
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
|
||||
|
||||
self.scrollable_frame = ttk.Frame(canvas)
|
||||
|
||||
self.scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
||||
)
|
||||
|
||||
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
canvas.grid(row=0, column=0, sticky="nsew")
|
||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
|
||||
def add_component_to_list(self, component_id: str, name: str, icon_path: Optional[str], callback: Callable):
|
||||
"""Adds a single, rich component widget to the scrollable list."""
|
||||
|
||||
# Frame per il singolo componente
|
||||
comp_frame = ttk.Frame(self.scrollable_frame, padding=5)
|
||||
comp_frame.pack(fill="x", expand=True, pady=2)
|
||||
|
||||
# Carica l'icona
|
||||
try:
|
||||
if icon_path:
|
||||
img = Image.open(icon_path).resize((48, 48), Image.Resampling.LANCZOS)
|
||||
photo = ImageTk.PhotoImage(img)
|
||||
self.photo_images[component_id] = photo # Salva il riferimento!
|
||||
icon_label = ttk.Label(comp_frame, image=photo)
|
||||
icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10))
|
||||
else:
|
||||
# Placeholder se non c'è icona
|
||||
icon_label = ttk.Label(comp_frame, text="⚫", font=("Segoe UI", 24))
|
||||
icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10))
|
||||
except Exception as e:
|
||||
print(f"Error loading icon for {name}: {e}")
|
||||
icon_label = ttk.Label(comp_frame, text="!", font=("Segoe UI", 24))
|
||||
icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10))
|
||||
|
||||
# Nome del componente
|
||||
name_label = ttk.Label(comp_frame, text=name, font=("Segoe UI", 12, "bold"))
|
||||
name_label.grid(row=0, column=1, sticky="w")
|
||||
|
||||
# --- Eventi di Click ---
|
||||
# Usa una lambda per passare l'id del componente al callback
|
||||
click_handler = lambda e, c_id=component_id: callback(c_id)
|
||||
comp_frame.bind("<Button-1>", click_handler)
|
||||
name_label.bind("<Button-1>", click_handler)
|
||||
icon_label.bind("<Button-1>", click_handler)
|
||||
|
||||
def _create_content_area(self, parent: ttk.Frame):
|
||||
"""Creates the frame that will host the components' UI."""
|
||||
content_frame = ttk.LabelFrame(parent, text="Component View", padding="10")
|
||||
content_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# This is a placeholder. In the future, this frame will be populated
|
||||
# by the ComponentManager with the UI of the selected component.
|
||||
placeholder_label = ttk.Label(content_frame, text="Component UI will be displayed here.")
|
||||
placeholder_label.pack(expand=True)
|
||||
|
||||
self.content_frame = content_frame
|
||||
|
||||
def _create_log_area(self, parent: ttk.Frame):
|
||||
"""Creates the scrollable text widget for logging."""
|
||||
log_frame = ttk.LabelFrame(parent, text="System Log", padding="10")
|
||||
log_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# Configure grid to make the text widget fill the frame
|
||||
log_frame.rowconfigure(0, weight=1)
|
||||
log_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# The text widget for displaying logs
|
||||
log_text = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10)
|
||||
log_text.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
# The scrollbar for the text widget
|
||||
scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=log_text.yview)
|
||||
log_text_widget = tk.Text(log_frame, wrap=tk.WORD, state=tk.DISABLED, height=10)
|
||||
log_text_widget.grid(row=0, column=0, sticky="nsew")
|
||||
scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=log_text_widget.yview)
|
||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
log_text_widget.configure(yscrollcommand=scrollbar.set)
|
||||
self.log_text = log_text_widget
|
||||
|
||||
# Link the scrollbar to the text widget
|
||||
log_text.configure(yscrollcommand=scrollbar.set)
|
||||
def clear_content_area(self):
|
||||
for widget in self.content_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
self.log_text = log_text
|
||||
def display_component_ui(self, component_ui_frame: tk.Frame):
|
||||
self.clear_content_area()
|
||||
component_ui_frame.pack(fill=tk.BOTH, expand=True)
|
||||
115
radian_plugins/test_component/adapter.py
Normal file
115
radian_plugins/test_component/adapter.py
Normal file
@ -0,0 +1,115 @@
|
||||
# C:/radian_plugins/test_component/adapter.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
# This import works because RADIAN was installed in editable mode.
|
||||
from radian.components.base_component import BaseComponent, ConfigType
|
||||
from radian.utils import logger
|
||||
|
||||
# Each component should have its own logger instance.
|
||||
log = logger.get_logger(__name__)
|
||||
|
||||
|
||||
class RadianAdapter(BaseComponent):
|
||||
"""
|
||||
A dummy component adapter for testing the RADIAN loading mechanism.
|
||||
|
||||
This class implements the full BaseComponent contract.
|
||||
"""
|
||||
def __init__(self):
|
||||
log.info("TestComponent's adapter is being initialized...")
|
||||
# The call to super().__init__() is important as it sets up
|
||||
# the initial configuration by calling get_default_config().
|
||||
super().__init__()
|
||||
# NOTE: Tkinter variables are now created inside get_config_ui
|
||||
# to ensure they are fresh each time the UI is built.
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Returns the user-friendly name of the component."""
|
||||
return "Test Component"
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""Returns a brief description of what the component does."""
|
||||
return "A dummy component for demonstrating and testing the plugin system."
|
||||
|
||||
def get_default_config(self) -> ConfigType:
|
||||
"""Defines the default configuration parameters for this component."""
|
||||
return {
|
||||
"test_parameter_1": "default_string_value",
|
||||
"test_parameter_2": True
|
||||
}
|
||||
|
||||
def get_icon_path(self) -> Optional[str]:
|
||||
"""Returns the path to this component's icon."""
|
||||
# The icon is expected to be in the same directory as this adapter file.
|
||||
icon_path = Path(__file__).parent / "test_component.png"
|
||||
if icon_path.exists():
|
||||
return str(icon_path)
|
||||
log.warning(f"Icon not found at: {icon_path}")
|
||||
return None
|
||||
|
||||
def get_config_ui(self, parent: tk.Frame) -> Optional[tk.Frame]:
|
||||
"""Creates the configuration UI for this test component."""
|
||||
log.debug(f"Creating config UI for '{self.get_name()}'")
|
||||
|
||||
frame = ttk.Frame(parent, padding="10")
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
# --- Initialize Tkinter variables locally within this method ---
|
||||
# This ensures they are new for each UI creation and avoids conflicts
|
||||
# with previously destroyed widgets.
|
||||
param1_var = tk.StringVar()
|
||||
param2_var = tk.BooleanVar()
|
||||
|
||||
# --- UI Widgets ---
|
||||
label1 = ttk.Label(frame, text="Test Parameter 1 (String):")
|
||||
label1.grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
|
||||
entry1 = ttk.Entry(frame, textvariable=param1_var)
|
||||
entry1.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||
|
||||
check2 = ttk.Checkbutton(frame, text="Enable Test Feature", variable=param2_var)
|
||||
check2.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
|
||||
|
||||
# --- Nested helper functions to manage UI state ---
|
||||
# These functions have access to the local tkinter variables.
|
||||
def update_ui_from_config():
|
||||
"""Helper to sync the UI with the current self.config state."""
|
||||
param1_var.set(self.config.get("test_parameter_1", ""))
|
||||
param2_var.set(self.config.get("test_parameter_2", False))
|
||||
|
||||
def update_config_from_ui(*args):
|
||||
"""Helper to sync self.config with the current UI state."""
|
||||
self.config["test_parameter_1"] = param1_var.get()
|
||||
self.config["test_parameter_2"] = param2_var.get()
|
||||
log.info(f"TestComponent config updated from UI: {self.config}")
|
||||
|
||||
# --- Load current config into UI variables ---
|
||||
update_ui_from_config()
|
||||
|
||||
# --- Bind UI changes back to the config dictionary ---
|
||||
param1_var.trace_add("write", update_config_from_ui)
|
||||
param2_var.trace_add("write", update_config_from_ui)
|
||||
|
||||
return frame
|
||||
|
||||
def run(self, input_data: Optional[any] = None) -> any:
|
||||
"""Executes the dummy logic of this component."""
|
||||
log.info(f"--- Running '{self.get_name()}' ---")
|
||||
if input_data:
|
||||
log.info(f"Received input data: {input_data}")
|
||||
|
||||
log.info(f"Using configuration: {self.config}")
|
||||
|
||||
if self.config.get("test_parameter_2"):
|
||||
log.info("Test Feature is ENABLED.")
|
||||
else:
|
||||
log.info("Test Feature is DISABLED.")
|
||||
|
||||
output_message = f"Output from {self.get_name()} with param1='{self.config.get('test_parameter_1')}'"
|
||||
log.info("--- Finished running ---")
|
||||
|
||||
return output_message
|
||||
Loading…
Reference in New Issue
Block a user