Initial commit
This commit is contained in:
3
src/plugins/__init__.py
Normal file
3
src/plugins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Gtk Bound Plugins Module
|
||||
"""
|
68
src/plugins/manifest.py
Normal file
68
src/plugins/manifest.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
return plugin
|
||||
|
||||
def get_loading_data(self):
|
||||
loading_data = {}
|
||||
requests = self._plugin.requests
|
||||
keys = requests.keys()
|
||||
|
||||
if "pass_events" in keys:
|
||||
if requests["pass_events"] in ["true"]:
|
||||
loading_data["pass_events"] = True
|
||||
|
||||
if "bind_keys" in keys:
|
||||
if isinstance(requests["bind_keys"], list):
|
||||
loading_data["bind_keys"] = requests["bind_keys"]
|
||||
|
||||
if "pass_ui_objects" in keys:
|
||||
if isinstance(requests["pass_ui_objects"], list):
|
||||
loading_data["pass_ui_objects"] = [ self._builder.get_object(obj) for obj in requests["pass_ui_objects"] ]
|
||||
|
||||
return self._plugin, loading_data
|
92
src/plugins/plugin_base.py
Normal file
92
src/plugins/plugin_base.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Python imports
|
||||
import os
|
||||
import time
|
||||
import inspect
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
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._builder = None
|
||||
self._ui_objects = None
|
||||
self._event_system = None
|
||||
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Must define regardless if needed and can 'pass' if plugin doesn't need it.
|
||||
Is intended to be used to setup internal signals or custom Gtk Builders/UI logic.
|
||||
"""
|
||||
raise PluginBaseException("Method hasn't been overriden...")
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
"""
|
||||
Requests Key: 'ui_target': "plugin_control_list",
|
||||
Must define regardless if needed and can 'pass' if plugin doesn't use it.
|
||||
Must return a widget if "ui_target" is set.
|
||||
"""
|
||||
raise PluginBaseException("Method hasn't been overriden...")
|
||||
|
||||
def set_ui_object_collection(self, ui_objects):
|
||||
"""
|
||||
Requests Key: "pass_ui_objects": [""]
|
||||
Request reference to a UI component. Will be passed back as array to plugin.
|
||||
Must define in plugin if set and an array of valid glade UI IDs is given.
|
||||
"""
|
||||
self._ui_objects = ui_objects
|
||||
|
||||
def set_event_system(self, event_system):
|
||||
"""
|
||||
Requests Key: 'pass_events': "true"
|
||||
Must define in plugin if "pass_events" is set to "true" string.
|
||||
"""
|
||||
self._event_system = event_system
|
||||
|
||||
def subscribe_to_events(self):
|
||||
...
|
||||
|
||||
def _connect_builder_signals(self, caller_class, builder):
|
||||
classes = [caller_class]
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = None
|
||||
try:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
except Exception as e:
|
||||
logger.debug(repr(e))
|
||||
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
def reload_package(self, plugin_path, module_dict_main=locals()):
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
def reload_package_recursive(current_dir, module_dict):
|
||||
for path in current_dir.iterdir():
|
||||
if "__init__" in str(path) or path.stem not in module_dict:
|
||||
continue
|
||||
|
||||
if path.is_file() and path.suffix == ".py":
|
||||
importlib.reload(module_dict[path.stem])
|
||||
elif path.is_dir():
|
||||
reload_package_recursive(path, module_dict[path.stem].__dict__)
|
||||
|
||||
reload_package_recursive(Path(plugin_path).parent, module_dict_main["module_dict_main"])
|
||||
|
||||
|
||||
def clear_children(self, widget: type) -> None:
|
||||
""" Clear children of a gtk widget. """
|
||||
for child in widget.get_children():
|
||||
widget.remove(child)
|
119
src/plugins/plugins_controller.py
Normal file
119
src/plugins/plugins_controller.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Python imports
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import traceback
|
||||
from os.path import join
|
||||
from os.path import isdir
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
|
||||
# Application imports
|
||||
from .manifest import Plugin
|
||||
from .manifest import ManifestProcessor
|
||||
|
||||
|
||||
|
||||
|
||||
class InvalidPluginException(Exception):
|
||||
...
|
||||
|
||||
|
||||
class PluginsController:
|
||||
"""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...
|
||||
|
||||
self._builder = settings_manager.get_builder()
|
||||
self._plugins_path = settings_manager.get_plugins_path()
|
||||
|
||||
self._plugins_dir_watcher = None
|
||||
self._plugin_collection = []
|
||||
|
||||
|
||||
def launch_plugins(self) -> None:
|
||||
self._set_plugins_watcher()
|
||||
self.load_plugins()
|
||||
|
||||
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 load_plugins(self, file: str = None) -> None:
|
||||
logger.info(f"Loading plugins...")
|
||||
parent_path = os.getcwd()
|
||||
|
||||
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)]:
|
||||
try:
|
||||
target = join(path, "plugin.py")
|
||||
manifest = ManifestProcessor(path, self._builder)
|
||||
|
||||
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)
|
||||
self.execute_plugin(module, plugin, loading_data)
|
||||
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)
|
||||
|
||||
locations = []
|
||||
self.collect_search_locations(path, locations)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(folder, target, submodule_search_locations = locations)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[folder] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
return module
|
||||
|
||||
def collect_search_locations(self, path, locations):
|
||||
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()
|
||||
|
||||
if "ui_target" in keys:
|
||||
loading_data["ui_target"].add( plugin.reference.generate_reference_ui_element() )
|
||||
loading_data["ui_target"].show()
|
||||
|
||||
if "pass_ui_objects" in keys:
|
||||
plugin.reference.set_ui_object_collection( loading_data["pass_ui_objects"] )
|
||||
|
||||
if "pass_events" in keys:
|
||||
plugin.reference.set_event_system(event_system)
|
||||
plugin.reference.subscribe_to_events()
|
||||
|
||||
if "bind_keys" in keys:
|
||||
keybindings.append_bindings( loading_data["bind_keys"] )
|
||||
|
||||
plugin.reference.run()
|
||||
self._plugin_collection.append(plugin)
|
||||
|
||||
def reload_plugins(self, file: str = None) -> None:
|
||||
logger.info(f"Reloading plugins... stub.")
|
Reference in New Issue
Block a user