From bbd8fd7a2c05842eff15882552b823be551dbbe7 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Fri, 22 May 2026 18:27:16 -0500 Subject: [PATCH] Moved mirage2 to use newer GTK template structure patterns --- src/__builtins__.py | 61 ++++-- src/__init__.py | 4 +- src/__main__.py | 24 ++- src/app.py | 59 +++--- src/core/__init__.py | 4 +- src/core/containers/__init__.py | 4 +- src/core/containers/base_container.py | 40 +++- src/core/containers/body_container.py | 47 +++++ src/core/containers/center_container.py | 46 +++++ src/core/containers/footer_container.py | 44 ++++ src/core/containers/header_container.py | 44 ++++ src/core/containers/left_container.py | 44 ++++ src/core/containers/mirage/__init__.py | 3 + .../{ => mirage}/image_list_scroll.py | 9 +- .../{ => mirage}/image_view_evebox.py | 4 +- .../{ => mirage}/image_view_scroll.py | 16 +- src/core/containers/{ => mirage}/left_box.py | 2 +- src/core/containers/{ => mirage}/right_box.py | 8 +- src/core/containers/right_container.py | 44 ++++ src/core/controller.py | 60 ------ src/core/controllers/__init__.py | 3 + src/core/controllers/base_controller.py | 88 ++++++++ .../base_controller_mixin.py} | 51 +++-- src/core/controllers/bridge_controller.py | 41 ++++ src/core/mixins/__init__.py | 3 - src/core/mixins/signals/__init__.py | 3 - src/core/mixins/signals/ipc_signals_mixin.py | 22 -- src/core/mixins/signals_mixins.py | 13 -- src/core/widgets/button_controls.py | 20 +- src/core/widgets/image.py | 8 +- src/core/widgets/image_list.py | 17 +- src/core/widgets/image_view.py | 35 ++-- src/core/widgets/ocr_window.py | 6 +- src/core/widgets/path_label.py | 4 +- src/core/widgets/separator_widget.py | 36 ++++ src/core/window.py | 129 ++++++++---- src/libs/__init__.py | 4 +- src/libs/command_system.py | 50 +++++ src/libs/controllers/__init__.py | 3 + src/libs/controllers/controller_base.py | 47 +++++ src/libs/controllers/controller_manager.py | 74 +++++++ .../controllers/controller_message_bus.py | 33 +++ src/libs/controllers/emit_dispatcher.py | 29 +++ src/libs/db/__init__.py | 6 + src/libs/db/db.py | 42 ++++ src/libs/db/models.py | 15 ++ src/libs/debugging.py | 20 +- src/libs/dto/__init__.py | 5 + src/libs/dto/base_event.py | 16 ++ src/libs/dto/plugins/__init__.py | 3 + src/libs/dto/plugins/manifest.py | 30 +++ src/libs/dto/plugins/manifest_meta.py | 19 ++ src/libs/dto/plugins/requests.py | 11 + src/libs/endpoint_registry.py | 22 -- src/libs/event_factory.py | 100 +++++++++ src/libs/event_system.py | 29 ++- src/libs/ipc_server.py | 57 ++++-- src/libs/keybindings.py | 11 + src/libs/mixins/__init__.py | 3 + src/libs/mixins/dnd_mixin.py | 70 +++++++ src/libs/mixins/ipc_signals_mixin.py | 37 ++++ .../mixins}/keyboard_signals_mixin.py | 74 ++++--- src/libs/mixins/observable_mixin.py | 26 +++ src/libs/settings/__init__.py | 4 +- src/libs/settings/manager.py | 125 ++++++++++++ src/libs/settings/options/__init__.py | 8 + src/libs/settings/options/config.py | 42 ++++ src/libs/settings/options/debugging.py | 12 ++ src/libs/settings/options/filters.py | 90 +++++++++ src/libs/settings/options/settings.py | 31 +++ src/libs/settings/options/theming.py | 16 ++ src/libs/settings/path_manager.py | 124 ++++++++++++ src/libs/settings/settings.py | 189 ------------------ src/libs/settings/start_check_mixin.py | 12 +- src/libs/singleton.py | 22 +- src/libs/singleton_raised.py | 29 +++ src/libs/status_icon.py | 67 +++++++ src/libs/widget_registery.py | 65 ++++++ src/plugins/__init__.py | 2 + src/plugins/controller.py | 182 +++++++++++++++++ src/plugins/manifest.py | 64 ------ src/plugins/manifest_manager.py | 75 +++++++ src/plugins/plugin_base.py | 61 ------ src/plugins/plugin_context.py | 42 ++++ src/plugins/plugin_reload_mixin.py | 77 +++++++ src/plugins/plugin_types/__init__.py | 7 + src/plugins/plugin_types/plugin_base.py | 46 +++++ src/plugins/plugin_types/plugin_code.py | 50 +++++ src/plugins/plugin_types/plugin_ui.py | 44 ++++ src/plugins/plugins_controller.py | 119 ----------- src/plugins/plugins_controller_mixin.py | 20 ++ src/plugins/plugins_ui.py | 122 +++++++++++ .../usr/share/mirage2/Main_Window.glade | 28 +++ .../usr/share/mirage2/contexct_menu.json | 3 +- 94 files changed, 2846 insertions(+), 814 deletions(-) create mode 100644 src/core/containers/body_container.py create mode 100644 src/core/containers/center_container.py create mode 100644 src/core/containers/footer_container.py create mode 100644 src/core/containers/header_container.py create mode 100644 src/core/containers/left_container.py create mode 100644 src/core/containers/mirage/__init__.py rename src/core/containers/{ => mirage}/image_list_scroll.py (87%) rename src/core/containers/{ => mirage}/image_view_evebox.py (96%) rename src/core/containers/{ => mirage}/image_view_scroll.py (90%) rename src/core/containers/{ => mirage}/left_box.py (87%) rename src/core/containers/{ => mirage}/right_box.py (78%) create mode 100644 src/core/containers/right_container.py delete mode 100644 src/core/controller.py create mode 100644 src/core/controllers/__init__.py create mode 100644 src/core/controllers/base_controller.py rename src/core/{controller_data.py => controllers/base_controller_mixin.py} (55%) create mode 100644 src/core/controllers/bridge_controller.py delete mode 100644 src/core/mixins/__init__.py delete mode 100644 src/core/mixins/signals/__init__.py delete mode 100644 src/core/mixins/signals/ipc_signals_mixin.py delete mode 100644 src/core/mixins/signals_mixins.py create mode 100644 src/core/widgets/separator_widget.py create mode 100644 src/libs/command_system.py create mode 100644 src/libs/controllers/__init__.py create mode 100644 src/libs/controllers/controller_base.py create mode 100644 src/libs/controllers/controller_manager.py create mode 100644 src/libs/controllers/controller_message_bus.py create mode 100644 src/libs/controllers/emit_dispatcher.py create mode 100644 src/libs/db/__init__.py create mode 100644 src/libs/db/db.py create mode 100644 src/libs/db/models.py create mode 100644 src/libs/dto/__init__.py create mode 100644 src/libs/dto/base_event.py create mode 100644 src/libs/dto/plugins/__init__.py create mode 100644 src/libs/dto/plugins/manifest.py create mode 100644 src/libs/dto/plugins/manifest_meta.py create mode 100644 src/libs/dto/plugins/requests.py delete mode 100644 src/libs/endpoint_registry.py create mode 100644 src/libs/event_factory.py create mode 100644 src/libs/mixins/__init__.py create mode 100644 src/libs/mixins/dnd_mixin.py create mode 100644 src/libs/mixins/ipc_signals_mixin.py rename src/{core/mixins/signals => libs/mixins}/keyboard_signals_mixin.py (58%) create mode 100644 src/libs/mixins/observable_mixin.py create mode 100644 src/libs/settings/manager.py create mode 100644 src/libs/settings/options/__init__.py create mode 100644 src/libs/settings/options/config.py create mode 100644 src/libs/settings/options/debugging.py create mode 100644 src/libs/settings/options/filters.py create mode 100644 src/libs/settings/options/settings.py create mode 100644 src/libs/settings/options/theming.py create mode 100644 src/libs/settings/path_manager.py delete mode 100644 src/libs/settings/settings.py create mode 100644 src/libs/singleton_raised.py create mode 100644 src/libs/status_icon.py create mode 100644 src/libs/widget_registery.py create mode 100644 src/plugins/controller.py delete mode 100644 src/plugins/manifest.py create mode 100644 src/plugins/manifest_manager.py delete mode 100644 src/plugins/plugin_base.py create mode 100644 src/plugins/plugin_context.py create mode 100644 src/plugins/plugin_reload_mixin.py create mode 100644 src/plugins/plugin_types/__init__.py create mode 100644 src/plugins/plugin_types/plugin_base.py create mode 100644 src/plugins/plugin_types/plugin_code.py create mode 100644 src/plugins/plugin_types/plugin_ui.py delete mode 100644 src/plugins/plugins_controller.py create mode 100644 src/plugins/plugins_controller_mixin.py create mode 100644 src/plugins/plugins_ui.py create mode 100644 user_config/usr/share/mirage2/Main_Window.glade diff --git a/src/__builtins__.py b/src/__builtins__.py index 1b6f792..049e448 100644 --- a/src/__builtins__.py +++ b/src/__builtins__.py @@ -1,50 +1,73 @@ # Python imports import builtins +import traceback import threading +import sys # Lib imports # Application imports -from libs.event_system import EventSystem -from libs.endpoint_registry import EndpointRegistry -from libs.keybindings import Keybindings from libs.logger import Logger -from libs.settings import Settings +from libs.event_system import EventSystem +from libs.keybindings import Keybindings +from libs.settings.manager import SettingsManager +from libs.widget_registery import WidgetRegisteryController # NOTE: Threads WILL NOT die with parent's destruction. def threaded_wrapper(fn): def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start() + thread = threading.Thread(target = fn, args = args, kwargs = kwargs, daemon = False) + thread.start() + return thread return wrapper # NOTE: Threads WILL die with parent's destruction. def daemon_threaded_wrapper(fn): def wrapper(*args, **kwargs): - threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start() + thread = threading.Thread(target = fn, args = args, kwargs = kwargs, daemon = True) + thread.start() + return thread return wrapper -def sizeof_fmt_def(num, suffix="B"): - for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]: - if abs(num) < 1024.0: - return f"{num:3.1f} {unit}{suffix}" - num /= 1024.0 - return f"{num:.1f} Yi{suffix}" +def call_chain_wrapper(fn): + def wrapper(*args, **kwargs): + for line in traceback.format_stack(): + print( line.strip() ) + + return fn(*args, **kwargs) + return wrapper # NOTE: Just reminding myself we can add to builtins two different ways... # __builtins__.update({"event_system": Builtins()}) builtins.APP_NAME = "Mirage2" + builtins.keybindings = Keybindings() builtins.event_system = EventSystem() -builtins.endpoint_registry = EndpointRegistry() -builtins.settings = Settings() -builtins.logger = Logger(settings.get_home_config_path(), \ - _ch_log_lvl=settings.get_ch_log_lvl(), \ - _fh_log_lvl=settings.get_fh_log_lvl()).get_logger() +builtins.settings_manager = SettingsManager() +builtins.widget_registery = WidgetRegisteryController() + +settings_manager.load_settings() + +builtins.logger = Logger( + settings_manager.path_manager.get_home_config_path(), \ + _ch_log_lvl = settings_manager.settings.debugging.ch_log_lvl, \ + _fh_log_lvl = settings_manager.settings.debugging.fh_log_lvl + ).get_logger() builtins.threaded = threaded_wrapper builtins.daemon_threaded = daemon_threaded_wrapper -builtins.sizeof_fmt = sizeof_fmt_def -builtins.event_sleep_time = 0.05 \ No newline at end of file +builtins.call_chain = call_chain_wrapper + + + +def custom_except_hook(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logger.error("Uncaught exception", exc_info = (exc_type, exc_value, exc_traceback)) + +# sys.excepthook = custom_except_hook diff --git a/src/__init__.py b/src/__init__.py index 90dc8da..6de34b8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,3 @@ """ - Start of package. -""" + Src Package. +""" \ No newline at end of file diff --git a/src/__main__.py b/src/__main__.py index c7cac99..b496871 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -4,12 +4,12 @@ import argparse import faulthandler import traceback -from setproctitle import setproctitle import tracemalloc tracemalloc.start() # Lib imports +from setproctitle import setproctitle # Application imports from __builtins__ import * @@ -17,8 +17,9 @@ from app import Application -def main(args, unknownargs): +def main(): setproctitle(f'{APP_NAME}') + settings_manager.set_start_load_time() if args.debug == "true": settings_manager.set_debug(True) @@ -26,8 +27,10 @@ def main(args, unknownargs): if args.trace_debug == "true": settings_manager.set_trace_debug(True) - settings.do_dirty_start_check() - Application(args, unknownargs) + settings_manager.do_dirty_start_check() + + app = Application() + app.run() @@ -36,19 +39,20 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() # Add long and short arguments - parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.") - parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.") - parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.") + parser.add_argument("--debug", "-d", default = "false", help = "Do extra console messaging.") + parser.add_argument("--trace-debug", "-td", default = "false", help = "Disable saves, ignore IPC lock, do extra console messaging.") + parser.add_argument("--no-plugins", "-np", default = "false", help = "Do not load plugins.") - parser.add_argument("--new-tab", "-nt", default="false", help="Opens a 'New Tab' if a handler is set for it.") - parser.add_argument("--file", "-f", default="default", help="JUST SOME FILE ARG.") + parser.add_argument("--new-tab", "-nt", default = "false", help = "Opens a 'New Tab' if a handler is set for it.") + parser.add_argument("--file", "-f", default = "default", help = "JUST SOME FILE ARG.") # Read arguments (If any...) args, unknownargs = parser.parse_known_args() + settings_manager.set_starting_args( args, unknownargs ) try: faulthandler.enable() # For better debug info - main(args, unknownargs) + main() except Exception as e: traceback.print_exc() quit() \ No newline at end of file diff --git a/src/app.py b/src/app.py index d28ec3f..261c8a6 100644 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,8 @@ # Python imports +from contextlib import suppress import signal import os +import json # Lib imports @@ -19,27 +21,44 @@ class AppLaunchException(Exception): class Application: """ docstring for Application. """ - def __init__(self, args, unknownargs): + def __init__(self): super(Application, self).__init__() - if not settings.is_trace_debug(): - self.load_ipc(args, unknownargs) - self.setup_debug_hook() - Window(args, unknownargs).main() - def load_ipc(self, args, unknownargs): - ipc_server = IPCServer() + def run(self): + if not settings_manager.is_trace_debug(): + if not self.load_ipc(): + return + + win = Window() + win.start() + + def load_ipc(self): + args, \ + unknownargs = settings_manager.get_starting_args() + ipc_server = IPCServer() + self.ipc_realization_check(ipc_server) + if ipc_server.is_ipc_alive: + return True - if not ipc_server.is_ipc_alive: - for arg in unknownargs + [args.new_tab,]: - if os.path.isfile(arg): - message = f"FILE|{arg}" - ipc_server.send_ipc_message(message) + logger.warning(f"{APP_NAME} IPC Server Exists: Have sent path(s) to it and closing...") + files: list = [] + for arg in unknownargs + [args.new_tab,]: + if os.path.isfile(arg): + files.append(f"file://{arg}") - raise AppLaunchException(f"{APP_NAME} IPC Server Exists: Have sent path(s) to it and closing...") + if os.path.isdir(arg): + message = f"DIR|{arg}" + ipc_server.send_ipc_message(message) + + if files: + message = f"FILES|{json.dumps(files)}" + ipc_server.send_ipc_message(message) + + return False def ipc_realization_check(self, ipc_server): try: @@ -47,18 +66,12 @@ class Application: except Exception: ipc_server.send_test_ipc_message() - try: - ipc_server.create_ipc_listener() - except Exception as e: - ... - def setup_debug_hook(self): - try: + # Typically: ValueError: signal only works in main thread + with suppress(ValueError): # kill -SIGUSR2 from Linux/Unix or SIGBREAK signal from Windows signal.signal( - vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"), + vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR2"), debug_signal_handler ) - except ValueError: - # Typically: ValueError: signal only works in main thread - ... \ No newline at end of file + diff --git a/src/core/__init__.py b/src/core/__init__.py index 90cfadc..6c4fff7 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1,3 +1,3 @@ """ - Gtk Bound Signal Module -""" + Core Package +""" \ No newline at end of file diff --git a/src/core/containers/__init__.py b/src/core/containers/__init__.py index 4efd4b9..c59e952 100644 --- a/src/core/containers/__init__.py +++ b/src/core/containers/__init__.py @@ -1,3 +1,3 @@ """ - Containers Module -""" + Containers Package +""" \ No newline at end of file diff --git a/src/core/containers/base_container.py b/src/core/containers/base_container.py index abf1fdd..cada0ec 100644 --- a/src/core/containers/base_container.py +++ b/src/core/containers/base_container.py @@ -6,8 +6,9 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk # Application imports -from .left_box import LeftBox -from .right_box import RightBox +from .header_container import HeaderContainer +from .body_container import BodyContainer +from .footer_container import FooterContainer @@ -17,19 +18,38 @@ class BaseContainer(Gtk.Box): self._setup_styling() self._setup_signals() - self._load_widgets() + self._subscribe_to_events() - self.show_all() + self.show() def _setup_styling(self): - self.set_orientation(Gtk.Orientation.HORIZONTAL) - ctx = self.get_style_context() - ctx.add_class("container-padding-5px") + self.ctx = self.get_style_context() + self.ctx.add_class("base-container") + + self.set_orientation(Gtk.Orientation.VERTICAL) + self._update_transparency() def _setup_signals(self): - ... + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + event_system.subscribe("update-transparency", self._update_transparency) + event_system.subscribe("remove-transparency", self._remove_transparency) + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() def _load_widgets(self): - self.add(LeftBox()) - self.add(RightBox()) + widget_registery.expose_object("base-container", self) + + self.add( HeaderContainer() ) + self.add( BodyContainer() ) + self.add( FooterContainer() ) + + def _update_transparency(self): + self.ctx.add_class(f"mw_transparency_{settings_manager.settings.theming.transparency}") + + def _remove_transparency(self): + self.ctx.remove_class(f"mw_transparency_{settings_manager.settings.theming.transparency}") \ No newline at end of file diff --git a/src/core/containers/body_container.py b/src/core/containers/body_container.py new file mode 100644 index 0000000..1a1f3d2 --- /dev/null +++ b/src/core/containers/body_container.py @@ -0,0 +1,47 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from .left_container import LeftContainer +from .center_container import CenterContainer +from .right_container import RightContainer + + + +class BodyContainer(Gtk.Box): + def __init__(self): + super(BodyContainer, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + + self.show() + + + def _setup_styling(self): + self.ctx = self.get_style_context() + self.ctx.add_class("body-container") + + self.set_orientation(Gtk.Orientation.HORIZONTAL) + + def _setup_signals(self): + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + widget_registery.expose_object("body-container", self) + + self.add( LeftContainer() ) + self.add( CenterContainer() ) + self.add( RightContainer() ) \ No newline at end of file diff --git a/src/core/containers/center_container.py b/src/core/containers/center_container.py new file mode 100644 index 0000000..8e7e348 --- /dev/null +++ b/src/core/containers/center_container.py @@ -0,0 +1,46 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from .mirage.left_box import LeftBox +from .mirage.right_box import RightBox + + + +class CenterContainer(Gtk.Box): + def __init__(self): + super(CenterContainer, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + + self.show() + + + def _setup_styling(self): + self.ctx = self.get_style_context() + self.ctx.add_class("center-container") + + self.set_orientation(Gtk.Orientation.HORIZONTAL) + self.set_hexpand(True) + self.set_vexpand(True) + self.set_size_request(320, -1) + + def _setup_signals(self): + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + self.add(LeftBox()) + self.add(RightBox()) diff --git a/src/core/containers/footer_container.py b/src/core/containers/footer_container.py new file mode 100644 index 0000000..8f99836 --- /dev/null +++ b/src/core/containers/footer_container.py @@ -0,0 +1,44 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from core.widgets.separator_widget import Separator + + + +class FooterContainer(Gtk.Box): + def __init__(self): + super(FooterContainer, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + + self.show() + + + def _setup_styling(self): + self.ctx = self.get_style_context() + self.ctx.add_class("footer-container") + + self.set_orientation(Gtk.Orientation.VERTICAL) + self.set_hexpand(True) + + def _setup_signals(self): + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + widget_registery.expose_object("footer-container", self) + + self.add( Separator("separator-footer", 0) ) diff --git a/src/core/containers/header_container.py b/src/core/containers/header_container.py new file mode 100644 index 0000000..372cf12 --- /dev/null +++ b/src/core/containers/header_container.py @@ -0,0 +1,44 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from core.widgets.separator_widget import Separator + + + +class HeaderContainer(Gtk.Box): + def __init__(self): + super(HeaderContainer, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + + self.show() + + + def _setup_styling(self): + self.ctx = self.get_style_context() + self.ctx.add_class("header-container") + + self.set_orientation(Gtk.Orientation.VERTICAL) + self.set_hexpand(True) + + def _setup_signals(self): + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + widget_registery.expose_object("header-container", self) + + self.add( Separator("separator-header", 0) ) diff --git a/src/core/containers/left_container.py b/src/core/containers/left_container.py new file mode 100644 index 0000000..0e86fbb --- /dev/null +++ b/src/core/containers/left_container.py @@ -0,0 +1,44 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from core.widgets.separator_widget import Separator + + + +class LeftContainer(Gtk.Box): + def __init__(self): + super(LeftContainer, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + + self.show() + + + def _setup_styling(self): + self.ctx = self.get_style_context() + self.ctx.add_class("left-container") + + self.set_orientation(Gtk.Orientation.HORIZONTAL) + self.set_vexpand(True) + + def _setup_signals(self): + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + widget_registery.expose_object("left-container", self) + + self.add( Separator("separator-left", 1) ) diff --git a/src/core/containers/mirage/__init__.py b/src/core/containers/mirage/__init__.py new file mode 100644 index 0000000..bcde3fe --- /dev/null +++ b/src/core/containers/mirage/__init__.py @@ -0,0 +1,3 @@ +""" + Containers > Mirage Package +""" \ No newline at end of file diff --git a/src/core/containers/image_list_scroll.py b/src/core/containers/mirage/image_list_scroll.py similarity index 87% rename from src/core/containers/image_list_scroll.py rename to src/core/containers/mirage/image_list_scroll.py index a39a62d..91c526d 100644 --- a/src/core/containers/image_list_scroll.py +++ b/src/core/containers/mirage/image_list_scroll.py @@ -6,7 +6,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk # Application imports -from ..widgets.image_list import ImageList +from core.widgets.image_list import ImageList @@ -17,7 +17,7 @@ class ImageListScroll(Gtk.ScrolledWindow): self.image_list_widget = None self.size = 0 self.start = 0 - self.end = settings.get_max_ring_thumbnail_list() + self.end = settings_manager.settings.config.max_ring_thumbnail_list self._setup_styling() self._setup_signals() @@ -36,7 +36,7 @@ class ImageListScroll(Gtk.ScrolledWindow): self.connect("edge-overshot", self._handle_edge_reached) def _subscribe_to_events(self): - event_system.subscribe("update_list_size_constraints", self._update_list_size_constraints) + event_system.subscribe("update-list-size-constraints", self._update_list_size_constraints) def _load_widgets(self): self.image_list_widget = ImageList() @@ -46,7 +46,7 @@ class ImageListScroll(Gtk.ScrolledWindow): def _update_list_size_constraints(self, size): self.size = size self.start = 0 - self.end = settings.get_max_ring_thumbnail_list() + self.end = settings_manager.settings.config.max_ring_thumbnail_list def _handle_edge_reached(self, widget, edge): children = self.image_list_widget.get_children() @@ -75,5 +75,4 @@ class ImageListScroll(Gtk.ScrolledWindow): def _unload_image(self, child): child.hide() - # child.image.clear() child.is_loaded = False \ No newline at end of file diff --git a/src/core/containers/image_view_evebox.py b/src/core/containers/mirage/image_view_evebox.py similarity index 96% rename from src/core/containers/image_view_evebox.py rename to src/core/containers/mirage/image_view_evebox.py index ff8e0f9..c0c18dc 100644 --- a/src/core/containers/image_view_evebox.py +++ b/src/core/containers/mirage/image_view_evebox.py @@ -8,7 +8,7 @@ from gi.repository import Gtk from gi.repository import Gdk # Application imports -from ..widgets.image_view import ImageView +from core.widgets.image_view import ImageView @@ -86,6 +86,6 @@ class ImageViewEveBox(Gtk.EventBox): self._drag_start_y = 0 def set_cursor(self, type = None): - window = settings.get_main_window() + window = settings_manager.get_main_window() cursor = Gdk.Cursor(type) window.get_window().set_cursor(cursor) diff --git a/src/core/containers/image_view_scroll.py b/src/core/containers/mirage/image_view_scroll.py similarity index 90% rename from src/core/containers/image_view_scroll.py rename to src/core/containers/mirage/image_view_scroll.py index d5f6e41..3d26ef6 100644 --- a/src/core/containers/image_view_scroll.py +++ b/src/core/containers/mirage/image_view_scroll.py @@ -18,7 +18,7 @@ class ImageViewScroll(Gtk.ScrolledWindow): def __init__(self): super(ImageViewScroll, self).__init__() - self.fimages = settings.get_images_filter() + self.fimages = tuple(settings_manager.settings.filters.images) self.curent_dir = None self.size_request = None @@ -42,7 +42,7 @@ class ImageViewScroll(Gtk.ScrolledWindow): self.connect('scroll-event', self.on_scroll) def _subscribe_to_events(self): - event_system.subscribe("do_filter_open", self._do_filter_open) + event_system.subscribe("do-filter-open", self._do_filter_open) def _load_widgets(self): vadjustment = self.get_vadjustment() @@ -82,7 +82,7 @@ class ImageViewScroll(Gtk.ScrolledWindow): has_loaded_image = False if not os.path.isdir(path): - event_system.emit("handle_file_from_dnd", (path,)) + event_system.emit("handle-file-from-dnd", (path,)) path = os.path.dirname(path) has_loaded_image = True @@ -104,9 +104,9 @@ class ImageViewScroll(Gtk.ScrolledWindow): if not has_loaded_image: img = img_list[0] target = os.path.join(path, img) - event_system.emit("handle_file_from_dnd", target) + event_system.emit("handle-file-from-dnd", target) - event_system.emit("load_image_list", (path, img_list)) + event_system.emit("load-image-list", (path, img_list)) @daemon_threaded def _size_request_change(self, widget = None, rect = None): @@ -116,7 +116,7 @@ class ImageViewScroll(Gtk.ScrolledWindow): if self.size_request.width != rect.width or self.size_request.height != rect.height: self.size_request = rect - GLib.idle_add(event_system.emit, *("size_allocate",)) + GLib.idle_add(event_system.emit, *("size-allocate",)) def on_scroll(self, widget = None, event = None): accel_mask = Gtk.accelerator_get_default_mod_mask() @@ -141,8 +141,8 @@ class ImageViewScroll(Gtk.ScrolledWindow): adjustment.set_value(current_val - step_val) else: if direction > 0: - event_system.emit("zoom_out") + event_system.emit("zoom-out") else: - event_system.emit("zoom_in") + event_system.emit("zoom-in") return True # NOTE: Stop event propigation diff --git a/src/core/containers/left_box.py b/src/core/containers/mirage/left_box.py similarity index 87% rename from src/core/containers/left_box.py rename to src/core/containers/mirage/left_box.py index 58cccc8..a60d517 100644 --- a/src/core/containers/left_box.py +++ b/src/core/containers/mirage/left_box.py @@ -23,7 +23,7 @@ class LeftBox(Gtk.Box): def _setup_styling(self): self.set_orientation(Gtk.Orientation.VERTICAL) - self.set_size_request(settings.get_thumbnail_with() + 15, -1) + self.set_size_request(settings_manager.settings.config.thumbnail_with + 15, -1) self.set_vexpand(True) def _setup_signals(self): diff --git a/src/core/containers/right_box.py b/src/core/containers/mirage/right_box.py similarity index 78% rename from src/core/containers/right_box.py rename to src/core/containers/mirage/right_box.py index a4cbd38..0793373 100644 --- a/src/core/containers/right_box.py +++ b/src/core/containers/mirage/right_box.py @@ -8,9 +8,9 @@ from gi.repository import Gtk # Application imports from .image_view_scroll import ImageViewScroll -from ..widgets.button_controls import ButtonControls -from ..widgets.path_label import PathLabel -from ..widgets.ocr_window import OCRWindow +from core.widgets.button_controls import ButtonControls +from core.widgets.path_label import PathLabel +from core.widgets.ocr_window import OCRWindow @@ -35,7 +35,7 @@ class RightBox(Gtk.Box): ... def _subscribe_to_events(self): - event_system.subscribe("background_fill", self._toggle_background) + event_system.subscribe("background-fill", self._toggle_background) def _load_widgets(self): window = OCRWindow() diff --git a/src/core/containers/right_container.py b/src/core/containers/right_container.py new file mode 100644 index 0000000..dcd6503 --- /dev/null +++ b/src/core/containers/right_container.py @@ -0,0 +1,44 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from core.widgets.separator_widget import Separator + + + +class RightContainer(Gtk.Box): + def __init__(self): + super(RightContainer, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + + self.show() + + + def _setup_styling(self): + self.ctx = self.get_style_context() + self.ctx.add_class("right-container") + + self.set_orientation(Gtk.Orientation.HORIZONTAL) + self.set_vexpand(True) + + def _setup_signals(self): + self.connect("show", self._handle_show) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + widget_registery.expose_object("right-container", self) + + self.add( Separator("separator-right", 1) ) diff --git a/src/core/controller.py b/src/core/controller.py deleted file mode 100644 index 4b3d8ae..0000000 --- a/src/core/controller.py +++ /dev/null @@ -1,60 +0,0 @@ -# Python imports -import os - -# Lib imports -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository import GLib - -# Application imports -from .mixins.signals_mixins import SignalsMixins -from .controller_data import ControllerData -from .containers.base_container import BaseContainer - - - -class Controller(SignalsMixins, ControllerData): - def __init__(self, args, unknownargs): - self.setup_controller_data() - - self._setup_styling() - self._setup_signals() - self._subscribe_to_events() - - if args.no_plugins == "false": - self.plugins.launch_plugins() - - collection = unknownargs + [args.file] if args.file and os.path.isfile(args.file) else unknownargs - event_system.emit("do_filter_open", (collection,)) - - def _setup_styling(self): - ... - - def _setup_signals(self): - self.window.connect("focus-out-event", self.unset_keys_and_data) - self.window.connect("key-press-event", self.on_global_key_press_controller) - self.window.connect("key-release-event", self.on_global_key_release_controller) - - def _subscribe_to_events(self): - event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc) - event_system.subscribe("handle_dir_from_ipc", self.handle_dir_from_ipc) - event_system.subscribe("tggl_top_main_menubar", self._tggl_top_main_menubar) - - def _tggl_top_main_menubar(self): - logger.debug("_tggl_top_main_menubar > stub...") - - def setup_builder_and_container(self): - self.builder = Gtk.Builder() - # self.builder.add_from_file(settings.get_glade_file()) - self.builder.expose_object("main_window", self.window) - - settings.set_builder(self.builder) - self.base_container = BaseContainer() - - settings.register_signals_to_builder([self, self.base_container]) - - def get_base_container(self): - return self.base_container diff --git a/src/core/controllers/__init__.py b/src/core/controllers/__init__.py new file mode 100644 index 0000000..0f53a83 --- /dev/null +++ b/src/core/controllers/__init__.py @@ -0,0 +1,3 @@ +""" + Controllers Package +""" \ No newline at end of file diff --git a/src/core/controllers/base_controller.py b/src/core/controllers/base_controller.py new file mode 100644 index 0000000..28e985e --- /dev/null +++ b/src/core/controllers/base_controller.py @@ -0,0 +1,88 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from plugins import plugins_controller + +from libs.mixins.ipc_signals_mixin import IPCSignalsMixin +from libs.mixins.keyboard_signals_mixin import KeyboardSignalsMixin + +from ..containers.base_container import BaseContainer + +from .base_controller_mixin import BaseControllerMixin +from .bridge_controller import BridgeController + + + +class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerMixin): + """ docstring for BaseController. """ + + def __init__(self): + + self._setup_controller_data() + self.plugins_controller.manual_launch_plugins() + + self._load_plugins(is_pre = True) + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_controllers() + self._load_plugins(is_pre = False) + + self._load_files() + + logger.info(f"Made it past {self.__class__} loading...") + settings_manager.set_end_load_time() + settings_manager.log_load_time() + + + def _setup_controller_data(self): + self.window = settings_manager.get_main_window() + self.base_container = BaseContainer() + self.plugins_controller = plugins_controller + + settings_manager.register_signals_to_builder([self, self.base_container]) + + self._collect_files_dirs() + + def _setup_styling(self): + ... + + def _setup_signals(self): + self.window.connect("focus-out-event", self.unset_keys_and_data) + self.window.connect("key-press-event", self.on_global_key_press_controller) + self.window.connect("key-release-event", self.on_global_key_release_controller) + + def _subscribe_to_events(self): + event_system.subscribe("shutting-down", lambda: print("Shutting down...")) + event_system.subscribe("handle-file-from-ipc", self.handle_file_from_ipc) + event_system.subscribe("handle-files-from-ipc", self.handle_files_from_ipc) + event_system.subscribe("handle-dir-from-ipc", self.handle_dir_from_ipc) + event_system.subscribe("tggl-top-main-menubar", self._tggl_top_main_menubar) + + def _load_controllers(self): + BridgeController() + + def _load_plugins(self, is_pre: bool): + args, unknownargs = settings_manager.get_starting_args() + if args.no_plugins == "true": return + + if is_pre: + self.plugins_controller.pre_launch_plugins() + return + + if not is_pre: + self.plugins_controller.post_launch_plugins() + return + + def _load_files(self): + for file in settings_manager.get_starting_files(): + event_system.emit("post-file-to-ipc", file) + + def _tggl_top_main_menubar(self): + logger.debug("_tggl_top_main_menubar > stub...") + diff --git a/src/core/controller_data.py b/src/core/controllers/base_controller_mixin.py similarity index 55% rename from src/core/controller_data.py rename to src/core/controllers/base_controller_mixin.py index 910f627..6862b46 100644 --- a/src/core/controller_data.py +++ b/src/core/controllers/base_controller_mixin.py @@ -1,30 +1,37 @@ # Python imports import os import subprocess +from shutil import which # Lib imports # Application imports -from plugins.plugins_controller import PluginsController +class BaseControllerMixin: + ''' BaseControllerMixin contains most of the state of the app at ay given time. It also has some support methods. ''' -class ControllerData: - ''' ControllerData contains most of the state of the app at ay given time. It also has some support methods. ''' + def _collect_files_dirs(self): + args, \ + unknownargs = settings_manager.get_starting_args() + files = [] - def setup_controller_data(self) -> None: - self.window = settings.get_main_window() - self.builder = None - self.base_container = None - self.was_midified_key = False - self.ctrl_down = False - self.shift_down = False - self.alt_down = False + for arg in unknownargs + [args.new_tab,]: + if os.path.isfile(arg): + files.append(f"{arg}") - self.setup_builder_and_container() - self.plugins = PluginsController() + if os.path.isdir(arg): + message = f"DIR|{arg}" + ipc_server.send_ipc_message(message) + if not files: return + + settings_manager.set_is_starting_with_file(True) + settings_manager.set_starting_files(files) + + def get_base_container(self): + return self.base_container def clear_console(self) -> None: ''' Clears the terminal screen. ''' @@ -56,14 +63,24 @@ class ControllerData: for child in widget.get_children(): widget.remove(child) - def get_clipboard_data(self, encoding="utf-8") -> str: - proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE) + def get_clipboard_data(self, encoding = "utf-8") -> str: + if not which("xclip"): + logger.info('xclip not found...') + return + + command = ['xclip','-selection','clipboard'] + proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout = subprocess.PIPE) retcode = proc.wait() data = proc.stdout.read() return data.decode(encoding).strip() - def set_clipboard_data(self, data: type, encoding="utf-8") -> None: - proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE) + def set_clipboard_data(self, data: type, encoding = "utf-8") -> None: + if not which("xclip"): + logger.info('xclip not found...') + return + + command = ['xclip','-selection','clipboard'] + proc = subprocess.Popen(command, stdin = subprocess.PIPE) proc.stdin.write(data.encode(encoding)) proc.stdin.close() retcode = proc.wait() diff --git a/src/core/controllers/bridge_controller.py b/src/core/controllers/bridge_controller.py new file mode 100644 index 0000000..bc2c542 --- /dev/null +++ b/src/core/controllers/bridge_controller.py @@ -0,0 +1,41 @@ +# Python imports +import base64 + +# Lib imports + +# Application imports + + + +class BridgeController: + def __init__(self): + + self._setup_signals() + self._subscribe_to_events() + + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + event_system.subscribe("handle-bridge-event", self.handle_bridge_event) + + + def handle_bridge_event(self, event): + match event.topic: + case "save": + event_system.emit(f"handle-file-event-{event.originator}", (event,)) + case "close": + event_system.emit(f"handle-file-event-{event.originator}", (event,)) + case "load_buffer": + event_system.emit(f"handle-file-event-{event.originator}", (event,)) + case "load_file": + event_system.emit(f"handle-file-event-{event.originator}", (event,)) + case "alert": + content = base64.b64decode( event.content.encode() ).decode("utf-8") + logger.info(f"\nMessage Topic: {event.topic}\nMessage Content: {content}") + case "error": + content = base64.b64decode( event.content.encode() ).decode("utf-8") + logger.info(content) + case _: + ... \ No newline at end of file diff --git a/src/core/mixins/__init__.py b/src/core/mixins/__init__.py deleted file mode 100644 index 4589fc7..0000000 --- a/src/core/mixins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" - Generic Mixins Module -""" diff --git a/src/core/mixins/signals/__init__.py b/src/core/mixins/signals/__init__.py deleted file mode 100644 index 03c3ec2..0000000 --- a/src/core/mixins/signals/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" - Signals module -""" diff --git a/src/core/mixins/signals/ipc_signals_mixin.py b/src/core/mixins/signals/ipc_signals_mixin.py deleted file mode 100644 index c334230..0000000 --- a/src/core/mixins/signals/ipc_signals_mixin.py +++ /dev/null @@ -1,22 +0,0 @@ -# Python imports - -# Lib imports - -# Application imports - - - - -class IPCSignalsMixin: - """ IPCSignalsMixin handle messages from another starting solarfm process. """ - - def print_to_console(self, message=None): - print(message) - - def handle_file_from_ipc(self, path: str) -> None: - print(f"File From IPC: {path}") - event_system.emit("do_filter_open", ([path],)) - - def handle_dir_from_ipc(self, path: str) -> None: - print(f"Dir From IPC: {path}") - event_system.emit("do_filter_open", ([path],)) diff --git a/src/core/mixins/signals_mixins.py b/src/core/mixins/signals_mixins.py deleted file mode 100644 index 76515f6..0000000 --- a/src/core/mixins/signals_mixins.py +++ /dev/null @@ -1,13 +0,0 @@ -# Python imports - -# Lib imports -from .signals.ipc_signals_mixin import IPCSignalsMixin -from .signals.keyboard_signals_mixin import KeyboardSignalsMixin - -# Application imports - - - - -class SignalsMixins(KeyboardSignalsMixin, IPCSignalsMixin): - ... diff --git a/src/core/widgets/button_controls.py b/src/core/widgets/button_controls.py index 819cc15..b589c7a 100644 --- a/src/core/widgets/button_controls.py +++ b/src/core/widgets/button_controls.py @@ -30,7 +30,7 @@ class ButtonControls(Gtk.ButtonBox): ... def _load_widgets(self): - icons_path = settings.get_icons_path() + icons_path = settings_manager.path_manager.get_icons_path() center_widget = Gtk.ButtonBox() zoomout_button = Gtk.Button() lrotate_button = Gtk.Button() @@ -97,32 +97,32 @@ class ButtonControls(Gtk.ButtonBox): self.set_center_widget(center_widget) def _zoom_out(self, widget = None, eve = None): - event_system.emit("zoom_out") + event_system.emit("zoom-out") def _rotate_left(self, widget = None, eve = None): - event_system.emit("rotate_left") + event_system.emit("rotate-left") def _vertical_flip(self, widget = None, eve = None): - event_system.emit("vertical_flip") + event_system.emit("vertical-flip") def _scale_1_two_1(self, widget = None, eve = None): self._unset_class(self.fit_button) self._set_class(self.one2one_button) - event_system.emit("scale_1_two_1") + event_system.emit("scale-1-to-1") def _fit_to_container(self, widget = None, eve = None): self._unset_class(self.one2one_button) self._set_class(self.fit_button) - event_system.emit("fit_to_container") + event_system.emit("fit-to-container") def _horizontal_flip(self, widget = None, eve = None): - event_system.emit("horizontal_flip") + event_system.emit("horizontal-flip") def _rotate_right(self, widget = None, eve = None): - event_system.emit("rotate_right") + event_system.emit("rotate-right") def _zoom_in(self, widget = None, eve = None): - event_system.emit("zoom_in") + event_system.emit("zoom-in") def _set_class(self, target): ctx = target.get_style_context() @@ -133,4 +133,4 @@ class ButtonControls(Gtk.ButtonBox): ctx.remove_class("button-highlighted") def _show_ocr(self, widget): - event_system.emit("show_ocr") + event_system.emit("show-ocr") diff --git a/src/core/widgets/image.py b/src/core/widgets/image.py index d780012..2cb84be 100644 --- a/src/core/widgets/image.py +++ b/src/core/widgets/image.py @@ -18,8 +18,8 @@ class Image(Gtk.EventBox): def __init__(self, path: str): super(Image, self).__init__() - self._thumbnail_with = settings.get_thumbnail_with() - self._thumbnail_height = settings.get_thumbnail_height() + self._thumbnail_with = settings_manager.settings.config.thumbnail_with + self._thumbnail_height = settings_manager.settings.config.thumbnail_height self.is_loaded = False self.image = None self.path = path @@ -45,7 +45,7 @@ class Image(Gtk.EventBox): def set_image_to_view(self, widget = None, eve = None): if eve.button == 1: - event_system.emit("handle_file_from_dnd", (self.path, )) + event_system.emit("handle-file-from-dnd", (self.path, )) def load_pixbuf(self): self.set_from_pixbuf( self.get_pixbuf_data(self.path, \ @@ -56,7 +56,7 @@ class Image(Gtk.EventBox): def set_from_pixbuf(self, pixbuf): self.image.set_from_pixbuf(pixbuf) - def get_pixbuf_data(self, path, w = 126, h = 126): + def get_pixbuf_data(self, path: str, w: int = 126, h: int = 126): path = self.path if not path else path pixbuf = None diff --git a/src/core/widgets/image_list.py b/src/core/widgets/image_list.py index 9c16c6d..e4ce0f1 100644 --- a/src/core/widgets/image_list.py +++ b/src/core/widgets/image_list.py @@ -33,12 +33,12 @@ class ImageList(Gtk.Box): ... def _subscribe_to_events(self): - event_system.subscribe("load_image_list", self.load_image_list) + event_system.subscribe("load-image-list", self.load_image_list) def _load_widgets(self): ... - def _clear_children(self, widget: type) -> None: + def _clear_children(self, widget: Gtk.Object) -> None: ''' Clear children of a gtk widget. ''' for child in widget.get_children(): widget.remove(child) @@ -49,9 +49,10 @@ class ImageList(Gtk.Box): path = os.path.join(self.path, img) paths.append(path) + paths.sort() return paths - def load_image_list(self, path = None, img_list: [] = []): + def load_image_list(self, path: str, img_list: list = []): if not path or len(img_list) == 0: return @@ -64,10 +65,14 @@ class ImageList(Gtk.Box): for file in paths: self.add( Image(file) ) - event_system.emit("update_list_size_constraints", (len(paths),)) + event_system.emit("update-list-size-constraints", (len(paths),)) self.show_range() - def show_range(self, i = 0, j = settings.get_max_ring_thumbnail_list()): + def show_range( + self, + i: int = 0, + j: int = settings_manager.settings.config.max_ring_thumbnail_list + ): children = self.get_children() if len(children) <= j: j = len(children) - 1 @@ -78,7 +83,7 @@ class ImageList(Gtk.Box): i += 1 @daemon_threaded - def load_child_pixbuf_threaded(self, child): + def load_child_pixbuf_threaded(self, child: Gtk.Object): GLib.idle_add(child.load_pixbuf) GLib.idle_add(child.show) Gtk.main_iteration() diff --git a/src/core/widgets/image_view.py b/src/core/widgets/image_view.py index 5d17dfb..01b4553 100644 --- a/src/core/widgets/image_view.py +++ b/src/core/widgets/image_view.py @@ -47,18 +47,18 @@ class ImageView(ImageViewMixin, Gtk.Image): ... def _subscribe_to_events(self): - event_system.subscribe("size_allocate", self._size_allocate) - event_system.subscribe("handle_file_from_dnd", self._handle_file_from_dnd) + event_system.subscribe("size-allocate", self._size_allocate) + event_system.subscribe("handle-file-from-dnd", self._handle_file_from_dnd) - event_system.subscribe("get_active_image_path", self._get_active_image_path) - event_system.subscribe("zoom_out", self._zoom_out) - event_system.subscribe("rotate_left", self._rotate_left) - event_system.subscribe("vertical_flip", self._vertical_flip) - event_system.subscribe("scale_1_two_1", self._scale_1_two_1) - event_system.subscribe("fit_to_container", self._fit_to_container) - event_system.subscribe("horizontal_flip", self._horizontal_flip) - event_system.subscribe("rotate_right", self._rotate_right) - event_system.subscribe("zoom_in", self._zoom_in) + event_system.subscribe("get-active-image-path", self._get_active_image_path) + event_system.subscribe("zoom-out", self._zoom_out) + event_system.subscribe("rotate-left", self._rotate_left) + event_system.subscribe("vertical-flip", self._vertical_flip) + event_system.subscribe("scale-1-to-1", self._scale_1_two_1) + event_system.subscribe("fit-to-container", self._fit_to_container) + event_system.subscribe("horizontal-flip", self._horizontal_flip) + event_system.subscribe("rotate-right", self._rotate_right) + event_system.subscribe("zoom-in", self._zoom_in) def _load_widgets(self): ... @@ -91,15 +91,22 @@ class ImageView(ImageViewMixin, Gtk.Image): width = self.pixbuff.get_width() height = self.pixbuff.get_height() - size = sizeof_fmt( getsize(path) ) + size = self.sizeof_fmt( getsize(path) ) path = f"{path} | {width} x {height} | {size}" - event_system.emit("update_path_label", (path,)) + event_system.emit("update-path-label", (path,)) if self.fit_to_win: self._fit_to_container() else: self._scale_1_two_1() + def sizeof_fmt(self, num, suffix = "B"): + for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]: + if abs(num) < 1024.0: + return f"{num:3.1f} {unit}{suffix}" + num /= 1024.0 + return f"{num:.1f} Yi{suffix}" + def set_as_gif(self, path): image = None try: @@ -127,4 +134,4 @@ class ImageView(ImageViewMixin, Gtk.Image): w, h = im.size return GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB, - False, 8, w, h, w * 3) \ No newline at end of file + False, 8, w, h, w * 3) diff --git a/src/core/widgets/ocr_window.py b/src/core/widgets/ocr_window.py index 1d0c084..d5c04be 100644 --- a/src/core/widgets/ocr_window.py +++ b/src/core/widgets/ocr_window.py @@ -19,7 +19,7 @@ class OCRWindow(Gtk.Window): def __init__(self): super(OCRWindow, self).__init__() - self.tesseract_path = f"{settings.get_home_config_path()}/tesseract-ocr.AppImage" + self.tesseract_path = f"{settings_manager.path_manager.get_home_config_path()}/tesseract-ocr.AppImage" self.download_url = "https://github.com/AlexanderP/tesseract-appimage/releases/download/v5.3.3/tesseract-5.3.3-x86_64.AppImage" self._setup_styling() @@ -30,7 +30,7 @@ class OCRWindow(Gtk.Window): def _setup_styling(self): self.set_title(f"Tesseract OCR") - self.set_icon_from_file( settings.get_window_icon() ) + self.set_icon_from_file( settings_manager.path_manager.get_window_icon() ) self.set_gravity(5) # 5 = CENTER self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS @@ -42,7 +42,7 @@ class OCRWindow(Gtk.Window): self.connect("delete-event", self._tear_down) def _subscribe_to_events(self): - event_system.subscribe("show_ocr", self._show_ocr) + event_system.subscribe("show-ocr", self._show_ocr) def _load_widgets(self): scrolled_window = Gtk.ScrolledWindow() diff --git a/src/core/widgets/path_label.py b/src/core/widgets/path_label.py index 5b0c8fe..c674daf 100644 --- a/src/core/widgets/path_label.py +++ b/src/core/widgets/path_label.py @@ -33,12 +33,12 @@ class PathLabel(Gtk.Label): self.set_margin_bottom(10) def _subscribe_to_events(self): - event_system.subscribe("update_path_label", self.update_path_label) + event_system.subscribe("update-path-label", self.update_path_label) def _load_widgets(self): ... - def update_path_label(self, path = None): + def update_path_label(self, path: str): if not path: return self.set_label(path) diff --git a/src/core/widgets/separator_widget.py b/src/core/widgets/separator_widget.py new file mode 100644 index 0000000..533bdbe --- /dev/null +++ b/src/core/widgets/separator_widget.py @@ -0,0 +1,36 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports + + + +class Separator(Gtk.Separator): + def __init__(self, id: str = None, ORIENTATION: int = 0): + super(Separator, self).__init__() + + if id: + widget_registery.expose_object(id, self) + + self.ORIENTATION = ORIENTATION + self._setup_styling() + self._setup_signals() + self._load_widgets() + + self.show() + + + def _setup_styling(self): + # HORIZONTAL = 0, VERTICAL = 1 + self.set_orientation(self.ORIENTATION) + + + def _setup_signals(self): + ... + + def _load_widgets(self): + ... diff --git a/src/core/window.py b/src/core/window.py index 37b28d8..7466ccb 100644 --- a/src/core/window.py +++ b/src/core/window.py @@ -1,5 +1,4 @@ # Python imports -import time import signal # Lib imports @@ -11,93 +10,151 @@ from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib +try: + from gi.repository import GdkX11 +except ImportError: + logger.debug("Could not import X11 gir module...") + # Application imports -from core.controller import Controller +from libs.status_icon import StatusIcon +from core.controllers.base_controller import BaseController -class ControllerStartExceptiom(Exception): +class ControllerStartException(Exception): ... + class Window(Gtk.ApplicationWindow): """ docstring for Window. """ - def __init__(self, args, unknownargs): + def __init__(self): super(Window, self).__init__() + settings_manager.set_main_window(self) - self._controller = None + self._status_icon = None + self._controller = None - settings.set_main_window(self) - self._set_window_data() self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets(args, unknownargs) - self.show() - - # NOTE: Need to set size after show b/c get_allocation methods are initially incorrect if done beforehand... + self._set_window_data() self._set_size_constraints() + self.show() + def _setup_styling(self): self.set_title(f"{APP_NAME}") - self.set_icon_from_file( settings.get_window_icon() ) + self.set_icon_from_file( settings_manager.path_manager.get_window_icon() ) + self.set_decorated(True) + self.set_skip_pager_hint(False) + self.set_skip_taskbar_hint(False) self.set_gravity(5) # 5 = CENTER self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS - def _set_size_constraints(self): - self.set_default_size(settings.get_main_window_width(), - settings.get_main_window_height()) - self.set_size_request(settings.get_main_window_min_width(), - settings.get_main_window_min_height()) + ctx = self.get_style_context() + ctx.add_class("main-window") + ctx.add_class(f"mw_transparency_{settings_manager.settings.theming.transparency}") def _setup_signals(self): - self.connect("delete-event", self._tear_down) - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._tear_down) + self.connect("focus-in-event", self._on_focus_in_event) + self.connect("focus-out-event", self._on_focus_out_event) + self.connect("show", self._handle_show) + + self.connect("delete-event", self.stop) + GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.stop) def _subscribe_to_events(self): - event_system.subscribe("tear_down", self._tear_down) + event_system.subscribe("tear-down", self.stop) + event_system.subscribe("load-interactive-debug", self._load_interactive_debug) - def _load_widgets(self, args, unknownargs): - if settings.is_debug(): + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + + def _load_widgets(self): + widget_registery.expose_object("main-window", self) + + if settings_manager.is_debug(): self.set_interactive_debugging(True) - - self._controller = Controller(args, unknownargs) + self._controller = BaseController() + self._status_icon = StatusIcon() if not self._controller: - raise ControllerStartException("Controller exited and doesn't exist...") + raise ControllerStartException("BaseController exited and doesn't exist...") self.add( self._controller.get_base_container() ) + def _display_manager(self): + """ Try to detect which display manager we are running under... """ + + import os + if os.environ.get('WAYLAND_DISPLAY'): + return 'WAYLAND' + + return 'X11' + + def _set_size_constraints(self): + _window_x = settings_manager.settings.config.main_window_x + _window_y = settings_manager.settings.config.main_window_y + _min_width = settings_manager.settings.config.main_window_min_width + _min_height = settings_manager.settings.config.main_window_min_height + _width = settings_manager.settings.config.main_window_width + _height = settings_manager.settings.config.main_window_height + + self.move(_window_x, _window_y - 28) + self.set_size_request(_min_width, _min_height) + self.set_default_size(_width, _height) + def _set_window_data(self) -> None: screen = self.get_screen() visual = screen.get_rgba_visual() - if visual and screen.is_composited() and settings.make_transparent() == 0: + if visual and screen.is_composited() and settings_manager.settings.config.make_transparent == 0: self.set_visual(visual) self.set_app_paintable(True) - self.connect("draw", self._area_draw) + # self.connect("draw", self._area_draw) # bind css file cssProvider = Gtk.CssProvider() - cssProvider.load_from_path( settings.get_css_file() ) - screen = Gdk.Screen.get_default() styleContext = Gtk.StyleContext() + cssProvider.load_from_path( settings_manager.path_manager.get_css_file() ) styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) def _area_draw(self, widget: Gtk.ApplicationWindow, cr: cairo.Context) -> None: - cr.set_source_rgba( *settings.get_paint_bg_color() ) + cr.set_source_rgba( *settings_manager.get_paint_bg_color() ) cr.set_operator(cairo.OPERATOR_SOURCE) cr.paint() cr.set_operator(cairo.OPERATOR_OVER) - def _tear_down(self, widget=None, eve=None): - settings.clear_pid() - time.sleep(event_sleep_time) - Gtk.main_quit() + def _on_focus_in_event(self, widget, event): + event_system.emit("pause-dnd-signals") - def main(self): - Gtk.main() \ No newline at end of file + def _on_focus_out_event(self, widget, event): + event_system.emit("listen-dnd-signals") + + def _load_interactive_debug(self): + self.set_interactive_debugging(True) + + + def start(self): + Gtk.main() + + def stop(self, widget = None, eve = None): + event_system.emit("shutting-down") + + size = self.get_size() + pos = self.get_position() + + settings_manager.set_main_window_width(size.width) + settings_manager.set_main_window_height(size.height) + settings_manager.set_main_window_x(pos.root_x) + settings_manager.set_main_window_y(pos.root_y) + settings_manager.save_settings() + + settings_manager.clear_pid() + Gtk.main_quit() diff --git a/src/libs/__init__.py b/src/libs/__init__.py index a8e5edd..620f163 100644 --- a/src/libs/__init__.py +++ b/src/libs/__init__.py @@ -1,3 +1,3 @@ """ - Utils module -""" + Libs Package +""" \ No newline at end of file diff --git a/src/libs/command_system.py b/src/libs/command_system.py new file mode 100644 index 0000000..221c2b3 --- /dev/null +++ b/src/libs/command_system.py @@ -0,0 +1,50 @@ +# Python imports +import types + +# Lib imports + +# Application imports +from .event_factory import Event_Factory, Code_Event_Types + + + +class CommandSystem: + def __init__(self, commands: dict | types.ModuleType): + super(CommandSystem, self).__init__() + + self.commands: dict | types.ModuleType = commands + self.data: tuple = () + + + def set_data(self, *args, **kwargs): + self.data = (args, kwargs) + + def exec(self, command: str) -> any: + """ + The 'exec' method passes the default 'self.data' to commands where custom args are not needed. + Ex: The 'code' widget has many internally created commands that + only need 'source_view' and so 'set_data' is called to set that. + """ + if not hasattr(self.commands, command): return + method = getattr(self.commands, command) + + args, kwargs = self.data + return method.execute(*args, **kwargs) + + def exec_with_args(self, command: str, *args, **kwargs) -> any: + """ + The 'exec_with_args' method passes custom args with the understanding + that the recipient has proper method signature to accept it- whether + *args or **kwargs or something else entirely. + """ + if not hasattr(self.commands, command): return + + method = getattr(self.commands, command) + return method.execute(*args, **kwargs) + + def add_command(self, command_name: str, command: callable): + setattr(self.commands, command_name, command) + + def remove_command(self, command_name: str, command: callable): + if hasattr(self.commands, command_name): + delattr(self.commands, command_name) diff --git a/src/libs/controllers/__init__.py b/src/libs/controllers/__init__.py new file mode 100644 index 0000000..1dd2ffc --- /dev/null +++ b/src/libs/controllers/__init__.py @@ -0,0 +1,3 @@ +""" + Libs Controllers Package +""" \ No newline at end of file diff --git a/src/libs/controllers/controller_base.py b/src/libs/controllers/controller_base.py new file mode 100644 index 0000000..3997ba6 --- /dev/null +++ b/src/libs/controllers/controller_base.py @@ -0,0 +1,47 @@ +# Python imports + +# Lib imports + +# Application imports +from ..singleton import Singleton + +from ..dto.base_event import BaseEvent + +from .emit_dispatcher import EmitDispatcher +from .controller_message_bus import ControllerMessageBus + + + +class ControllerBaseException(Exception): + ... + + + +class ControllerBase(Singleton, EmitDispatcher): + def __init__(self): + super(ControllerBase, self).__init__() + + self.controller_message_bus: ControllerMessageBus = None + + + def _controller_message(self, event: BaseEvent): + raise ControllerBaseException("Controller Base '_controller_message' must be overridden...") + + def set_controller_message_bus(self, controller_message_bus: ControllerMessageBus): + self.controller_message_bus = controller_message_bus + + def message(self, event: BaseEvent): + return self.controller_message_bus.message(event) + + def message_to(self, name: str, event: BaseEvent): + return self.controller_message_bus.message_to(name, event) + + def message_to_selected(self, names: list[str], event: BaseEvent): + for name in names: + self.controller_message_bus.message_to_selected(name, event) + + def register_controller(self, name: str, controller): + self.controller_message_bus.register_controller(name, controller) + + def unregister_controller(self, name: str): + self.controller_message_bus.unregister_controller(name) diff --git a/src/libs/controllers/controller_manager.py b/src/libs/controllers/controller_manager.py new file mode 100644 index 0000000..aa5afb3 --- /dev/null +++ b/src/libs/controllers/controller_manager.py @@ -0,0 +1,74 @@ +# Python imports + +# Lib imports + +# Application imports +from ..singleton import Singleton +from ..event_factory import Code_Event_Types + +from .controller_base import ControllerBase +from .controller_message_bus import ControllerMessageBus + + + +class ControllerManagerException(Exception): + ... + + + +class ControllerManager(Singleton, dict): + """ + ControllerManager registers controllers by key/value pair. + It binds the message bus methods methods each controller has + due to extending ControllerBase. + """ + + def __init__(self): + super(ControllerManager, self).__init__() + + self.message_bus: ControllerMessageBus \ + = self._crete_controller_message_bus() + + + def _crete_controller_message_bus(self) -> ControllerMessageBus: + controller_message_bus = ControllerMessageBus() + controller_message_bus.message_to = self.message_to + controller_message_bus.message = self.message + controller_message_bus.register_controller = self.register_controller + controller_message_bus.unregister_controller = self.unregister_controller + + return controller_message_bus + + def register_controller(self, name: str, controller: ControllerBase): + if not name or controller == None: + raise ControllerManagerException("Must pass in a 'name' and 'controller'...") + + if name in self.keys(): + raise ControllerManagerException( + f"Can't bind controller to existing registered name of '{name}'..." + ) + + controller.set_controller_message_bus( self.message_bus ) + + self[name] = controller + + def unregister_controller(self, name: str): + if not name: + raise ControllerManagerException("Must pass in a 'name'...") + + if not name in self.keys(): + raise ControllerManagerException( + f"Can't find controller registered with name of '{name}'..." + ) + + self.pop(name, None) + + def get_controllers_key_list(self) -> list[str]: + return self.keys() + + def message_to(self, name: str, event: Code_Event_Types.CodeEvent): + self[name]._controller_message(event) + + def message(self, event: Code_Event_Types.CodeEvent): + for key in self.keys(): + self[key]._controller_message(event) diff --git a/src/libs/controllers/controller_message_bus.py b/src/libs/controllers/controller_message_bus.py new file mode 100644 index 0000000..29e82a8 --- /dev/null +++ b/src/libs/controllers/controller_message_bus.py @@ -0,0 +1,33 @@ +# Python imports + +# Lib imports + +# Application imports +from ..dto.base_event import BaseEvent + + + +class ControllerMessageBusException(Exception): + ... + + + +class ControllerMessageBus: + def __init__(self): + super(ControllerMessageBus, self).__init__() + + + def message(self, event: BaseEvent): + raise ControllerMessageBusException("Controller Message Bus 'message' must be overriden by Controller Manager...") + + def message_to(self, name: str, event: BaseEvent): + raise ControllerMessageBusException("Controller Message Bus 'message_to' must be overriden by Controller Manager...") + + def message_to_selected(self, name: list, event: BaseEvent): + raise ControllerMessageBusException("Controller Message Bus 'message_to_selected' must be overriden by Controller Manager...") + + def register_controller(self, name: str, controller): + raise ControllerMessageBusException("Controller Message Bus 'register_controller' must be overriden by Controller Manager...") + + def unregister_controller(self, name: str): + raise ControllerMessageBusException("Controller Message Bus 'unregister_controller' must be overriden by Controller Manager...") diff --git a/src/libs/controllers/emit_dispatcher.py b/src/libs/controllers/emit_dispatcher.py new file mode 100644 index 0000000..878ce67 --- /dev/null +++ b/src/libs/controllers/emit_dispatcher.py @@ -0,0 +1,29 @@ +# Python imports + +# Lib imports + +# Application imports +from ..dto.base_event import BaseEvent + + + +class EmitDispatcher: + """ + EmitDispatcher is used for allowing controllers to pass/hook in + their message system to children that need to signal events. + Note how we are not handling return info from the 'message' methods + whereas a controller would or could do so. + """ + + def __init__(self): + super(EmitDispatcher, self).__init__() + + + def emit(self, event: BaseEvent): + self.message(event) + + def emit_to(self, controller: str, event: BaseEvent): + self.message_to(controller, event) + + def emit_to_selected(self, names: list[str], event: BaseEvent): + self.message_to_selected(names, event) diff --git a/src/libs/db/__init__.py b/src/libs/db/__init__.py new file mode 100644 index 0000000..2b83284 --- /dev/null +++ b/src/libs/db/__init__.py @@ -0,0 +1,6 @@ +""" + Libs DB Package +""" + +from .models import User +from .db import DB \ No newline at end of file diff --git a/src/libs/db/db.py b/src/libs/db/db.py new file mode 100644 index 0000000..b2b22ce --- /dev/null +++ b/src/libs/db/db.py @@ -0,0 +1,42 @@ +# Python imports +from typing import Optional +from os import path + +# Lib imports +from sqlmodel import Session, create_engine + +# Application imports +from .models import SQLModel, User + + + +class DB: + def __init__(self): + super(DB, self).__init__() + + self.engine = None + + self.create_engine() + + + def create_engine(self): + db_path = f"sqlite:///{settings_manager.get_home_config_path()}/database.db" + self.engine = create_engine(db_path) + + SQLModel.metadata.create_all(self.engine) + + def _add_entry(self, entry): + with Session(self.engine) as session: + session.add(entry) + session.commit() + + + def add_user_entry(self, name = None, password = None, email = None): + if not name or not password or not email: return + + user = User() + user.name = name + user.password = password + user.email = email + + self._add_entry(user) diff --git a/src/libs/db/models.py b/src/libs/db/models.py new file mode 100644 index 0000000..4ce4007 --- /dev/null +++ b/src/libs/db/models.py @@ -0,0 +1,15 @@ +# Python imports +from typing import Optional + +# Lib imports +from sqlmodel import SQLModel, Field + +# Application imports + + + +class User(SQLModel, table = True): + id: Optional[int] = Field(default = None, primary_key = True) + name: str + password: str + email: Optional[str] = None diff --git a/src/libs/debugging.py b/src/libs/debugging.py index b84193a..5eaa286 100644 --- a/src/libs/debugging.py +++ b/src/libs/debugging.py @@ -18,7 +18,7 @@ def debug_signal_handler(signal, frame): rpdb2.start_embedded_debugger("foobar", True, True) rpdb2.setbreak(depth=1) return - except StandardError: + except Exception: ... try: @@ -26,7 +26,7 @@ def debug_signal_handler(signal, frame): logger.debug("\n\nStarting embedded rconsole debugger...\n\n") rconsole.spawn_server() return - except StandardError as ex: + except Exception as ex: ... try: @@ -34,7 +34,15 @@ def debug_signal_handler(signal, frame): logger.debug("\n\nStarting PuDB debugger...\n\n") set_trace(paused = True) return - except StandardError as ex: + except Exception as ex: + ... + + try: + import ipdb + logger.debug("\n\nStarting IPDB debugger...\n\n") + ipdb.set_trace() + return + except Exception as ex: ... try: @@ -42,11 +50,11 @@ def debug_signal_handler(signal, frame): logger.debug("\n\nStarting embedded PDB debugger...\n\n") pdb.Pdb(skip=['gi.*']).set_trace() return - except StandardError as ex: + except Exception as ex: ... try: import code code.interact() - except StandardError as ex: - logger.debug(f"{ex}, returning to normal program flow...") + except Exception as ex: + logger.debug(f"{ex}, returning to normal program flow...") \ No newline at end of file diff --git a/src/libs/dto/__init__.py b/src/libs/dto/__init__.py new file mode 100644 index 0000000..6e319fb --- /dev/null +++ b/src/libs/dto/__init__.py @@ -0,0 +1,5 @@ +""" + Libs DTO(s) Package +""" + +from .base_event import BaseEvent \ No newline at end of file diff --git a/src/libs/dto/base_event.py b/src/libs/dto/base_event.py new file mode 100644 index 0000000..a91c7db --- /dev/null +++ b/src/libs/dto/base_event.py @@ -0,0 +1,16 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + + +@dataclass(slots = True) +class BaseEvent: + topic: str = None + content: any = None + raw_content: any = None + success: callable = None + fail: callable = None diff --git a/src/libs/dto/plugins/__init__.py b/src/libs/dto/plugins/__init__.py new file mode 100644 index 0000000..0f976bd --- /dev/null +++ b/src/libs/dto/plugins/__init__.py @@ -0,0 +1,3 @@ +""" + Libs Plugin DTO(s) Package +""" diff --git a/src/libs/dto/plugins/manifest.py b/src/libs/dto/plugins/manifest.py new file mode 100644 index 0000000..4840aa8 --- /dev/null +++ b/src/libs/dto/plugins/manifest.py @@ -0,0 +1,30 @@ +# 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 = "" + description: str = "" + version: str = "0.0.1" + support: str = "support@mail.com" + credit: str = "" + copyright: str = "GPLv2" + pre_launch: bool = False + autoload: bool = True + 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/libs/dto/plugins/manifest_meta.py b/src/libs/dto/plugins/manifest_meta.py new file mode 100644 index 0000000..8e1056b --- /dev/null +++ b/src/libs/dto/plugins/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/libs/dto/plugins/requests.py b/src/libs/dto/plugins/requests.py new file mode 100644 index 0000000..010ff82 --- /dev/null +++ b/src/libs/dto/plugins/requests.py @@ -0,0 +1,11 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass +class Requests: + bind_keys: list = field(default_factory = lambda: []) diff --git a/src/libs/endpoint_registry.py b/src/libs/endpoint_registry.py deleted file mode 100644 index 86e4295..0000000 --- a/src/libs/endpoint_registry.py +++ /dev/null @@ -1,22 +0,0 @@ -# Python imports - -# Lib imports - -# Application imports -from .singleton import Singleton - - - -class EndpointRegistry(Singleton): - def __init__(self): - self._endpoints = {} - - def register(self, rule, **options): - def decorator(f): - self._endpoints[rule] = f - return f - - return decorator - - def get_endpoints(self): - return self._endpoints diff --git a/src/libs/event_factory.py b/src/libs/event_factory.py new file mode 100644 index 0000000..6b07b3b --- /dev/null +++ b/src/libs/event_factory.py @@ -0,0 +1,100 @@ +# Python imports +import inspect +from typing import Dict, Type +import re + +# Lib imports + +# Application imports +from .singleton import Singleton + +from .dto.base_event import BaseEvent + + + +class EventFactory(Singleton): + def __init__(self): + + self._event_classes: Dict[str, Type[BaseEvent]] = {} + + def register_event(self, event_type: str, event_class: Type[BaseEvent]): + self._event_classes[event_type] = event_class + + def register_events(self, events: dict): + i = 0 + for name, obj in events: + if not self._is_valid_event_class(obj): continue + + event_type = self._class_name_to_event_type(name) + + self._event_classes[event_type] = obj + App_Event_Types.add_event_class(name, obj) + i += 1 + + logger.debug(f"Registered {i} event types:") + + def unregister_events(self, events: dict): + i = 0 + for name, obj in events: + if not self._is_valid_event_class(obj): continue + + event_type = self._class_name_to_event_type(name) + + del self._event_classes[event_type] + App_Event_Types.remove_event_class(name) + i += 1 + + logger.debug(f"Unregistered {i} event types:") + + def create_event(self, event_type: str, **kwargs) -> BaseEvent: + if event_type not in self._event_classes: + raise ValueError(f"Unknown event type: {event_type}") + + event_class = self._event_classes[event_type] + event = event_class() + + for key, value in kwargs.items(): + if not hasattr(event, key): + raise ValueError(f"Event class {event_class.__name__} has no attribute '{key}'") + + setattr(event, key, value) + + return event + + + def _auto_register_events(self, events: dict): + self.register_events(events) + + def _is_valid_event_class(self, obj) -> bool: + return ( + inspect.isclass(obj) and + issubclass(obj, BaseEvent) and + obj != BaseEvent + ) + + def _class_name_to_event_type(self, class_name: str) -> str: + base_name = class_name[:-5] if class_name.endswith('Event') else class_name + return re.sub(r'(? bool: + return (inspect.isclass(obj) and issubclass(obj, BaseEvent) and obj != BaseEvent) + + def add_event_class(self, name: str, event_class: Type[BaseEvent]): + setattr(self, name, event_class) + + def remove_event_class(self, name: str): + delattr(self, name) + + + +App_Event_Types = EventNamespace() +Event_Factory = EventFactory() + diff --git a/src/libs/event_system.py b/src/libs/event_system.py index 9d876cf..531f843 100644 --- a/src/libs/event_system.py +++ b/src/libs/event_system.py @@ -13,18 +13,34 @@ class EventSystem(Singleton): def __init__(self): self.subscribers = defaultdict(list) + self._is_paused = False + + self._subscribe_to_events() - def subscribe(self, event_type, fn): + def _subscribe_to_events(self): + self.subscribe("pause_event_processing", self._pause_processing_events) + self.subscribe("resume_event_processing", self._resume_processing_events) + + def _pause_processing_events(self): + self._is_paused = True + + def _resume_processing_events(self): + self._is_paused = False + + def subscribe(self, event_type: str, fn: callable): self.subscribers[event_type].append(fn) - def unsubscribe(self, event_type, fn): + def unsubscribe(self, event_type: str, fn: callable): self.subscribers[event_type].remove(fn) - def unsubscribe_all(self, event_type): + def unsubscribe_all(self, event_type: str): self.subscribers.pop(event_type, None) - def emit(self, event_type, data = None): + def emit(self, event_type: str, data: tuple = ()): + if self._is_paused and event_type != "resume_event_processing": + return + if event_type in self.subscribers: for fn in self.subscribers[event_type]: if data: @@ -35,7 +51,10 @@ class EventSystem(Singleton): else: fn() - def emit_and_await(self, event_type, data = None): + def emit_and_await(self, event_type: str, data: tuple = ()): + if self._is_paused and event_type != "resume_event_processing": + return + """ NOTE: Should be used when signal has only one listener and vis-a-vis """ if event_type in self.subscribers: response = None diff --git a/src/libs/ipc_server.py b/src/libs/ipc_server.py index a4767bc..e9803ef 100644 --- a/src/libs/ipc_server.py +++ b/src/libs/ipc_server.py @@ -2,6 +2,7 @@ import os import threading import time +from contextlib import suppress from multiprocessing.connection import Client from multiprocessing.connection import Listener @@ -16,7 +17,7 @@ class IPCServer(Singleton): """ Create a listener so that other {APP_NAME} instances send requests back to existing instance. """ def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"): self.is_ipc_alive = False - self._ipc_port = 4848 + self._ipc_port = 0 # Use 0 to let Listener chose port self._ipc_address = ipc_address self._conn_type = conn_type self._ipc_authkey = b'' + bytes(f'{APP_NAME}-ipc', 'utf-8') @@ -35,13 +36,14 @@ class IPCServer(Singleton): self._subscribe_to_events() def _subscribe_to_events(self): - event_system.subscribe("post_file_to_ipc", self.send_ipc_message) + event_system.subscribe("post-file-to-ipc", self.send_ipc_message) def create_ipc_listener(self) -> None: if self._conn_type == "socket": - if os.path.exists(self._ipc_address) and settings.is_dirty_start(): - os.unlink(self._ipc_address) + if settings_manager.is_dirty_start(): + with suppress(FileNotFoundError, PermissionError): + os.unlink(self._ipc_address) listener = Listener(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey) elif "unsecured" not in self._conn_type: @@ -56,37 +58,55 @@ class IPCServer(Singleton): @daemon_threaded def _run_ipc_loop(self, listener) -> None: # NOTE: Not thread safe if using with Gtk. Need to import GLib and use idle_add - while True: + while self.is_ipc_alive: try: conn = listener.accept() start_time = time.perf_counter() self._handle_ipc_message(conn, start_time) + except EOFError as e: + logger.debug( repr(e) ) + except (OSError, ConnectionError, BrokenPipeError) as e: + logger.debug( f"IPC connection error: {e}" ) except Exception as e: - ... + logger.debug( f"Unexpected IPC error: {e}" ) + finally: + conn.close() listener.close() def _handle_ipc_message(self, conn, start_time) -> None: - while True: + while self.is_ipc_alive: msg = conn.recv() - if settings.is_debug(): - print(msg) + logger.debug(msg) if "FILE|" in msg: file = msg.split("FILE|")[1].strip() if file: - event_system.emit("handle_file_from_ipc", file) + event_system.emit("handle-file-from-ipc", file) + + conn.close() + break + + if "FILES|" in msg: + import json + data = msg.split("FILES|")[1].strip() + files = json.loads(data) + if files: + event_system.emit("handle-files-from-ipc", (files,)) + + conn.close() + break if "DIR|" in msg: file = msg.split("DIR|")[1].strip() if file: - event_system.emit("handle_dir_from_ipc", file) + event_system.emit("handle-dir-from-ipc", file) conn.close() break - if msg in ['close connection', 'close server']: + if msg in ['close connection', 'close server', 'Empty Data...']: conn.close() break @@ -109,9 +129,11 @@ class IPCServer(Singleton): conn.send(message) conn.close() except ConnectionRefusedError as e: - print("Connection refused...") + logger.error("Connection refused...") + except (OSError, ConnectionError, BrokenPipeError) as e: + logger.error( f"IPC connection error: {e}" ) except Exception as e: - print(repr(e)) + logger.error( f"Unexpected IPC error: {e}" ) def send_test_ipc_message(self, message: str = "Empty Data...") -> None: @@ -128,6 +150,9 @@ class IPCServer(Singleton): except ConnectionRefusedError as e: if self._conn_type == "socket": logger.error("IPC Socket no longer valid.... Removing.") - os.unlink(self._ipc_address) + with suppress(FileNotFoundError, PermissionError): + os.unlink(self._ipc_address) + except (OSError, ConnectionError, BrokenPipeError) as e: + logger.error( f"IPC connection error: {e}" ) except Exception as e: - logger.error( repr(e) ) + logger.error( f"Unexpected IPC error: {e}" ) \ No newline at end of file diff --git a/src/libs/keybindings.py b/src/libs/keybindings.py index 50e7b71..0437b72 100644 --- a/src/libs/keybindings.py +++ b/src/libs/keybindings.py @@ -42,6 +42,17 @@ class Keybindings(Singleton): self.keymap = Gdk.Keymap.get_default() self.configure({}) + def print_keys(self): + print(self.keys) + + def append_bindings(self, combos): + """ Accept new binding(s) and reload """ + for item in combos: + method, keys = item.split(":") + self.keys[method] = keys + + self.reload() + def configure(self, bindings): """ Accept new bindings and reconfigure with them """ self.keys = bindings diff --git a/src/libs/mixins/__init__.py b/src/libs/mixins/__init__.py new file mode 100644 index 0000000..c45077c --- /dev/null +++ b/src/libs/mixins/__init__.py @@ -0,0 +1,3 @@ +""" + Libs Mixins Package +""" \ No newline at end of file diff --git a/src/libs/mixins/dnd_mixin.py b/src/libs/mixins/dnd_mixin.py new file mode 100644 index 0000000..8b427f7 --- /dev/null +++ b/src/libs/mixins/dnd_mixin.py @@ -0,0 +1,70 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import Gio + +# Application imports + + + +class DnDMixin: + + def _setup_dnd(self): + flags = Gtk.DestDefaults.ALL + + PLAIN_TEXT_TARGET_TYPE = 70 + URI_TARGET_TYPE = 80 + + text_target = Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags(0), PLAIN_TEXT_TARGET_TYPE) + uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE) + + # targets = [ text_target, uri_target ] + targets = [ uri_target ] + + action = Gdk.DragAction.COPY + + # self.drag_dest_set_target_list(targets) + self.drag_dest_set(flags, targets, action) + + self._setup_dnd_signals() + + def _setup_dnd_signals(self): + # self.connect("drag-motion", self._on_drag_motion) + # self.connect('drag-drop', self._on_drag_set) + self.connect("drag-data-received", self._on_drag_data_received) + + def _on_drag_motion(self, widget, drag_context, x, y, time): + Gdk.drag_status(drag_context, drag_context.get_actions(), time) + + return False + + def _on_drag_set(self, widget, drag_context, data, info, time): + self.drag_get_data(drag_context, drag_context.list_targets()[-1], time) + + return False + + def _on_drag_data_received(self, widget, drag_context, x, y, data, info, time): + if info == 70: return + + if info == 80: + uris = data.get_uris() + files = [] + + if not uris: + uris = data.get_text().split("\n") + + for uri in uris: + gfile = None + try: + gfile = Gio.File.new_for_uri(uri) + except Exception as e: + gfile = Gio.File.new_for_path(uri) + + files.append(gfile) + + event_system.emit('set-pre-drop-dnd', (files,)) \ No newline at end of file diff --git a/src/libs/mixins/ipc_signals_mixin.py b/src/libs/mixins/ipc_signals_mixin.py new file mode 100644 index 0000000..3c3cfac --- /dev/null +++ b/src/libs/mixins/ipc_signals_mixin.py @@ -0,0 +1,37 @@ +# Python imports + +# Lib imports +import gi +from gi.repository import GLib + +# Application imports + + + + +class IPCSignalsMixin: + """ IPCSignalsMixin handle messages from another starting {APP_NAME} process. """ + + def print_to_console(self, message = None): + logger.debug(message) + + def handle_file_from_ipc(self, fpath: str) -> None: + logger.debug(f"File From IPC: {fpath}") + GLib.idle_add( + self.broadcast_message, "do-filter-open", ([fpath],) + ) + + def handle_files_from_ipc(self, uris: list) -> None: + logger.debug(f"Files From IPC: {uris}") + GLib.idle_add( + self.broadcast_message, "handle-files", (uris,) + ) + + def handle_dir_from_ipc(self, fpath: str) -> None: + logger.debug(f"Dir From IPC: {fpath}") + GLib.idle_add( + self.broadcast_message, "do-filter-open", ([fpath],) + ) + + def broadcast_message(self, message_type: str = "none", data: () = ()) -> None: + event_system.emit(message_type, data) \ No newline at end of file diff --git a/src/core/mixins/signals/keyboard_signals_mixin.py b/src/libs/mixins/keyboard_signals_mixin.py similarity index 58% rename from src/core/mixins/signals/keyboard_signals_mixin.py rename to src/libs/mixins/keyboard_signals_mixin.py index 1a99277..6f95023 100644 --- a/src/core/mixins/signals/keyboard_signals_mixin.py +++ b/src/libs/mixins/keyboard_signals_mixin.py @@ -19,14 +19,28 @@ valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]") class KeyboardSignalsMixin: """ KeyboardSignalsMixin keyboard hooks controller. """ + was_midified_key = False + ctrl_down = False + shift_down = False + alt_down = False + + # TODO: Need to set methods that use this to somehow check the keybindings state instead. - def unset_keys_and_data(self, widget=None, eve=None): + def unset_keys_and_data(self, widget = None, eve = None): self.ctrl_down = False self.shift_down = False self.alt_down = False + def unmap_special_keys(self, keyname): + if "control" in keyname: + self.ctrl_down = False + if "shift" in keyname: + self.shift_down = False + if "alt" in keyname: + self.alt_down = False + def on_global_key_press_controller(self, eve, user_data): - keyname = Gdk.keyval_name(user_data.keyval).lower() + keyname = Gdk.keyval_name(user_data.keyval).lower() modifiers = Gdk.ModifierType(user_data.get_state() & ~Gdk.ModifierType.LOCK_MASK) self.was_midified_key = True if modifiers != 0 else False @@ -46,15 +60,8 @@ class KeyboardSignalsMixin: if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]: should_return = self.was_midified_key and (self.ctrl_down or self.shift_down or self.alt_down) + self.unmap_special_keys(keyname) - if "control" in keyname: - self.ctrl_down = False - if "shift" in keyname: - self.shift_down = False - if "alt" in keyname: - self.alt_down = False - - # NOTE: In effect a filter after releasing a modifier and we have a modifier mapped if should_return: self.was_midified_key = False return @@ -65,30 +72,31 @@ class KeyboardSignalsMixin: logger.debug(f"on_global_key_release_controller > mapping > {mapping}") if mapping: - # See if in controller scope - try: - getattr(self, mapping)() - return True - except Exception: - # Must be plugins scope, event call, OR we forgot to add method to controller scope - if "||" in mapping: - sender, eve_type = mapping.split("||") - else: - sender = "" - eve_type = mapping - - self.handle_key_event_system(sender, eve_type) + self.handle_mapped_key_event(mapping) else: - logger.debug(f"on_global_key_release_controller > key > {keyname}") + self.handle_as_key_event_scope(keyname) - if self.ctrl_down: - if not keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]: - self.handle_key_event_system(None, mapping) - else: - ... + def handle_mapped_key_event(self, mapping): + try: + self.handle_as_controller_scope(mapping) + except Exception: + self.handle_as_plugin_scope(mapping) + + def handle_as_controller_scope(self, mapping): + getattr(self, mapping)() + + def handle_as_plugin_scope(self, mapping): + if "||" in mapping: + sender, eve_type = mapping.split("||") + else: + sender = "" + eve_type = mapping + + self.handle_key_event_system(sender, eve_type) + + def handle_as_key_event_scope(self, keyname): + if self.ctrl_down and not keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]: + self.handle_key_event_system(None, keyname) def handle_key_event_system(self, sender, eve_type): - event_system.emit(eve_type) - - def keyboard_close_tab(self): - ... + event_system.emit(eve_type) \ No newline at end of file diff --git a/src/libs/mixins/observable_mixin.py b/src/libs/mixins/observable_mixin.py new file mode 100644 index 0000000..64207e5 --- /dev/null +++ b/src/libs/mixins/observable_mixin.py @@ -0,0 +1,26 @@ +# Python imports + +# Lib imports + +# Application imports +from ..dto.observable_event import ObservableEvent + + + +class ObservableMixin: + observers = [] + + def add_observer(self, observer: any): + if not hasattr(observer, 'notification') or not callable(getattr(observer, 'notification')): + raise ValueError(f"Observer '{observer}' must implement a `notification` method.") + + self.observers.append(observer) + + def remove_observer(self, observer: any): + if not observer in self.observers: return + + self.observers.remove(observer) + + def notify_observers(self, event: ObservableEvent): + for observer in self.observers: + observer.notification(event) \ No newline at end of file diff --git a/src/libs/settings/__init__.py b/src/libs/settings/__init__.py index e07c5a0..168d6ee 100644 --- a/src/libs/settings/__init__.py +++ b/src/libs/settings/__init__.py @@ -1,4 +1,4 @@ """ - Settings module + Libs Settings Package """ -from .settings import Settings +from .manager import SettingsManager \ No newline at end of file diff --git a/src/libs/settings/manager.py b/src/libs/settings/manager.py new file mode 100644 index 0000000..8538526 --- /dev/null +++ b/src/libs/settings/manager.py @@ -0,0 +1,125 @@ +# Python imports +import inspect +import time + +# Lib imports + +# Application imports +from ..singleton import Singleton +from .start_check_mixin import StartCheckMixin + +from .path_manager import PathManager +from .options.settings import Settings + + + +class SettingsManager(StartCheckMixin, Singleton): + def __init__(self): + self.path_manager: PathManager = PathManager() + self.settings: Settings = None + + self._main_window = None + self._builder = None + + self._trace_debug: bool = False + self._debug: bool = False + self._dirty_start: bool = False + self._passed_in_file: bool = False + self._starting_files: list = [] + + self.PAINT_BG_COLOR: tuple = (0, 0, 0, 0.0) + + self.load_keybindings() + self.load_context_menu_data() + + + def get_monitor_data(self) -> list: + screen = self._main_window.get_screen() + monitors = [] + for m in range(screen.get_n_monitors()): + monitors.append(screen.get_monitor_geometry(m)) + print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y)) + + return monitors + + def get_main_window(self) -> any: return self._main_window + def get_builder(self) -> any: return self._builder + def get_paint_bg_color(self) -> any: return self.PAINT_BG_COLOR + def get_context_menu_data(self) -> str: return self._context_menu_data + + def get_icon_theme(self) -> str: return self._ICON_THEME + def get_starting_files(self) -> list: return self._starting_files + def get_guake_key(self) -> tuple: return self._guake_key + + def get_starting_args(self): + return self.args, self.unknownargs + + def set_main_window(self, window): self._main_window = window + def set_builder(self, builder) -> any: self._builder = builder + + def set_main_window_x(self, x: int = 0): self.settings.config.main_window_x = x + def set_main_window_y(self, y: int = 0): self.settings.config.main_window_y = y + def set_main_window_width(self, width: int = 800): self.settings.config.main_window_width = width + def set_main_window_height(self, height: int = 600): self.settings.config.main_window_height = height + def set_main_window_min_width(self, width: int = 720): self.settings.config.main_window_min_width = width + def set_main_window_min_height(self, height: int = 480): self.settings.config.main_window_min_height = height + def set_starting_files(self, files: list): self._starting_files = files + def set_start_load_time(self): self._start_load_time = time.perf_counter() + def set_end_load_time(self): self._end_load_time = time.perf_counter() + + def set_starting_args(self, args, unknownargs): + self.args = args + self.unknownargs = unknownargs + + def set_trace_debug(self, trace_debug: bool): + self._trace_debug = trace_debug + + def set_debug(self, debug: bool): + self._debug = debug + + def set_is_starting_with_file(self, is_passed_in_file: bool = False): + self._passed_in_file = is_passed_in_file + + def is_trace_debug(self) -> str: return self._trace_debug + def is_debug(self) -> str: return self._debug + def is_starting_with_file(self) -> bool: return self._passed_in_file + + def log_load_time(self): logger.info( f"Load Time: {self._end_load_time - self._start_load_time}" ) + + + def register_signals_to_builder(self, classes = None): + handlers = {} + + for c in classes: + methods = None + try: + methods = inspect.getmembers(c, predicate = inspect.ismethod) + handlers.update(methods) + except Exception as e: + ... + + self._builder.connect_signals(handlers) + + def call_method(self, target_class: any = None, _method_name: str = "", data: any = None): + method_name = str(_method_name) + method = getattr(target_class, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}") + return method(data) if data else method() + + def load_keybindings(self): + bindings = self.path_manager.load_keybindings() + + keybindings.configure(bindings) + + def load_context_menu_data(self): + self._context_menu_data = self.path_manager.load_context_menu_data() + + def load_settings(self): + data = self.path_manager.load_settings() + if not data: + self.settings = Settings() + return + + self.settings = Settings(**data) + + def save_settings(self): + self.path_manager.save_settings(self.settings) diff --git a/src/libs/settings/options/__init__.py b/src/libs/settings/options/__init__.py new file mode 100644 index 0000000..e06487a --- /dev/null +++ b/src/libs/settings/options/__init__.py @@ -0,0 +1,8 @@ +""" + Settings.Options Package +""" +from .settings import Settings +from .config import Config +from .filters import Filters +from .theming import Theming +from .debugging import Debugging \ No newline at end of file diff --git a/src/libs/settings/options/config.py b/src/libs/settings/options/config.py new file mode 100644 index 0000000..932b052 --- /dev/null +++ b/src/libs/settings/options/config.py @@ -0,0 +1,42 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass(slots = True) +class Config: + base_of_home: str = "" + hide_hidden_files: str = "true" + thumbnailer_path: str = "ffmpegthumbnailer" + blender_thumbnailer_path: str = "" + go_past_home: str = "true" + lock_folder: str = "false" + locked_folders: list = field(default_factory=lambda: [ "venv", "flasks" ]) + mplayer_options: str = "-quiet -really-quiet -xy 1600 -geometry 50%:50%" + music_app: str = "/opt/deadbeef/bin/deadbeef" + media_app: str = "mpv" + image_app: str = "mirage" + office_app: str = "libreoffice" + pdf_app: str = "evince" + code_app: str = "atom" + text_app: str = "leafpad" + file_manager_app: str = "solarfm" + terminal_app: str = "terminator" + remux_folder_max_disk_usage: str = "8589934592" + max_ring_thumbnail_list: int = 10 + thumbnail_with: int = 256 + thumbnail_height: int = 256 + make_transparent: int = 0 + main_window_x: int = 721 + main_window_y: int = 465 + main_window_min_width: int = 720 + main_window_min_height: int = 480 + main_window_width: int = 800 + main_window_height: int = 600 + application_dirs: list = field(default_factory=lambda: [ + "/usr/share/applications", + f"{settings_manager.path_manager.get_home_path()}/.local/share/applications" + ]) diff --git a/src/libs/settings/options/debugging.py b/src/libs/settings/options/debugging.py new file mode 100644 index 0000000..3fc605d --- /dev/null +++ b/src/libs/settings/options/debugging.py @@ -0,0 +1,12 @@ +# Python imports +from dataclasses import dataclass + +# Lib imports + +# Application imports + + +@dataclass +class Debugging: + ch_log_lvl: int = 10 + fh_log_lvl: int = 20 diff --git a/src/libs/settings/options/filters.py b/src/libs/settings/options/filters.py new file mode 100644 index 0000000..e5293bf --- /dev/null +++ b/src/libs/settings/options/filters.py @@ -0,0 +1,90 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports + + +@dataclass +class Filters: + meshs: list = field(default_factory=lambda: [ + ".blend", + ".dae", + ".fbx", + ".gltf", + ".obj", + ".stl" + ]) + code: list = field(default_factory=lambda: [ + ".cpp", + ".css", + ".c", + ".go", + ".html", + ".htm", + ".java", + ".js", + ".json", + ".lua", + ".md", + ".py", + ".rs", + ".toml", + ".xml", + ".pom" + ]) + videos: list = field(default_factory=lambda:[ + ".mkv", + ".mp4", + ".webm", + ".avi", + ".mov", + ".m4v", + ".mpg", + ".mpeg", + ".wmv", + ".flv" + ]) + office: list = field(default_factory=lambda: [ + ".doc", + ".docx", + ".xls", + ".xlsx", + ".xlt", + ".xltx", + ".xlm", + ".ppt", + ".pptx", + ".pps", + ".ppsx", + ".odt", + ".rtf" + ]) + images: list = field(default_factory=lambda: [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".ico", + ".tga", + ".webp" + ]) + text: list = field(default_factory=lambda: [ + ".txt", + ".text", + ".sh", + ".cfg", + ".conf", + ".log" + ]) + music: list = field(default_factory=lambda: [ + ".psf", + ".mp3", + ".ogg", + ".flac", + ".m4a" + ]) + pdf: list = field(default_factory=lambda: [ + ".pdf" + ]) diff --git a/src/libs/settings/options/settings.py b/src/libs/settings/options/settings.py new file mode 100644 index 0000000..d107a42 --- /dev/null +++ b/src/libs/settings/options/settings.py @@ -0,0 +1,31 @@ +# Python imports +from dataclasses import dataclass, field +from dataclasses import asdict + +# Lib imports + +# Application imports +from .config import Config +from .filters import Filters +from .theming import Theming +from .debugging import Debugging + + +@dataclass +class Settings: + load_defaults: bool = True + config: Config = field(default_factory=lambda: Config()) + filters: Filters = field(default_factory=lambda: Filters()) + theming: Theming = field(default_factory=lambda: Theming()) + debugging: Debugging = field(default_factory=lambda: Debugging()) + + def __post_init__(self): + if not self.load_defaults: + self.load_defaults = False + self.config = Config(**self.config) + self.filters = Filters(**self.filters) + self.theming = Theming(**self.theming) + self.debugging = Debugging(**self.debugging) + + def as_dict(self): + return asdict(self) diff --git a/src/libs/settings/options/theming.py b/src/libs/settings/options/theming.py new file mode 100644 index 0000000..c52f50e --- /dev/null +++ b/src/libs/settings/options/theming.py @@ -0,0 +1,16 @@ +# Python imports +from dataclasses import dataclass + +# Lib imports + +# Application imports + + +@dataclass +class Theming: + transparency: int = 64 + default_zoom: int = 12 + syntax_theme: str = "penguins-in-space" + success_color: str = "#88cc27" + warning_color: str = "#ffa800" + error_color: str = "#ff0000" diff --git a/src/libs/settings/path_manager.py b/src/libs/settings/path_manager.py new file mode 100644 index 0000000..8f80c6e --- /dev/null +++ b/src/libs/settings/path_manager.py @@ -0,0 +1,124 @@ +# Python imports +import json +import zipfile + +from os import path +from os import mkdir + +# Lib imports + +# Application imports + + + +class MissingConfigError(Exception): + pass + + + +class PathManager: + def __init__(self): + self._SCRIPT_PTH: str = path.dirname(path.realpath(__file__)) + self._USER_HOME: str = path.expanduser('~') + self._HOME_CONFIG_PATH: str = f"{self._USER_HOME}/.config/{APP_NAME.lower()}" + self._USR_PATH: str = f"/usr/share/{APP_NAME.lower()}" + self._USR_CONFIG_FILE: str = f"{self._USR_PATH}/settings.json" + + self._CONTEXT_PATH: str = f"{self._HOME_CONFIG_PATH}/context_path" + self._PLUGINS_PATH: str = f"{self._HOME_CONFIG_PATH}/plugins" + self._DEFAULT_ICONS: str = f"{self._HOME_CONFIG_PATH}/icons" + self._CONFIG_FILE: str = f"{self._HOME_CONFIG_PATH}/settings.json" + self._GLADE_FILE: str = f"{self._HOME_CONFIG_PATH}/Main_Window.glade" + self._CSS_FILE: str = f"{self._HOME_CONFIG_PATH}/stylesheet.css" + self._KEY_BINDINGS_FILE: str = f"{self._HOME_CONFIG_PATH}/key-bindings.json" + self._PID_FILE: str = f"{self._HOME_CONFIG_PATH}/{APP_NAME.lower()}.pid" + self._UI_WIDGETS_PATH: str = f"{self._HOME_CONFIG_PATH}/ui_widgets" + self._CONTEXT_MENU: str = f"{self._HOME_CONFIG_PATH}/context_menu.json" + self._WINDOW_ICON: str = f"{self._DEFAULT_ICONS}/{APP_NAME.lower()}.png" + + # self._USR_CONFIG_FILE: str = f"{self._USR_PATH}/settings.json" + # self._PLUGINS_PATH: str = f"plugins" + # self._CONFIG_FILE: str = f"settings.json" + # self._GLADE_FILE: str = f"Main_Window.glade" + # self._CSS_FILE: str = f"stylesheet.css" + # self._KEY_BINDINGS_FILE: str = f"key-bindings.json" + # self._PID_FILE: str = f"{APP_NAME.lower()}.pid" + # self._WINDOW_ICON: str = f"{APP_NAME.lower()}.png" + # self._UI_WIDGETS_PATH: str = f"ui_widgets" + # self._CONTEXT_MENU: str = f"context_menu.json" + # self._DEFAULT_ICONS: str = f"icons" + + + # with zipfile.ZipFile("files.zip", mode="r", allowZip64=True) as zf: + # with io.TextIOWrapper(zf.open("text1.txt"), encoding="utf-8") as f: + + + if not path.exists(self._HOME_CONFIG_PATH): + mkdir(self._HOME_CONFIG_PATH) + if not path.exists(self._PLUGINS_PATH): + mkdir(self._PLUGINS_PATH) + + if not path.exists(self._DEFAULT_ICONS): + self._DEFAULT_ICONS = f"{self._USR_PATH}/icons" + if not path.exists(self._DEFAULT_ICONS): + raise MissingConfigError("Unable to find the application icons directory.") + if not path.exists(self._GLADE_FILE): + self._GLADE_FILE = f"{self._USR_PATH}/Main_Window.glade" + if not path.exists(self._GLADE_FILE): + raise MissingConfigError("Unable to find the application Glade file.") + if not path.exists(self._KEY_BINDINGS_FILE): + self._KEY_BINDINGS_FILE = f"{self._USR_PATH}/key-bindings.json" + if not path.exists(self._KEY_BINDINGS_FILE): + raise MissingConfigError("Unable to find the application Keybindings file.") + if not path.exists(self._CSS_FILE): + self._CSS_FILE = f"{self._USR_PATH}/stylesheet.css" + if not path.exists(self._CSS_FILE): + raise MissingConfigError("Unable to find the application Stylesheet file.") + if not path.exists(self._WINDOW_ICON): + self._WINDOW_ICON = f"{self._USR_PATH}/icons/{APP_NAME.lower()}.png" + if not path.exists(self._WINDOW_ICON): + raise MissingConfigError("Unable to find the application icon.") + if not path.exists(self._UI_WIDGETS_PATH): + self._UI_WIDGETS_PATH = f"{self._USR_PATH}/ui_widgets" + if not path.exists(self._CONTEXT_MENU): + self._CONTEXT_MENU = f"{self._USR_PATH}/context_menu.json" + + + def get_glade_file(self) -> str: return self._GLADE_FILE + def get_ui_widgets_path(self) -> str: return self._UI_WIDGETS_PATH + def get_context_path(self) -> str: return self._CONTEXT_PATH + def get_plugins_path(self) -> str: return self._PLUGINS_PATH + def get_icons_path(self) -> str: return self._DEFAULT_ICONS + def get_css_file(self) -> str: return self._CSS_FILE + def get_home_config_path(self) -> str: return self._HOME_CONFIG_PATH + def get_window_icon(self) -> str: return self._WINDOW_ICON + def get_home_path(self) -> str: return self._USER_HOME + + def load_keybindings(self): + try: + with open(self._KEY_BINDINGS_FILE) as file: + return json.load(file)["keybindings"] + except Exception as e: + print( f"Settings Path Manager: {self._KEY_BINDINGS_FILE}\n\t\t{repr(e)}" ) + return {} + + def load_context_menu_data(self): + try: + with open(self._CONTEXT_MENU) as file: + return json.load(file) + except Exception as e: + print( f"Settings Path Manager: {self._CONTEXT_MENU}\n\t\t{repr(e)}" ) + return {} + + def load_settings(self): + if not path.exists(self._CONFIG_FILE): + return None + + with open(self._CONFIG_FILE) as file: + data = json.load(file) + data["load_defaults"] = False + return data + + def save_settings(self, settings: any): + with open(self._CONFIG_FILE, 'w') as outfile: + json.dump(settings.as_dict(), outfile, separators=(',', ':'), indent=4) \ No newline at end of file diff --git a/src/libs/settings/settings.py b/src/libs/settings/settings.py deleted file mode 100644 index 997658b..0000000 --- a/src/libs/settings/settings.py +++ /dev/null @@ -1,189 +0,0 @@ -# Python imports -import os -import io -import json -import inspect -import zipfile - -# Lib imports - -# Application imports -from ..singleton import Singleton -from .start_check_mixin import StartCheckMixin - - -class MissingConfigError(Exception): - pass - - - -class Settings(StartCheckMixin, Singleton): - def __init__(self): - self._SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) - self._USER_HOME = os.path.expanduser('~') - self._HOME_CONFIG_PATH = f"{self._USER_HOME}/.config/{APP_NAME.lower()}" - self._USR_PATH = f"/usr/share/{APP_NAME.lower()}" - self._USR_CONFIG_FILE = f"{self._USR_PATH}/settings.json" - - self._PLUGINS_PATH = f"{self._HOME_CONFIG_PATH}/plugins" - self._DEFAULT_ICONS = f"{self._HOME_CONFIG_PATH}/icons" - self._CONFIG_FILE = f"{self._HOME_CONFIG_PATH}/settings.json" - self._GLADE_FILE = f"{self._HOME_CONFIG_PATH}/Main_Window.glade" - self._CSS_FILE = f"{self._HOME_CONFIG_PATH}/stylesheet.css" - self._KEY_BINDINGS_FILE = f"{self._HOME_CONFIG_PATH}/key-bindings.json" - self._PID_FILE = f"{self._HOME_CONFIG_PATH}/{APP_NAME.lower()}.pid" - self._UI_WIDEGTS_PATH = f"{self._HOME_CONFIG_PATH}/ui_widgets" - self._CONTEXT_MENU = f"{self._HOME_CONFIG_PATH}/contexct_menu.json" - self._WINDOW_ICON = f"{self._DEFAULT_ICONS}/{APP_NAME.lower()}.png" - - if not os.path.exists(self._HOME_CONFIG_PATH): - os.mkdir(self._HOME_CONFIG_PATH) - if not os.path.exists(self._PLUGINS_PATH): - os.mkdir(self._PLUGINS_PATH) - - if not os.path.exists(self._CONFIG_FILE): - import shutil - try: - shutil.copyfile(self._USR_CONFIG_FILE, self._CONFIG_FILE) - except Exception as e: - raise - - if not os.path.exists(self._DEFAULT_ICONS): - self._DEFAULT_ICONS = f"{self._USR_PATH}/icons" - if not os.path.exists(self._DEFAULT_ICONS): - raise MissingConfigError("Unable to find the application icons directory.") - # if not os.path.exists(self._GLADE_FILE): - # self._GLADE_FILE = f"{self._USR_PATH}/Main_Window.glade" - # if not os.path.exists(self._GLADE_FILE): - raise MissingConfigError("Unable to find the application Glade file.") - if not os.path.exists(self._KEY_BINDINGS_FILE): - self._KEY_BINDINGS_FILE = f"{self._USR_PATH}/key-bindings.json" - if not os.path.exists(self._KEY_BINDINGS_FILE): - raise MissingConfigError("Unable to find the application Keybindings file.") - if not os.path.exists(self._CSS_FILE): - self._CSS_FILE = f"{self._USR_PATH}/stylesheet.css" - if not os.path.exists(self._CSS_FILE): - raise MissingConfigError("Unable to find the application Stylesheet file.") - if not os.path.exists(self._WINDOW_ICON): - self._WINDOW_ICON = f"{self._USR_PATH}/icons/{APP_NAME.lower()}.png" - if not os.path.exists(self._WINDOW_ICON): - raise MissingConfigError("Unable to find the application icon.") - if not os.path.exists(self._UI_WIDEGTS_PATH): - self._UI_WIDEGTS_PATH = f"{self._USR_PATH}/ui_widgets" - if not os.path.exists(self._CONTEXT_MENU): - self._CONTEXT_MENU = f"{self._USR_PATH}/contexct_menu.json" - - - try: - with open(self._KEY_BINDINGS_FILE) as file: - bindings = json.load(file)["keybindings"] - keybindings.configure(bindings) - except Exception as e: - print( f"Settings: {self._KEY_BINDINGS_FILE}\n\t\t{repr(e)}" ) - - try: - with open(self._CONTEXT_MENU) as file: - self._context_menu_data = json.load(file) - except Exception as e: - print( f"Settings: {self._CONTEXT_MENU}\n\t\t{repr(e)}" ) - - - self._main_window = None - self._main_window_w = 800 - self._main_window_h = 600 - self._main_window_mw = 720 - self._main_window_mh = 480 - self._builder = None - self.PAINT_BG_COLOR = (0, 0, 0, 0.54) - - self._trace_debug = False - self._debug = False - self._dirty_start = False - - self.load_settings() - - - def register_signals_to_builder(self, classes=None): - handlers = {} - - for c in classes: - methods = None - try: - methods = inspect.getmembers(c, predicate=inspect.ismethod) - handlers.update(methods) - except Exception as e: - ... - - self._builder.connect_signals(handlers) - - def set_main_window(self, window): self._main_window = window - def set_builder(self, builder) -> any: self._builder = builder - - - def get_monitor_data(self) -> list: - screen = self._main_window.get_screen() - monitors = [] - for m in range(screen.get_n_monitors()): - monitors.append(screen.get_monitor_geometry(m)) - print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y)) - - return monitors - - def get_main_window(self) -> any: return self._main_window - def get_main_window_width(self) -> any: return self._main_window_w - def get_main_window_height(self) -> any: return self._main_window_h - def get_main_window_min_width(self) -> any: return self._main_window_mw - def get_main_window_min_height(self) -> any: return self._main_window_mh - def get_builder(self) -> any: return self._builder - def get_paint_bg_color(self) -> any: return self.PAINT_BG_COLOR - def get_glade_file(self) -> str: return self._GLADE_FILE - def get_ui_widgets_path(self) -> str: return self._UI_WIDEGTS_PATH - def get_context_menu_data(self) -> str: return self._context_menu_data - - def get_plugins_path(self) -> str: return self._PLUGINS_PATH - def get_icon_theme(self) -> str: return self._ICON_THEME - def get_css_file(self) -> str: return self._CSS_FILE - def get_home_config_path(self) -> str: return self._HOME_CONFIG_PATH - def get_window_icon(self) -> str: return self._WINDOW_ICON - def get_home_path(self) -> str: return self._USER_HOME - - def get_icons_path(self) -> str: return self._DEFAULT_ICONS - def make_transparent(self) -> int: return self._config["make_transparent"] - def get_thumbnail_with(self) -> int: return self._config["thumbnail_with"] - def get_thumbnail_height(self) -> int: return self._config["thumbnail_height"] - def get_max_ring_thumbnail_list(self) -> int: return self._config["max_ring_thumbnail_list"] - - # Filter returns - def get_office_filter(self) -> tuple: return tuple(self._settings["filters"]["office"]) - def get_vids_filter(self) -> tuple: return tuple(self._settings["filters"]["videos"]) - def get_text_filter(self) -> tuple: return tuple(self._settings["filters"]["text"]) - def get_music_filter(self) -> tuple: return tuple(self._settings["filters"]["music"]) - def get_images_filter(self) -> tuple: return tuple(self._settings["filters"]["images"]) - def get_pdf_filter(self) -> tuple: return tuple(self._settings["filters"]["pdf"]) - - def get_success_color(self) -> str: return self._theming["success_color"] - def get_warning_color(self) -> str: return self._theming["warning_color"] - def get_error_color(self) -> str: return self._theming["error_color"] - - def is_trace_debug(self) -> str: return self._trace_debug - def is_debug(self) -> str: return self._debug - - def get_ch_log_lvl(self) -> str: return self._settings["debugging"]["ch_log_lvl"] - def get_fh_log_lvl(self) -> str: return self._settings["debugging"]["fh_log_lvl"] - - def set_trace_debug(self, trace_debug): - self._trace_debug = trace_debug - - def set_debug(self, debug): - self._debug = debug - - - def load_settings(self): - with open(self._CONFIG_FILE) as f: - self._settings = json.load(f) - self._config = self._settings["config"] - self._theming = self._settings["theming"] - - def save_settings(self): - with open(self._CONFIG_FILE, 'w') as outfile: - json.dump(self._settings, outfile, separators=(',', ':'), indent=4) \ No newline at end of file diff --git a/src/libs/settings/start_check_mixin.py b/src/libs/settings/start_check_mixin.py index 9221e11..a15d2b7 100644 --- a/src/libs/settings/start_check_mixin.py +++ b/src/libs/settings/start_check_mixin.py @@ -2,6 +2,7 @@ import os import json import inspect +from contextlib import suppress # Lib imports @@ -24,8 +25,8 @@ class StartCheckMixin: self._print_pid(pid) return - if os.path.exists(self._PID_FILE): - with open(self._PID_FILE, "r") as f: + if os.path.exists(self.path_manager._PID_FILE): + with open(self.path_manager._PID_FILE, "r") as f: pid = f.readline().strip() if pid not in ("", None): if self.is_pid_alive( int(pid) ): @@ -56,8 +57,9 @@ class StartCheckMixin: print(f"{APP_NAME} PID: {pid}") def _clean_pid(self): - os.unlink(self._PID_FILE) + with suppress(FileNotFoundError, PermissionError): + os.unlink(self.path_manager._PID_FILE) def _write_pid(self, pid): - with open(self._PID_FILE, "w") as _pid: - _pid.write(f"{pid}") + with open(self.path_manager._PID_FILE, "w") as _pid: + _pid.write(f"{pid}") \ No newline at end of file diff --git a/src/libs/singleton.py b/src/libs/singleton.py index 23b7191..cfe6534 100644 --- a/src/libs/singleton.py +++ b/src/libs/singleton.py @@ -1,4 +1,5 @@ # Python imports +from typing import Type, TypeVar, Any # Lib imports @@ -11,14 +12,21 @@ class SingletonError(Exception): +T = TypeVar('T', bound = 'Singleton') + + + class Singleton: - ccount = 0 + _instances = {} - def __new__(cls, *args, **kwargs): - obj = super(Singleton, cls).__new__(cls) - cls.ccount += 1 + def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: + if cls in cls._instances: return cls._instances[cls] - if cls.ccount == 2: - raise SingletonError(f"Exceeded {cls.__name__} instantiation limit...") + instance = super().__new__(cls) + cls._instances[cls] = instance + return instance - return obj + @classmethod + def destroy(cls): + if cls in cls._instances: + del cls._instances[cls] diff --git a/src/libs/singleton_raised.py b/src/libs/singleton_raised.py new file mode 100644 index 0000000..bb88d9b --- /dev/null +++ b/src/libs/singleton_raised.py @@ -0,0 +1,29 @@ +# Python imports +from typing import Type, TypeVar, Any + +# Lib imports + +# Application imports + + + +class SingletonError(Exception): + pass + + + +T = TypeVar('T', bound='SingletonRaised') + +class SingletonRaised: + __instance = None + + def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: + if cls.__instance is not None: + raise SingletonError(f"'{cls.__name__}' is a Singleton. Cannot create a new instance...") + + cls.__instance = super(SingletonRaised, cls).__new__(cls) + return cls.__instance + + def __init__(self) -> None: + if self.__instance is not None: + return diff --git a/src/libs/status_icon.py b/src/libs/status_icon.py new file mode 100644 index 0000000..2769a05 --- /dev/null +++ b/src/libs/status_icon.py @@ -0,0 +1,67 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('AppIndicator3', '0.1') +from gi.repository import Gtk +from gi.repository import GLib +from gi.repository import AppIndicator3 + +# Application imports + + + +class StatusIcon(): + """ StatusIcon for Application to go to Status Tray. """ + + def __init__(self): + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + ... + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + status_menu = Gtk.Menu() + icon_theme = Gtk.IconTheme.get_default() + check_menu_item = Gtk.CheckMenuItem.new_with_label("Update icon") + quit_menu_item = Gtk.MenuItem.new_with_label("Quit") + + # Create StatusNotifierItem + self.indicator = AppIndicator3.Indicator.new( + f"{APP_NAME}-statusicon", + "gtk-info", + AppIndicator3.IndicatorCategory.APPLICATION_STATUS) + self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) + + check_menu_item.connect("activate", self.check_menu_item_cb) + quit_menu_item.connect("activate", self.quit_menu_item_cb) + icon_theme.connect('changed', self.icon_theme_changed_cb) + + self.indicator.set_menu(status_menu) + status_menu.append(check_menu_item) + status_menu.append(quit_menu_item) + status_menu.show_all() + + def update_icon(self, icon_name): + self.indicator.set_icon(icon_name) + + def check_menu_item_cb(self, widget, data = None): + icon_name = "parole" if widget.get_active() else "gtk-info" + self.update_icon(icon_name) + + def icon_theme_changed_cb(self, theme): + self.update_icon("gtk-info") + + def quit_menu_item_cb(self, widget, data = None): + event_system.emit("tear-down") diff --git a/src/libs/widget_registery.py b/src/libs/widget_registery.py new file mode 100644 index 0000000..fe8b3ce --- /dev/null +++ b/src/libs/widget_registery.py @@ -0,0 +1,65 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from .controllers.controller_base import ControllerBase +from .dto.base_event import BaseEvent + + + +class WidgetRegisteryController(ControllerBase): + """docstring for WidgetRegisteryController.""" + + def __init__(self): + super(WidgetRegisteryController, self).__init__() + + self._builder: Gtk.Builder = None + self.objects: dict = {} + self.builder_keys: list = [] + + self._load_glade_file() + + + def _load_glade_file(self): + self._builder = Gtk.Builder.new_from_file( settings_manager.path_manager.get_glade_file() ) + settings_manager.set_builder(self._builder) + + widgets = self._builder.get_objects() + for widget in widgets: + if not hasattr(widget, "get_name"): continue + self.builder_keys.append( widget.get_name() ) + + def _controller_message(self, event: BaseEvent): + ... + + def list_objects(self, id: str) -> list: + return self.objects.keys() + self.builder_keys + + def list_non_builder_objects(self, id: str) -> list: + return self.objects.keys() + + def list_builder_objects(self, id: str) -> list: + return self.builder_keys + + def get_object(self, id: str) -> any: + if id in self.objects: + return self.objects[id] + + return self._builder.get_object(id) + + def expose_object(self, id: str, object: any, use_gtk: bool = False): + if not use_gtk: + self.objects[id] = object + return + + self._builder.expose_object(id, object) + self.builder_keys.append(id) + + def dereference_object(self, id: str): + self.builder_keys.remove(id) + if id in self.objects: + del self.objects[id] diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py index 5624b32..8fde4dc 100644 --- a/src/plugins/__init__.py +++ b/src/plugins/__init__.py @@ -1,3 +1,5 @@ """ Gtk Bound Plugins Module """ + +from .controller import plugins_controller diff --git a/src/plugins/controller.py b/src/plugins/controller.py new file mode 100644 index 0000000..048fd8e --- /dev/null +++ b/src/plugins/controller.py @@ -0,0 +1,182 @@ +# Python imports +import os +import sys +import importlib +import traceback + +from os.path import join +from os.path import isdir + +# Lib imports +import gi +from gi.repository import Gtk +from gi.repository import GLib + +# Application imports +from libs.event_factory import Event_Factory, App_Event_Types +from libs.controllers.controller_base import ControllerBase +from libs.dto.plugins.manifest_meta import ManifestMeta +from libs.dto.base_event import BaseEvent + +from .manifest_manager import ManifestManager +from .plugins_controller_mixin import PluginsControllerMixin +from .plugin_reload_mixin import PluginReloadMixin +from .plugin_context import PluginContext +from .plugins_ui import PluginsUI + + + +class PluginsControllerException(Exception): + ... + + + +class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixin): + """ PluginsController controller """ + + def __init__(self): + super(PluginsController, self).__init__() + + # path = os.path.dirname(os.path.realpath(__file__)) + # sys.path.insert(0, path) # NOTE: I think I'm not using this correctly... + + self.plugins_ui: PluginsUI = PluginsUI() + self._manifest_manager: ManifestManager = ManifestManager() + + self._plugin_collection: list = [] + self._plugins_path: str = settings_manager.path_manager.get_plugins_path() + + self._set_plugins_watcher() + + + def _controller_message(self, event: BaseEvent): + for manifest_meta in self._plugin_collection: + manifest_meta.instance._controller_message(event) + + if isinstance(event, App_Event_Types.TogglePluginsUiEvent): + self.toggle_plugins_ui() + + 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 not os.path.isdir(_path): continue + self._collect_search_locations(_path, locations) + + def _load_plugins( + self, + manifest_metas: list = [], + is_pre_launch: bool = False + ): + parent_path = os.getcwd() + + for manifest_meta in manifest_metas: + try: + path, \ + folder, \ + manifest = manifest_meta.path, manifest_meta.folder, manifest_meta.manifest + target = join(path, "plugin.py") + + if not os.path.exists(target): + raise PluginsControllerException( + "Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load..." + ) + + module = self._load_plugin_module(path, folder, target) + + self._handle_plugin_execute(is_pre_launch, module, manifest_meta) + except PluginsControllerException as e: + logger.info(f"Malformed Plugin: Not loading -->: '{manifest_meta.folder}' !") + logger.debug(f"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 _handle_plugin_execute( + self, is_pre_launch: bool, module, manifest_meta + ): + if not is_pre_launch: + GLib.idle_add( + self.execute_plugin, module, manifest_meta + ) + return + + self.execute_plugin(module, manifest_meta) + + def pre_launch_plugins(self): + logger.info(f"Loading pre-launch plugins...") + manifest_metas: list = self._manifest_manager.get_pre_launch_plugins() + self._load_plugins(manifest_metas, is_pre_launch = True) + + for manifest_meta in manifest_metas: + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def post_launch_plugins(self): + logger.info(f"Loading post-launch plugins...") + manifest_metas: list = self._manifest_manager.get_post_launch_plugins() + self._load_plugins(manifest_metas) + + for manifest_meta in manifest_metas: + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def manual_launch_plugins(self): + logger.info(f"Collecting manual-launch plugins...") + manifest_metas: list = self._manifest_manager.get_manual_launch_plugins() + + for manifest_meta in manifest_metas: + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def toggle_plugin_load_state(self, widget, manifest_meta): + if manifest_meta.instance: + self._plugin_collection.remove(manifest_meta) + manifest_meta.instance.unload() + manifest_meta.instance = None + widget.set_label("Load") + return + + self._load_plugins( [manifest_meta] ) + widget.set_label("Unload") + + def execute_plugin(self, module: type, manifest_meta: ManifestMeta): + plugin = module.Plugin() + plugin.plugin_context: PluginContext = self.create_plugin_context() + + manifest = manifest_meta.manifest + manifest_meta.instance = plugin + + if manifest.requests.bind_keys: + keybindings.append_bindings( manifest.requests.bind_keys ) + + manifest_meta.instance.load() + manifest_meta.instance.run() + + self._plugin_collection.append(manifest_meta) + + def create_plugin_context(self): + plugin_context: PluginContext = PluginContext() + + plugin_context.request_ui_element: callable = self.request_ui_element + plugin_context.emit: callable = self.emit + plugin_context.emit_to: callable = self.emit_to + plugin_context.emit_to_selected: callable = self.emit_to_selected + plugin_context.register_controller: callable = self.register_controller + plugin_context.unregister_controller: callable = self.unregister_controller + + return plugin_context + + def toggle_plugins_ui(self, widget = None): + self.plugins_ui.hide() if self.plugins_ui.is_visible() else self.plugins_ui.show() + +plugins_controller = PluginsController() diff --git a/src/plugins/manifest.py b/src/plugins/manifest.py deleted file mode 100644 index 4088eed..0000000 --- a/src/plugins/manifest.py +++ /dev/null @@ -1,64 +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 - - -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"] - - return self._plugin, loading_data diff --git a/src/plugins/manifest_manager.py b/src/plugins/manifest_manager.py new file mode 100644 index 0000000..ffe7e91 --- /dev/null +++ b/src/plugins/manifest_manager.py @@ -0,0 +1,75 @@ +# Python imports +import os +import json +from os.path import join + +# Lib imports + +# Application imports +from libs.dto.plugins.manifest_meta import ManifestMeta +from libs.dto.plugins.manifest import Manifest + + + +class ManifestMapperException(Exception): + ... + + + +class ManifestManager: + def __init__(self): + + self._plugins_path: str = \ + settings_manager.path_manager.get_plugins_path() + + self.pre_launch_manifests: list = [] + self.post_launch_manifests: list = [] + self.manual_launch_manifests: list = [] + + self.load_manifests() + + + def load_manifests(self): + logger.info(f"Loading manifests...") + + for path, folder in [ + [join(self._plugins_path, item), item] + for item in os.listdir(self._plugins_path) + if os.path.isdir( join(self._plugins_path, item) ) + ]: + self.load(folder, path) + + def load(self, folder, path) -> ManifestMeta: + 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 not manifest.autoload: + self.manual_launch_manifests.append(manifest_meta) + return manifest_meta + + if manifest.pre_launch: + self.pre_launch_manifests.append(manifest_meta) + else: + self.post_launch_manifests.append(manifest_meta) + + return manifest_meta + + def get_pre_launch_plugins(self) -> list: + return self.pre_launch_manifests + + def get_post_launch_plugins(self) -> list: + return self.post_launch_manifests + + def get_manual_launch_plugins(self) -> list: + return self.manual_launch_manifests diff --git a/src/plugins/plugin_base.py b/src/plugins/plugin_base.py deleted file mode 100644 index 3130bb4..0000000 --- a/src/plugins/plugin_base.py +++ /dev/null @@ -1,61 +0,0 @@ -# Python imports -import os -import time - -# Lib imports - -# Application imports - - -class PluginBaseException(Exception): - ... - - -class PluginBase: - def __init__(self): - 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_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 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 subscribe_to_events(self): - ... - - - def clear_children(self, widget: type) -> None: - """ Clear children of a gtk widget. """ - for child in widget.get_children(): - widget.remove(child) diff --git a/src/plugins/plugin_context.py b/src/plugins/plugin_context.py new file mode 100644 index 0000000..eb093e0 --- /dev/null +++ b/src/plugins/plugin_context.py @@ -0,0 +1,42 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.base_event import BaseEvent + + + +class PluginContextException(Exception): + ... + + + +class PluginContext: + """ PluginContext """ + + def __init__(self): + super(PluginContext, self).__init__() + + + def _controller_message(self, event: BaseEvent): + raise PluginContextException("Plugin Context '_controller_message' must be overridden...") + + def request_ui_element(self, element_id: str): + raise PluginContextException("Plugin Context 'request_ui_element' must be overridden...") + + def emit(self, event: BaseEvent): + raise PluginContextException("Plugin Context 'emit' must be overridden...") + + def emit_to(self, name: str, event: BaseEvent): + raise PluginContextException("Plugin Context 'emit_to' must be overridden...") + + def emit_to_selected(self, names: list[str], event: BaseEvent): + raise PluginContextException("Plugin Context 'emit_to_selected' must be overridden...") + + def register_controller(self, name: str, controller): + raise PluginContextException("Plugin Context 'register_controller' must be overridden...") + + def unregister_controller(self, name: str): + raise PluginContextException("Plugin Context 'unregister_controller' must be overridden...") + diff --git a/src/plugins/plugin_reload_mixin.py b/src/plugins/plugin_reload_mixin.py new file mode 100644 index 0000000..9b665e2 --- /dev/null +++ b/src/plugins/plugin_reload_mixin.py @@ -0,0 +1,77 @@ +# 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 is Gio.FileMonitorEvent.RENAMED: + ... + + if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.MOVED_IN]: + self.add_plugin(file) + + if eve_type in [Gio.FileMonitorEvent.DELETED, Gio.FileMonitorEvent.MOVED_OUT]: + self.remove_plugin(file) + + def add_plugin(self, file: str) -> None: + logger.info(f"Adding plugin: {file.get_uri()}") + uri = file.get_uri() + path = uri.replace("file://", "") + folder = path.split("/")[-1] + manifest_meta = self._manifest_manager.load(folder, path) + + self._load_plugins( [manifest_meta] ) + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def remove_plugin(self, file: str) -> None: + logger.info(f"Removing plugin: {file.get_uri()}") + + manifests = self._manifest_manager.pre_launch_manifests \ + + self._manifest_manager.post_launch_manifests \ + + self._manifest_manager.manual_launch_manifests + + for manifest_meta in manifests: + if not manifest_meta.folder in file.get_uri(): continue + + if manifest_meta in self._manifest_manager.pre_launch_manifests: + self._manifest_manager.pre_launch_manifests.remove(manifest_meta) + elif manifest_meta in self._manifest_manager.post_launch_manifests: + self._manifest_manager.post_launch_manifests.remove(manifest_meta) + elif manifest_meta in self._manifest_manager.manual_launch_manifests: + self._manifest_manager.manual_launch_manifests.remove(manifest_meta) + + self.plugins_ui.remove_row(manifest_meta) + break + + del manifests + for manifest_meta in self._plugin_collection[:]: + if not manifest_meta.folder in file.get_uri(): continue + + manifest_meta.instance.unload() + manifest_meta.instance = None + self._plugin_collection.remove(manifest_meta) + + break diff --git a/src/plugins/plugin_types/__init__.py b/src/plugins/plugin_types/__init__.py new file mode 100644 index 0000000..5814687 --- /dev/null +++ b/src/plugins/plugin_types/__init__.py @@ -0,0 +1,7 @@ +""" + Plugin Types Module +""" + +from .plugin_base import PluginBase +from .plugin_ui import PluginUI +from .plugin_code import PluginCode diff --git a/src/plugins/plugin_types/plugin_base.py b/src/plugins/plugin_types/plugin_base.py new file mode 100644 index 0000000..221643f --- /dev/null +++ b/src/plugins/plugin_types/plugin_base.py @@ -0,0 +1,46 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.base_event import BaseEvent + +from ..plugin_context import PluginContext + + + +class PluginBaseException(Exception): + ... + + + +class PluginBase: + def __init__(self, *args, **kwargs): + super(PluginBase, self).__init__(*args, **kwargs) + + self.plugin_context: PluginContext = None + + + def _controller_message(self, event: BaseEvent): + raise PluginBaseException("Plugin Base '_controller_message' must be overriden by Plugin") + + def load(self): + raise PluginBaseException("Plugin Base 'load' must be overriden by Plugin") + + def unload(self): + raise PluginBaseException("Plugin Base 'unload' must be overriden by Plugin") + + def run(self): + raise PluginBaseException("Plugin Base 'run' must be overriden by Plugin") + + def request_ui_element(self, element_id: str): + raise PluginBaseException("Plugin Base 'request_ui_element' must be overriden by Plugin") + + def emit(self, event: BaseEvent): + raise PluginBaseException("Plugin Base 'emit' must be overriden by Plugin") + + def emit_to(self, name: str, event: BaseEvent): + raise PluginBaseException("Plugin Base 'emit_to' must be overriden by Plugin") + + def emit_to_selected(self, names: list[str], event: BaseEvent): + raise PluginBaseException("Plugin Base 'emit_to_selected' must be overriden by Plugin") diff --git a/src/plugins/plugin_types/plugin_code.py b/src/plugins/plugin_types/plugin_code.py new file mode 100644 index 0000000..3c6188d --- /dev/null +++ b/src/plugins/plugin_types/plugin_code.py @@ -0,0 +1,50 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.base_event import BaseEvent + +from ..plugin_context import PluginContext +from .plugin_base import PluginBase + + + +class PluginCodeException(Exception): + ... + + + +class PluginCode(PluginBase): + def __init__(self, *args, **kwargs): + super(PluginCode, self).__init__(*args, **kwargs) + + self.plugin_context: PluginContext = None + + + def _controller_message(self, event: BaseEvent): + raise PluginCodeException("Plugin Code '_controller_message' must be overriden by Plugin") + + def load(self): + raise PluginCodeException("Plugin Code 'load' must be overriden by Plugin") + + def run(self): + raise PluginCodeException("Plugin Code 'run' must be overriden by Plugin") + + def register_controller(self, name: str, controller): + return self.plugin_context.register_controller(name, controller) + + def unregister_controller(self, name: str): + return self.plugin_context.unregister_controller(name) + + def request_ui_element(self, element_id: str): + return self.plugin_context.request_ui_element(element_id) + + def emit(self, event: BaseEvent): + return self.plugin_context.emit(event) + + def emit_to(self, name: str, event: BaseEvent): + return self.plugin_context.emit_to(name, event) + + def emit_to_selected(self, names: list[str], event: BaseEvent): + return self.plugin_context.emit_to_selected(names, event) diff --git a/src/plugins/plugin_types/plugin_ui.py b/src/plugins/plugin_types/plugin_ui.py new file mode 100644 index 0000000..f09e09f --- /dev/null +++ b/src/plugins/plugin_types/plugin_ui.py @@ -0,0 +1,44 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.base_event import BaseEvent + +from ..plugin_context import PluginContext +from .plugin_base import PluginBase + + + +class PluginCodeException(Exception): + ... + + + +class PluginUI(PluginBase): + def __init__(self, *args, **kwargs): + super(PluginUI, self).__init__(*args, **kwargs) + + self.plugin_context: PluginContext = None + + + def _controller_message(self, event: BaseEvent): + raise PluginCodeException("Plugin UI '_controller_message' must be overriden by Plugin") + + def load(self): + raise PluginCodeException("Plugin UI 'load' must be overriden by Plugin") + + def run(self): + raise PluginCodeException("Plugin UI 'run' must be overriden by Plugin") + + def request_ui_element(self, element_id: str): + return self.plugin_context.request_ui_element(element_id) + + def emit(self, event: BaseEvent): + return self.plugin_context.emit(event) + + def emit_to(self, name: str, event: BaseEvent): + return self.plugin_context.emit_to(name, event) + + def emit_to_selected(self, names: list[str], event: BaseEvent): + return self.plugin_context.emit_to_selected(names, event) diff --git a/src/plugins/plugins_controller.py b/src/plugins/plugins_controller.py deleted file mode 100644 index f0561f7..0000000 --- a/src/plugins/plugins_controller.py +++ /dev/null @@ -1,119 +0,0 @@ -# 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.get_builder() - self._plugins_path = settings.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: - print(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: - print(f"Malformed Plugin: Not loading -->: '{folder}' !") - 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_all() - - 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_fm_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: - print(f"Reloading plugins... stub.") diff --git a/src/plugins/plugins_controller_mixin.py b/src/plugins/plugins_controller_mixin.py new file mode 100644 index 0000000..2dc72be --- /dev/null +++ b/src/plugins/plugins_controller_mixin.py @@ -0,0 +1,20 @@ +# Python imports + +# Lib imports + +# Application imports + + + +class InvalidPluginException(Exception): + ... + + + +class PluginsControllerMixin: + + def request_ui_element(self, target_id: str): + if not target_id in widget_registery.objects: + raise InvalidPluginException('Unknown UI "target_id" given in requests.') + + return widget_registery.objects[target_id] diff --git a/src/plugins/plugins_ui.py b/src/plugins/plugins_ui.py new file mode 100644 index 0000000..17649e4 --- /dev/null +++ b/src/plugins/plugins_ui.py @@ -0,0 +1,122 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib + +# Application imports + + + +class PluginsUI(Gtk.Dialog): + def __init__(self): + super(PluginsUI, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + header = Gtk.HeaderBar() + self.ctx = self.get_style_context() + self.ctx.add_class("plugin-ui") + + self.set_title("Plugins") + self.set_size_request(450, 530) + self.set_modal(False) + self.set_deletable(False) + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + + header.set_title("Plugins") + self.set_titlebar(header) + header.show() + + window = widget_registery.get_object("main-window") + self.set_transient_for(window) + self.set_destroy_with_parent(True) + + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + + def _setup_signals(self): + self.connect("focus-out-event", self._on_focus_out) + self.connect("key-release-event", self._on_key_release) + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + widget_registery.expose_object("plugin-ui", self) + + content_area = self.get_content_area() + scrolled_win = Gtk.ScrolledWindow() + viewport = Gtk.Viewport() + self.list_box = Gtk.ListBox() + + self.list_box.set_selection_mode( Gtk.SelectionMode.NONE ) + scrolled_win.set_vexpand(True) + + viewport.add(self.list_box) + scrolled_win.add(viewport) + content_area.add(scrolled_win) + + scrolled_win.show_all() + + def _on_key_release(self, widget, event): + ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK + shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK + + if ctrl_pressed: + if shift_pressed: + if event.keyval == Gdk.KEY_P: + self.hide() + + def _on_focus_out(self, *args): + self.hide() + GLib.idle_add(self.hide) + + def add_row(self, manifest_meta, callback: callable): + box = Gtk.Box() + plugin_lbl = Gtk.Label(label = manifest_meta.manifest.name) + author_lbl = Gtk.Label(label = manifest_meta.manifest.author) + version_lbl = Gtk.Label(label = manifest_meta.manifest.version) + is_autoload = manifest_meta.manifest.autoload + toggle_bttn = Gtk.ToggleButton(label = "Unload" if is_autoload else "Load") + + toggle_bttn.set_active(is_autoload) + plugin_lbl.set_hexpand(True) + box.set_hexpand(True) + version_lbl.set_margin_left(15) + version_lbl.set_margin_right(15) + toggle_bttn.set_size_request(120, -1) + + toggle_bttn.toggle_id = \ + toggle_bttn.connect("toggled", callback, manifest_meta) + box.toggle_bttn = toggle_bttn + + box.add(plugin_lbl) + box.add(author_lbl) + box.add(version_lbl) + box.add(toggle_bttn) + box.manifest_meta = manifest_meta + + box.show_all() + self.list_box.add(box) + + def remove_row(self, manifest_meta): + for row in self.list_box.get_children(): + child = row.get_children()[0] + if not child.manifest_meta == manifest_meta: continue + + child.manifest_meta = None + toggle_bttn = getattr(child, "toggle_bttn", None) + toggle_bttn.disconnect(toggle_bttn.toggle_id) + + self.list_box.remove(row) + child.destroy() + break diff --git a/user_config/usr/share/mirage2/Main_Window.glade b/user_config/usr/share/mirage2/Main_Window.glade new file mode 100644 index 0000000..c1a1964 --- /dev/null +++ b/user_config/usr/share/mirage2/Main_Window.glade @@ -0,0 +1,28 @@ + + + + + + True + False + vertical + + + True + False + Loaded Me From Glade! + + + False + True + 0 + + + + + + + + + + diff --git a/user_config/usr/share/mirage2/contexct_menu.json b/user_config/usr/share/mirage2/contexct_menu.json index 2c63c08..0967ef4 100644 --- a/user_config/usr/share/mirage2/contexct_menu.json +++ b/user_config/usr/share/mirage2/contexct_menu.json @@ -1,2 +1 @@ -{ -} +{}