From 28483e2b6cda5e5c2abe73bc3d1d0de95af9dcc1 Mon Sep 17 00:00:00 2001 From: Rob <5183487+Rexeh@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:01:13 +0000 Subject: [PATCH] Begin moving out plugin handling from UI layer, now done with PluginWrappers owning this pre-load of UI --- joystick_diagrams/__main__.py | 2 +- joystick_diagrams/app_init.py | 18 ++++- joystick_diagrams/app_state.py | 33 +++----- .../plugins/dcs_world_plugin/main.py | 1 + joystick_diagrams/plugins/plugin_interface.py | 3 +- .../ui/mock_main/configure_page.py | 4 +- .../ui/mock_main/plugin_settings.py | 80 ++++++++++--------- .../ui/mock_main/setting_page.py | 70 ++++++++-------- joystick_diagrams/ui/plugin_wrapper.py | 72 ++++++++++++++++- 9 files changed, 183 insertions(+), 100 deletions(-) diff --git a/joystick_diagrams/__main__.py b/joystick_diagrams/__main__.py index 08fc10e..be7e75a 100644 --- a/joystick_diagrams/__main__.py +++ b/joystick_diagrams/__main__.py @@ -28,7 +28,7 @@ window = MainWindow() window.show() - apply_stylesheet(app, theme="dark_blue.xml", invert_secondary=False) + apply_stylesheet(app, theme="light_blue.xml", invert_secondary=True) app.exec() except Exception as error: # pylint: disable=broad-except diff --git a/joystick_diagrams/app_init.py b/joystick_diagrams/app_init.py index 3baf5b7..6651269 100644 --- a/joystick_diagrams/app_init.py +++ b/joystick_diagrams/app_init.py @@ -1,15 +1,31 @@ +import logging + from joystick_diagrams.app_state import AppState +from joystick_diagrams.db import db_init from joystick_diagrams.plugin_manager import ParserPluginManager +_logger = logging.getLogger(__name__) + def init(): + # TODO log and orchestrate this better + + # Setup datastore + db_init.init() + # ------------------------------- + # Get global state object _state = AppState() + # ------------------------------- + + # -- Initialise Plugins System -- - # Initialise Plugins System plugins = ParserPluginManager() + plugins.load_discovered_plugins() _state.init_plugins(plugins) + # ------------------------------- + if __name__ == "__main__": pass diff --git a/joystick_diagrams/app_state.py b/joystick_diagrams/app_state.py index f64c67b..071b745 100644 --- a/joystick_diagrams/app_state.py +++ b/joystick_diagrams/app_state.py @@ -21,9 +21,7 @@ def __new__(cls, *args, **kwargs): def _init(self) -> None: self.plugin_manager: ParserPluginManager | None = None - self.plugin_profile_collections: list - self.raw_profiles: list = [] # TODO repalce with parsed profiles from Plugins - self.profileObjectMapping = self.update_profile_object_mapping() + self.profileObjectMapping: dict[str, Profile_] = {} self.profileParentMapping: dict[str, list[str]] = {} # TODO correctly initialise these self.processedProfileObjectMapping: dict[str, Profile_] = {} # TODO Think here about name colissions self.update_processed_profiles() @@ -31,21 +29,18 @@ def _init(self) -> None: def init_plugins(self, plugin_manager: ParserPluginManager): self.plugin_manager = plugin_manager - def process_loaded_plugins(self): - self.plugin_profile_collections = self.plugin_manager.process_loaded_plugins() - # TODO clean up - self.raw_profiles.clear() - for pc in self.plugin_profile_collections: - name = pc[0] - for profile in pc[1].profiles.values(): - # TODO tidy up and push up - profile.name = f"{profile.name} - {name}" - self.raw_profiles.append(profile) - self.profileObjectMapping = self.update_profile_object_mapping() - self.update_processed_profiles() + def process_loaded_plugins(self, profile_collections: dict[str, ProfileCollection]): + # Clear existing processed profiles + self.profileObjectMapping.clear() + + for profile_source, profiles in profile_collections.items(): + for profile_name, profile_obj in profiles.profiles.items(): + combined_name = f"{profile_source} - {profile_name}" + self.profileObjectMapping[combined_name] = profile_obj - def update_profile_object_mapping(self): - return {x.name: x for x in self.raw_profiles} + _logger.debug(f"Loaded plugins resulted in the following profiles being detected {self.profileObjectMapping}") + + self.update_processed_profiles() def get_processed_profile(self, profile_identifier: str) -> Profile_: """Return inherited profile for given Profile Identifier.""" @@ -133,6 +128,4 @@ def profile_mock(self): if __name__ == "__main__": - _appState = AppState() - _appState.update_processed_profiles() - print(_appState) + pass diff --git a/joystick_diagrams/plugins/dcs_world_plugin/main.py b/joystick_diagrams/plugins/dcs_world_plugin/main.py index ea07d5f..92d0557 100644 --- a/joystick_diagrams/plugins/dcs_world_plugin/main.py +++ b/joystick_diagrams/plugins/dcs_world_plugin/main.py @@ -15,6 +15,7 @@ class ParserPlugin(PluginInterface): def __init__(self): + super().__init__() self.settings = settings self.settings.validators.register() self.path = None diff --git a/joystick_diagrams/plugins/plugin_interface.py b/joystick_diagrams/plugins/plugin_interface.py index 7d985ed..25c2550 100644 --- a/joystick_diagrams/plugins/plugin_interface.py +++ b/joystick_diagrams/plugins/plugin_interface.py @@ -32,6 +32,7 @@ def __init__(self, dialog_title: str, default_path: str, supported_extensions: l @property @abstractmethod def path_type(self) -> FolderPath | FilePath: + """Returns a valid path type object to specify the plugins path sourcing method.""" ... def file_not_valid_exception(self, exception_message: str): @@ -71,7 +72,7 @@ def name(self) -> str: @property def version(self) -> str: - """Returns a version property""" + """Returns a valid path type object""" """Returns a version property""" return f"{self.settings.VERSION}" @property diff --git a/joystick_diagrams/ui/mock_main/configure_page.py b/joystick_diagrams/ui/mock_main/configure_page.py index d0c9c1b..e42906f 100644 --- a/joystick_diagrams/ui/mock_main/configure_page.py +++ b/joystick_diagrams/ui/mock_main/configure_page.py @@ -33,8 +33,8 @@ def get_profiles(self): def initialise_available_profiles(self): self.profileList.clear() profiles = self.get_profiles() - for i in profiles.values(): - self.profileList.addItem(i.name) + for key in profiles.keys(): + self.profileList.addItem(key) def initialise_customise_binds(self): profiles = self.appState.get_processed_profiles() diff --git a/joystick_diagrams/ui/mock_main/plugin_settings.py b/joystick_diagrams/ui/mock_main/plugin_settings.py index 99e92c5..b7ef599 100644 --- a/joystick_diagrams/ui/mock_main/plugin_settings.py +++ b/joystick_diagrams/ui/mock_main/plugin_settings.py @@ -35,54 +35,81 @@ def __init__(self, *args, **kwargs): self.plugin = None # Attributes - # Connections self.pluginEnabled.stateChanged.connect(self.handle_enabled_change) self.configureLink.clicked.connect(self.open_file_dialog) + self.pluginModified.connect(self.trigger_plugin_save) # Setup - # Style Overrides - self.configureLink.setStyleSheet("color: white !important;") def setup(self): """Initialise the widget configuration""" # Setup labels self.pluginEnabled.setChecked(self.plugin.enabled) - self.plugin_name_label.setText(self.plugin.plugin_name) - self.plugin_version_label.setText(self.plugin.plugin_version) + self.plugin_name_label.setText(self.plugin.name) + self.plugin_version_label.setText(self.plugin.version) # Setup file selection - self.configureLink.setText(self.get_plugin_path_type().dialog_title) - self.configureLink.setDescription("") + if self.plugin.path: + self.configureLink.setText(RECONFIGURE_TEXT) + self.configureLink.setDescription(self.plugin.path.__str__()) + else: + self.configureLink.setText(self.get_plugin_path_type().dialog_title) + self.configureLink.setDescription("") + + # Handle plugin procesing depending on its enabled state + self.handle_plugin_enabled_state() + + self.plugin.plugin_loaded = True + + def handle_plugin_enabled_state(self): + """Setup the plugin based on the enabled state. Only fully load enabled plugins for safety""" + state = self.plugin.enabled - if self.plugin.plugin.path: - self.set_plugin_path(self.plugin.plugin.path) + match state: + case 0: + self.configureLink.setDisabled(1) - # TODO if file already set from storage... + case 1: + self.configureLink.setDisabled(0) + case _: + self.configureLink.setDisabled(1) + + def trigger_plugin_save(self): + self.plugin.store_plugin_configuration() + + @Slot() def handle_enabled_change(self, data): + # Set the enabled state based on data self.plugin.enabled = data if data == 0 else 1 - print(f"Plugin enabled state is now {self.plugin.enabled}") - self.pluginModified.emit(self.plugin) - def get_plugin_file_extensions(self): - return [] + # Process based on the new state + self.handle_plugin_enabled_state() + + _logger.debug(f"Plugin enabled state is now {self.plugin.enabled}") + + self.pluginModified.emit(self.plugin) def get_plugin_path_type(self) -> PluginInterface.FilePath | PluginInterface.FolderPath: - return self.plugin.plugin.path_type + return self.plugin.path_type def set_plugin_path(self, path: Path): if not isinstance(path, Path): + _logger.error(f"Plugin path for {self.plugin.name} was not a Path object") return try: - load = self.plugin.plugin.set_path(path) + _logger.debug(f"Atempting path set for plugin {self.plugin.name}") + load = self.plugin.set_path(path) if not load: + _logger.error(f"An error occured seting the path for {self.plugin.name}") raise JoystickDiagramsException("Error loading plugin") if load: + _logger.info(f"Path successfully set for {self.plugin.name}") self.configureLink.setText(RECONFIGURE_TEXT) self.configureLink.setDescription(path.__str__()) self.pluginPathConfigured.emit(self.plugin) @@ -118,27 +145,6 @@ def open_file_dialog(self) -> None: case _: _logger.error("Unexpected plugin path type given.") - # def load_plugin_settings(self, data): - - -# self.pluginVersionInfo.setText(f"Version {data.version}") -# self.pluginName.setText(f"{data.name} Settings") - -# # Path Setup -# self.pluginPath.setText(data.path) - -# # Prevents duplicate signals from being connected -# if self.pluginPathButton.isSignalConnected(QMetaMethod.fromSignal(self.pluginPathButton.clicked)): -# self.pluginPathButton.clicked.disconnect() - -# # Clean up -# # Additional validation to be added to PluginInterface/Plugin loading -# if isinstance(data.path_type, PluginInterface.FilePath): -# self.pluginPathButton.clicked.connect(lambda: self.file_dialog(data.path_type)) - -# if isinstance(data.path_type, PluginInterface.FolderPath): -# self.pluginPathButton.clicked.connect(lambda: self.folder_dialog(data.path_type)) - if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/joystick_diagrams/ui/mock_main/setting_page.py b/joystick_diagrams/ui/mock_main/setting_page.py index d197b1d..7abe1c5 100644 --- a/joystick_diagrams/ui/mock_main/setting_page.py +++ b/joystick_diagrams/ui/mock_main/setting_page.py @@ -16,6 +16,7 @@ from joystick_diagrams.app_state import AppState from joystick_diagrams.db import db_init, db_plugin_data from joystick_diagrams.exceptions import JoystickDiagramsException +from joystick_diagrams.input.profile_collection import ProfileCollection from joystick_diagrams.plugins.plugin_interface import PluginInterface from joystick_diagrams.ui.mock_main.plugin_settings import PluginSettings from joystick_diagrams.ui.mock_main.qt_designer import setting_page_ui @@ -25,7 +26,7 @@ class PluginsPage(QMainWindow, setting_page_ui.Ui_Form): # Refactor pylint: disable=too-many-instance-attributes - parsePlugins = Signal() + profileCollectionChange = Signal() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -33,12 +34,13 @@ def __init__(self, *args, **kwargs): self.appState = AppState() # Attributes - self.plugin_wrappers = [] + self.plugin_wrappers: list[PluginWrapper] = [] self.window_content = None # Connections self.parserPluginList.itemClicked.connect(self.plugin_selected) - self.parsePlugins.connect(self.execute_plugin_parsers) + self.parserPluginList.itemChanged.connect(self.plugin_selected) + self.profileCollectionChange.connect(self.update_profile_collections) # Setup self.remove_defaults() @@ -50,71 +52,71 @@ def __init__(self, *args, **kwargs): def remove_defaults(self): self.parserPluginList.clear() - def get_plugin_configuration(self, plugin_name: str): - return db_plugin_data.get_plugin_configuration(plugin_name) - - @Slot() - def store_plugin_configuration(self, plugin_object: PluginWrapper): - db_plugin_data.add__update_plugin_configuration(plugin_object.plugin_name, plugin_object.enabled) - def initialise_plugins(self): """Initialise the available plugins into wrapper objects for use in UI. PluginWrapper enriches the Plugin model with UI specific data """ for plugin in self.appState.plugin_manager.get_available_plugins(): - plugin_lookup = self.get_plugin_configuration(plugin.name) - - if plugin_lookup: - enabled_flag = plugin_lookup[1] - self.plugin_wrappers.append( - PluginWrapper(plugin.name, plugin.version, plugin.icon, plugin, enabled=enabled_flag) - ) - else: - # Create Wrapper - wrapper = PluginWrapper(plugin.name, plugin.version, plugin.icon, plugin, enabled=False) - # Store the plugin with default of FALSE for enabled - self.store_plugin_configuration(wrapper) - self.plugin_wrappers.append(wrapper) + self.plugin_wrappers.append(PluginWrapper(plugin)) def populate_available_plugin_list(self): for plugin_data in self.plugin_wrappers: - item = QListWidgetItem(QIcon(plugin_data.plugin_icon), plugin_data.plugin_name) + item = QListWidgetItem(QIcon(plugin_data.icon), plugin_data.name) item.setData(Qt.UserRole, plugin_data) self.parserPluginList.addItem(item) + self.pre_intiialise_plugin_wrappers() + + def pre_intiialise_plugin_wrappers(self): + """Sets up the wrappers in the UI, so that users don't need to click on them individually + + This could be done better but for now it works + """ + for item in range(self.parserPluginList.count()): + self.parserPluginList.setCurrentRow(item) + self.plugin_selected() + @Slot() - def plugin_selected(self, item): + def plugin_selected(self): if self.window_content: self.window_content.hide() self.window_content = PluginSettings() + # Signals/Slots + self.window_content.pluginPathConfigured.connect(self.handle_plugin_path_load) + # Page Setup For now self.window_content.plugin = self.get_selected_plugin_object() - self.window_content.setup() - # Signals/Slots - self.window_content.pluginModified.connect(self.store_plugin_configuration) - self.window_content.pluginPathConfigured.connect(self.handle_plugin_path_load) + self.window_content.setup() self.window_content.setParent(self.pluginOptionsWidget) self.window_content.show() @Slot() def handle_plugin_path_load(self, plugin: PluginWrapper): - print(f"Data is {plugin}") + _logger.debug(f"Plugin path changed for {plugin}, attempting to process plugin") try: plugin.plugin_profile_collection = plugin.plugin.process() - print(f"Data is {plugin.plugin_profile_collection}") + self.profileCollectionChange.emit() except JoystickDiagramsException: pass + def get_plugin_wrapper_collections(self) -> dict[str, ProfileCollection]: + """Returns a list of Profile Collections that are tagged with the Plugin Name where the plugin is enabled""" + return { + x.name: x.plugin_profile_collection + for x in self.plugin_wrappers + if x.enabled and x.plugin_profile_collection + } + @Slot() - def execute_plugin_parsers(self): - print("Executing plugin parsers") - self.appState.process_loaded_plugins() + def update_profile_collections(self): + _logger.debug(f"Updating profile collections from all plugins") + self.appState.process_loaded_plugins(self.get_plugin_wrapper_collections()) def get_selected_plugin_object(self) -> PluginInterface: return self.parserPluginList.currentItem().data(Qt.UserRole) diff --git a/joystick_diagrams/ui/plugin_wrapper.py b/joystick_diagrams/ui/plugin_wrapper.py index f71e8b5..ee323d7 100644 --- a/joystick_diagrams/ui/plugin_wrapper.py +++ b/joystick_diagrams/ui/plugin_wrapper.py @@ -1,18 +1,82 @@ from dataclasses import dataclass, field from pathlib import Path +from db import db_plugin_data + from joystick_diagrams.input.profile_collection import ProfileCollection from joystick_diagrams.plugins.plugin_interface import PluginInterface @dataclass class PluginWrapper: - plugin_name: str - plugin_version: str - plugin_icon: Path + plugin: PluginInterface + _enabled: bool = field(init=False) plugin_profile_collection: ProfileCollection = field(init=False) - enabled: bool def __post_init__(self): self.plugin_profile_collection = None + self.setup_plugin() + + def process(self): + if self.path: + self.plugin_profile_collection = self.plugin.process() + + def set_path(self, path: Path) -> bool: + set = self.plugin.set_path(path) + return set + + def setup_plugin(self): + """Sets up a pluginf or first use or restores existing state""" + + existing_configuration = self.get_plugin_configuration(self.plugin.name) + + if existing_configuration: + self.enabled = existing_configuration[1] + else: + self.store_plugin_configuration() + + self.plugin.load_settings() + + print(self.plugin.path) + + def get_plugin_configuration(self, plugin_name: str): + return db_plugin_data.get_plugin_configuration(plugin_name) + + def store_plugin_configuration(self): + db_plugin_data.add__update_plugin_configuration(self.name, self.enabled) + + @property + def path(self): + print(f"Getting path for {self.name} which is {self.plugin.path}") + return self.plugin.path + + @property + def name(self): + return self.plugin.name + + @property + def version(self): + return self.plugin.version + + @property + def icon(self): + return self.plugin.icon + + @property + def path_type(self): + return self.plugin.path_type + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, value): + + self._enabled = False if isinstance(value, property) else bool(value) + + if self._enabled is True: + self.process() + print("Processed the plugin on enabling") + return self.enabled