diff --git a/plugins/README.txt b/plugins/README.txt index dfbb0f9..a85e24c 100644 --- a/plugins/README.txt +++ b/plugins/README.txt @@ -10,22 +10,22 @@ class Manifest: author: str = "John Doe" version: str = "0.0.1" support: str = "" + pre_launch: bool = False requests: {} = { 'pass_ui_objects': ["plugin_control_list"], - 'pass_events': "true", + 'pass_events': True, 'bind_keys': [] } - pre_launch: bool = False ``` ### Requests ``` requests: {} = { - 'pass_events': "true", # If empty or not present will be ignored. - "pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin. + 'pass_events': true, # If empty or not present will be ignored. + "pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin. 'bind_keys': [f"{name}||send_message:f"], - f"{name}||do_save:s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right. + f"{name}||do_save:s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right. } ``` diff --git a/plugins/template/manifest.json b/plugins/template/manifest.json index 1f8c8a5..374fab8 100644 --- a/plugins/template/manifest.json +++ b/plugins/template/manifest.json @@ -6,7 +6,7 @@ "support": "", "requests": { "ui_target": "plugin_control_list", - "pass_events": "true", + "pass_events": true, "bind_keys": ["Example Plugin||send_message:f"] } } diff --git a/src/core/widgets/code/tab_widget.py b/src/core/widgets/code/tab_widget.py index cb3ab07..f1cc6c2 100644 --- a/src/core/widgets/code/tab_widget.py +++ b/src/core/widgets/code/tab_widget.py @@ -49,7 +49,7 @@ class TabWidget(Gtk.Box): self.add(self._label_eve_box) self.add(self.close_btn) - def __del__(self): + def clear_signals_and_data(self): del self.file self._label_eve_box.disconnect(self._label_eve_box_id) self.close_btn.disconnect(self.close_btn_id) diff --git a/src/core/widgets/code/tabs_widget.py b/src/core/widgets/code/tabs_widget.py index 8e7c865..208f44a 100644 --- a/src/core/widgets/code/tabs_widget.py +++ b/src/core/widgets/code/tabs_widget.py @@ -94,6 +94,7 @@ class TabsWidget(Gtk.ScrolledWindow): child.file.remove_observer(self) self.tabs.remove(child) + child.clear_signals_and_data() del child return diff --git a/src/plugins/dto/__init__.py b/src/plugins/dto/__init__.py new file mode 100644 index 0000000..2e533d6 --- /dev/null +++ b/src/plugins/dto/__init__.py @@ -0,0 +1,3 @@ +""" + Gtk Plugins DTO Module +""" diff --git a/src/plugins/dto/manifest.py b/src/plugins/dto/manifest.py new file mode 100644 index 0000000..54c7f26 --- /dev/null +++ b/src/plugins/dto/manifest.py @@ -0,0 +1,27 @@ +# Python imports +from dataclasses import dataclass, field +from dataclasses import asdict + +# Gtk imports + +# Application imports +from .requests import Requests + + + +@dataclass +class Manifest: + name: str = "" + author: str = "" + credit: str = "" + version: str = "0.0.1" + support: str = "support@mail.com" + pre_launch: bool = False + requests: Requests = field(default_factory = lambda: Requests()) + + def __post_init__(self): + if isinstance(self.requests, dict): + self.requests = Requests(**self.requests) + + def as_dict(self): + return asdict(self) diff --git a/src/plugins/dto/manifest_meta.py b/src/plugins/dto/manifest_meta.py new file mode 100644 index 0000000..8e1056b --- /dev/null +++ b/src/plugins/dto/manifest_meta.py @@ -0,0 +1,19 @@ +# Python imports +from dataclasses import dataclass, field +from dataclasses import asdict + +# Gtk imports + +# Application imports +from .manifest import Manifest + + + +@dataclass +class ManifestMeta: + folder: str = "" + path: str = "" + manifest: Manifest = field(default_factory = lambda: Manifest()) + + def as_dict(self): + return asdict(self) diff --git a/src/plugins/dto/requests.py b/src/plugins/dto/requests.py new file mode 100644 index 0000000..d20ab41 --- /dev/null +++ b/src/plugins/dto/requests.py @@ -0,0 +1,16 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass +class Requests: + ui_target: str = "" + ui_target_id: str = "" + pass_events: bool = False + pass_fm_events: bool = False + pass_ui_objects: list = field(default_factory = lambda: []) + bind_keys: list = field(default_factory = lambda: []) diff --git a/src/plugins/manifest.py b/src/plugins/manifest.py deleted file mode 100644 index 7cb701c..0000000 --- a/src/plugins/manifest.py +++ /dev/null @@ -1,75 +0,0 @@ -# Python imports -import os -import json -from os.path import join - -# Lib imports - -# Application imports - - - - -class ManifestProcessor(Exception): - ... - - -class Plugin: - path: str = None - name: str = None - author: str = None - version: str = None - support: str = None - requests:{} = None - reference: type = None - pre_launch: bool = False - - -class ManifestProcessor: - def __init__(self, path, builder): - manifest = join(path, "manifest.json") - if not os.path.exists(manifest): - raise Exception("Invalid Plugin Structure: Plugin doesn't have 'manifest.json'. Aboarting load...") - - self._path = path - self._builder = builder - with open(manifest) as f: - data = json.load(f) - self._manifest = data["manifest"] - self._plugin = self.collect_info() - - def collect_info(self) -> Plugin: - plugin = Plugin() - plugin.path = self._path - plugin.name = self._manifest["name"] - plugin.author = self._manifest["author"] - plugin.version = self._manifest["version"] - plugin.support = self._manifest["support"] - plugin.requests = self._manifest["requests"] - - if "pre_launch" in self._manifest.keys(): - plugin.pre_launch = True if self._manifest["pre_launch"] == "true" else False - - return plugin - - def get_loading_data(self): - loading_data = {} - requests = self._plugin.requests - - if "pass_events" in requests: - if requests["pass_events"] in ["true"]: - loading_data["pass_events"] = True - - if "pass_ui_objects" in requests: - if isinstance(requests["pass_ui_objects"], list): - loading_data["pass_ui_objects"] = [ self._builder.get_object(obj) for obj in requests["pass_ui_objects"] ] - - if "bind_keys" in requests: - if isinstance(requests["bind_keys"], list): - loading_data["bind_keys"] = requests["bind_keys"] - - return self._plugin, loading_data - - def is_pre_launch(self): - return self._plugin.pre_launch - diff --git a/src/plugins/manifest_manager.py b/src/plugins/manifest_manager.py new file mode 100644 index 0000000..ae4f3bb --- /dev/null +++ b/src/plugins/manifest_manager.py @@ -0,0 +1,68 @@ +# Python imports +import os +import json +from os.path import join + +# Lib imports + +# Application imports +from .dto.manifest_meta import ManifestMeta +from .dto.manifest import Manifest + + + +class ManifestMapperException(Exception): + ... + + + +class ManifestManager: + def __init__(self): + + self._plugins_path = settings_manager.get_plugins_path() + + self.pre_launch_manifests = [] + self.post_launch_manifests = [] + + self.load_manifests() + + + def load_manifests(self): + logger.info(f"Loading manifests...") + + for path, folder in [ + [join(self._plugins_path, item), item] + if + os.path.isdir( join(self._plugins_path, item) ) + else + None + for item in os.listdir(self._plugins_path) + ]: + self.load(folder, path) + + def load(self, folder, path): + manifest_pth = join(path, "manifest.json") + + if not os.path.exists(manifest_pth): + raise ManifestMapperException("Invalid Plugin Structure: Plugin doesn't have 'manifest.json'. Aboarting load...") + + with open(manifest_pth) as f: + data = json.load(f) + manifest = Manifest(**data) + manifest_meta = ManifestMeta() + + manifest_meta.folder = folder + manifest_meta.path = path + manifest_meta.manifest = manifest + + if manifest.pre_launch: + self.pre_launch_manifests.append(manifest_meta) + else: + self.post_launch_manifests.append(manifest_meta) + + def get_pre_launch_manifests(self) -> dict: + return self.pre_launch_manifests + + def get_post_launch_plugins(self) -> None: + return self.post_launch_manifests + diff --git a/src/plugins/plugin_base.py b/src/plugins/plugin_base.py index 3650495..7a63917 100644 --- a/src/plugins/plugin_base.py +++ b/src/plugins/plugin_base.py @@ -15,12 +15,12 @@ class PluginBaseException(Exception): class PluginBase: def __init__(self, **kwargs): super().__init__(**kwargs) - self.name = "Example Plugin" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus - # where self.name should not be needed for message comms + self.name = "Example Plugin" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus + # where self.name should not be needed for message comms - self._builder = None - self._ui_objects = None - self._event_system = None + self._builder = None + self._ui_objects = None + self._event_system = None def run(self): @@ -48,8 +48,8 @@ class PluginBase: def set_event_system(self, event_system): """ - Requests Key: 'pass_events': "true" - Must define in plugin if "pass_events" is set to "true" string. + Requests Key: 'pass_events': true + Must define in plugin if "pass_events" is set to true. """ self._event_system = event_system diff --git a/src/plugins/plugin_reload_mixin.py b/src/plugins/plugin_reload_mixin.py new file mode 100644 index 0000000..2b256ff --- /dev/null +++ b/src/plugins/plugin_reload_mixin.py @@ -0,0 +1,36 @@ +# Python imports + +# Lib imports +import gi +from gi.repository import Gio + +# Application imports + + + +class PluginReloadMixin: + _plugins_dir_watcher = None + + def _set_plugins_watcher(self) -> None: + self._plugins_dir_watcher = Gio.File.new_for_path( + self._plugins_path + ).monitor_directory( + Gio.FileMonitorFlags.WATCH_MOVES, + Gio.Cancellable() + ) + + self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ()) + + def _on_plugins_changed(self, + file_monitor, file, + other_file = None, + eve_type = None, + data = None + ): + if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, + Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, + Gio.FileMonitorEvent.MOVED_OUT]: + self.reload_plugins(file) + + def reload_plugins(self, file: str = None) -> None: + logger.info(f"Reloading plugins... stub.") diff --git a/src/plugins/plugins_controller.py b/src/plugins/plugins_controller.py index 10d5dc2..72b5c73 100644 --- a/src/plugins/plugins_controller.py +++ b/src/plugins/plugins_controller.py @@ -8,15 +8,12 @@ from os.path import isdir # Lib imports import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk from gi.repository import GLib -from gi.repository import Gio # Application imports -from .manifest import Plugin -from .manifest import ManifestProcessor - +from .dto.manifest_meta import ManifestMeta +from .plugin_reload_mixin import PluginReloadMixin +from .manifest_manager import ManifestManager @@ -24,95 +21,59 @@ class InvalidPluginException(Exception): ... -class PluginsController: + +class PluginsController(PluginReloadMixin): """PluginsController controller""" def __init__(self): - path = os.path.dirname(os.path.realpath(__file__)) - sys.path.insert(0, path) # NOTE: I think I'm not using this correctly... + # path = os.path.dirname(os.path.realpath(__file__)) + # sys.path.insert(0, path) # NOTE: I think I'm not using this correctly... - self._builder = settings_manager.get_builder() - self._plugins_path = settings_manager.get_plugins_path() - - self._plugins_dir_watcher = None self._plugin_collection = [] - self._plugin_manifests = {} - self._load_manifests() - - - def _load_manifests(self): - logger.info(f"Loading manifests...") - - for path, folder in [[join(self._plugins_path, item), item] if os.path.isdir(join(self._plugins_path, item)) else None for item in os.listdir(self._plugins_path)]: - manifest = ManifestProcessor(path, self._builder) - self._plugin_manifests[path] = { - "path": path, - "folder": folder, - "manifest": manifest - } + self._plugins_path = settings_manager.get_plugins_path() + self._manifest_manager = ManifestManager() self._set_plugins_watcher() - def _set_plugins_watcher(self) -> None: - self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \ - .monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable()) - self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ()) - - def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None): - if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, - Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, - Gio.FileMonitorEvent.MOVED_OUT]: - self.reload_plugins(file) def pre_launch_plugins(self) -> None: logger.info(f"Loading pre-launch plugins...") - plugin_manifests: {} = {} - - for key in self._plugin_manifests: - target_manifest = self._plugin_manifests[key]["manifest"] - if target_manifest.is_pre_launch(): - plugin_manifests[key] = self._plugin_manifests[key] - - self._load_plugins(plugin_manifests, is_pre_launch = True) + manifest_metas: [] = self._manifest_manager.get_pre_launch_manifests() + self._load_plugins(manifest_metas, is_pre_launch = True) def post_launch_plugins(self) -> None: logger.info(f"Loading post-launch plugins...") - plugin_manifests: {} = {} + manifest_metas: [] = self._manifest_manager.get_post_launch_plugins() + self._load_plugins(manifest_metas) - for key in self._plugin_manifests: - target_manifest = self._plugin_manifests[key]["manifest"] - if not target_manifest.is_pre_launch(): - plugin_manifests[key] = self._plugin_manifests[key] - - self._load_plugins(plugin_manifests) - - def _load_plugins(self, plugin_manifests: {} = {}, is_pre_launch: bool = False) -> None: + def _load_plugins( + self, + manifest_metas: [] = [], + is_pre_launch: bool = False + ) -> None: parent_path = os.getcwd() - for key in plugin_manifests: - target_manifest = plugin_manifests[key] - path, folder, manifest = target_manifest["path"], target_manifest["folder"], target_manifest["manifest"] + for manifest_meta in manifest_metas: + path, folder, manifest = manifest_meta.path, manifest_meta.folder, manifest_meta.manifest try: target = join(path, "plugin.py") if not os.path.exists(target): raise InvalidPluginException("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...") - plugin, loading_data = manifest.get_loading_data() - module = self.load_plugin_module(path, folder, target) + module = self.load_plugin_module(path, folder, target) if is_pre_launch: - self.execute_plugin(module, plugin, loading_data) + self.execute_plugin(module, manifest_meta) else: - GLib.idle_add(self.execute_plugin, *(module, plugin, loading_data)) + GLib.idle_add(self.execute_plugin, module, manifest_meta) except Exception as e: logger.info(f"Malformed Plugin: Not loading -->: '{folder}' !") logger.debug("Trace: ", traceback.print_exc()) os.chdir(parent_path) - def load_plugin_module(self, path, folder, target): os.chdir(path) @@ -126,33 +87,48 @@ class PluginsController: return module - def collect_search_locations(self, path, locations): + def collect_search_locations(self, path: str, locations: list): locations.append(path) for file in os.listdir(path): _path = os.path.join(path, file) if os.path.isdir(_path): self.collect_search_locations(_path, locations) - def execute_plugin(self, module: type, plugin: Plugin, loading_data: []): - plugin.reference = module.Plugin() - keys = loading_data.keys() + def execute_plugin(self, module: type, manifest_meta: ManifestMeta): + manifest = manifest_meta.manifest + manifest_meta.instance = module.Plugin() - if "ui_target" in keys: - loading_data["ui_target"].add( plugin.reference.generate_reference_ui_element() ) - loading_data["ui_target"].show() + if manifest.requests.ui_target: + builder = settings_manager.get_builder() + ui_target = manifest.requests.ui_target + ui_target_id = manifest.requests.ui_target_id - if "pass_ui_objects" in keys: - plugin.reference.set_ui_object_collection( loading_data["pass_ui_objects"] ) + if not ui_target == "other": + ui_target = builder.get_object(ui_target) + else: + if not ui_target_id: + raise InvalidPluginException('Invalid "ui_target_id" given in requests. Must have one if setting "ui_target" to "other"...') - if "pass_events" in keys: - plugin.reference.set_event_system(event_system) - plugin.reference.subscribe_to_events() + ui_target = builder.get_object(ui_target_id) - if "bind_keys" in keys: - keybindings.append_bindings( loading_data["bind_keys"] ) + if not ui_target: + raise InvalidPluginException('Unknown "ui_target" given in requests.') - plugin.reference.run() - self._plugin_collection.append(plugin) + ui_element = manifest_meta.instance.generate_reference_ui_element() - def reload_plugins(self, file: str = None) -> None: - logger.info(f"Reloading plugins... stub.") + ui_target.add(ui_element) + + if manifest.requests.pass_ui_objects: + manifest_meta.instance.set_ui_object_collection( + [ builder.get_object(obj) for obj in manifest.requests.pass_ui_objects ] + ) + + if manifest.requests.pass_events: + manifest_meta.instance.set_event_system(event_system) + manifest_meta.instance.subscribe_to_events() + + if manifest.requests.bind_keys: + keybindings.append_bindings( manifest.requests.bind_keys ) + + manifest_meta.instance.run() + self._plugin_collection.append(manifest_meta)