Moved mirage2 to use newer GTK template structure patterns

This commit is contained in:
2026-05-22 18:27:16 -05:00
parent 447b087188
commit bbd8fd7a2c
94 changed files with 2846 additions and 814 deletions

View File

@@ -1,50 +1,73 @@
# Python imports # Python imports
import builtins import builtins
import traceback
import threading import threading
import sys
# Lib imports # Lib imports
# Application 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.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. # NOTE: Threads WILL NOT die with parent's destruction.
def threaded_wrapper(fn): def threaded_wrapper(fn):
def wrapper(*args, **kwargs): 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 return wrapper
# NOTE: Threads WILL die with parent's destruction. # NOTE: Threads WILL die with parent's destruction.
def daemon_threaded_wrapper(fn): def daemon_threaded_wrapper(fn):
def wrapper(*args, **kwargs): 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 return wrapper
def sizeof_fmt_def(num, suffix="B"): def call_chain_wrapper(fn):
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]: def wrapper(*args, **kwargs):
if abs(num) < 1024.0: for line in traceback.format_stack():
return f"{num:3.1f} {unit}{suffix}" print( line.strip() )
num /= 1024.0
return f"{num:.1f} Yi{suffix}" return fn(*args, **kwargs)
return wrapper
# NOTE: Just reminding myself we can add to builtins two different ways... # NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()}) # __builtins__.update({"event_system": Builtins()})
builtins.APP_NAME = "Mirage2" builtins.APP_NAME = "Mirage2"
builtins.keybindings = Keybindings() builtins.keybindings = Keybindings()
builtins.event_system = EventSystem() builtins.event_system = EventSystem()
builtins.endpoint_registry = EndpointRegistry() builtins.settings_manager = SettingsManager()
builtins.settings = Settings() builtins.widget_registery = WidgetRegisteryController()
builtins.logger = Logger(settings.get_home_config_path(), \
_ch_log_lvl=settings.get_ch_log_lvl(), \ settings_manager.load_settings()
_fh_log_lvl=settings.get_fh_log_lvl()).get_logger()
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.threaded = threaded_wrapper
builtins.daemon_threaded = daemon_threaded_wrapper builtins.daemon_threaded = daemon_threaded_wrapper
builtins.sizeof_fmt = sizeof_fmt_def builtins.call_chain = call_chain_wrapper
builtins.event_sleep_time = 0.05
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

View File

@@ -1,3 +1,3 @@
""" """
Start of package. Src Package.
""" """

View File

@@ -4,12 +4,12 @@
import argparse import argparse
import faulthandler import faulthandler
import traceback import traceback
from setproctitle import setproctitle
import tracemalloc import tracemalloc
tracemalloc.start() tracemalloc.start()
# Lib imports # Lib imports
from setproctitle import setproctitle
# Application imports # Application imports
from __builtins__ import * from __builtins__ import *
@@ -17,8 +17,9 @@ from app import Application
def main(args, unknownargs): def main():
setproctitle(f'{APP_NAME}') setproctitle(f'{APP_NAME}')
settings_manager.set_start_load_time()
if args.debug == "true": if args.debug == "true":
settings_manager.set_debug(True) settings_manager.set_debug(True)
@@ -26,8 +27,10 @@ def main(args, unknownargs):
if args.trace_debug == "true": if args.trace_debug == "true":
settings_manager.set_trace_debug(True) settings_manager.set_trace_debug(True)
settings.do_dirty_start_check() settings_manager.do_dirty_start_check()
Application(args, unknownargs)
app = Application()
app.run()
@@ -36,19 +39,20 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
# Add long and short arguments # Add long and short arguments
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.") 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("--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("--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("--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("--file", "-f", default = "default", help = "JUST SOME FILE ARG.")
# Read arguments (If any...) # Read arguments (If any...)
args, unknownargs = parser.parse_known_args() args, unknownargs = parser.parse_known_args()
settings_manager.set_starting_args( args, unknownargs )
try: try:
faulthandler.enable() # For better debug info faulthandler.enable() # For better debug info
main(args, unknownargs) main()
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
quit() quit()

View File

@@ -1,6 +1,8 @@
# Python imports # Python imports
from contextlib import suppress
import signal import signal
import os import os
import json
# Lib imports # Lib imports
@@ -19,27 +21,44 @@ class AppLaunchException(Exception):
class Application: class Application:
""" docstring for Application. """ """ docstring for Application. """
def __init__(self, args, unknownargs): def __init__(self):
super(Application, self).__init__() super(Application, self).__init__()
if not settings.is_trace_debug():
self.load_ipc(args, unknownargs)
self.setup_debug_hook() self.setup_debug_hook()
Window(args, unknownargs).main()
def load_ipc(self, args, unknownargs): def run(self):
ipc_server = IPCServer() 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) self.ipc_realization_check(ipc_server)
if ipc_server.is_ipc_alive:
return True
if not ipc_server.is_ipc_alive: logger.warning(f"{APP_NAME} IPC Server Exists: Have sent path(s) to it and closing...")
for arg in unknownargs + [args.new_tab,]: files: list = []
if os.path.isfile(arg): for arg in unknownargs + [args.new_tab,]:
message = f"FILE|{arg}" if os.path.isfile(arg):
ipc_server.send_ipc_message(message) 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): def ipc_realization_check(self, ipc_server):
try: try:
@@ -47,18 +66,12 @@ class Application:
except Exception: except Exception:
ipc_server.send_test_ipc_message() ipc_server.send_test_ipc_message()
try:
ipc_server.create_ipc_listener()
except Exception as e:
...
def setup_debug_hook(self): def setup_debug_hook(self):
try: # Typically: ValueError: signal only works in main thread
with suppress(ValueError):
# kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows # kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows
signal.signal( signal.signal(
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"), vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR2"),
debug_signal_handler debug_signal_handler
) )
except ValueError:
# Typically: ValueError: signal only works in main thread
...

View File

@@ -1,3 +1,3 @@
""" """
Gtk Bound Signal Module Core Package
""" """

View File

@@ -1,3 +1,3 @@
""" """
Containers Module Containers Package
""" """

View File

@@ -6,8 +6,9 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # Application imports
from .left_box import LeftBox from .header_container import HeaderContainer
from .right_box import RightBox from .body_container import BodyContainer
from .footer_container import FooterContainer
@@ -17,19 +18,38 @@ class BaseContainer(Gtk.Box):
self._setup_styling() self._setup_styling()
self._setup_signals() self._setup_signals()
self._load_widgets() self._subscribe_to_events()
self.show_all() self.show()
def _setup_styling(self): def _setup_styling(self):
self.set_orientation(Gtk.Orientation.HORIZONTAL) self.ctx = self.get_style_context()
ctx = self.get_style_context() self.ctx.add_class("base-container")
ctx.add_class("container-padding-5px")
self.set_orientation(Gtk.Orientation.VERTICAL)
self._update_transparency()
def _setup_signals(self): 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): def _load_widgets(self):
self.add(LeftBox()) widget_registery.expose_object("base-container", self)
self.add(RightBox())
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}")

View File

@@ -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() )

View File

@@ -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())

View File

@@ -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) )

View File

@@ -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) )

View File

@@ -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) )

View File

@@ -0,0 +1,3 @@
"""
Containers > Mirage Package
"""

View File

@@ -6,7 +6,7 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # 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.image_list_widget = None
self.size = 0 self.size = 0
self.start = 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_styling()
self._setup_signals() self._setup_signals()
@@ -36,7 +36,7 @@ class ImageListScroll(Gtk.ScrolledWindow):
self.connect("edge-overshot", self._handle_edge_reached) self.connect("edge-overshot", self._handle_edge_reached)
def _subscribe_to_events(self): 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): def _load_widgets(self):
self.image_list_widget = ImageList() self.image_list_widget = ImageList()
@@ -46,7 +46,7 @@ class ImageListScroll(Gtk.ScrolledWindow):
def _update_list_size_constraints(self, size): def _update_list_size_constraints(self, size):
self.size = size self.size = size
self.start = 0 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): def _handle_edge_reached(self, widget, edge):
children = self.image_list_widget.get_children() children = self.image_list_widget.get_children()
@@ -75,5 +75,4 @@ class ImageListScroll(Gtk.ScrolledWindow):
def _unload_image(self, child): def _unload_image(self, child):
child.hide() child.hide()
# child.image.clear()
child.is_loaded = False child.is_loaded = False

View File

@@ -8,7 +8,7 @@ from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
# Application imports # 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 self._drag_start_y = 0
def set_cursor(self, type = None): def set_cursor(self, type = None):
window = settings.get_main_window() window = settings_manager.get_main_window()
cursor = Gdk.Cursor(type) cursor = Gdk.Cursor(type)
window.get_window().set_cursor(cursor) window.get_window().set_cursor(cursor)

View File

@@ -18,7 +18,7 @@ class ImageViewScroll(Gtk.ScrolledWindow):
def __init__(self): def __init__(self):
super(ImageViewScroll, self).__init__() super(ImageViewScroll, self).__init__()
self.fimages = settings.get_images_filter() self.fimages = tuple(settings_manager.settings.filters.images)
self.curent_dir = None self.curent_dir = None
self.size_request = None self.size_request = None
@@ -42,7 +42,7 @@ class ImageViewScroll(Gtk.ScrolledWindow):
self.connect('scroll-event', self.on_scroll) self.connect('scroll-event', self.on_scroll)
def _subscribe_to_events(self): 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): def _load_widgets(self):
vadjustment = self.get_vadjustment() vadjustment = self.get_vadjustment()
@@ -82,7 +82,7 @@ class ImageViewScroll(Gtk.ScrolledWindow):
has_loaded_image = False has_loaded_image = False
if not os.path.isdir(path): 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) path = os.path.dirname(path)
has_loaded_image = True has_loaded_image = True
@@ -104,9 +104,9 @@ class ImageViewScroll(Gtk.ScrolledWindow):
if not has_loaded_image: if not has_loaded_image:
img = img_list[0] img = img_list[0]
target = os.path.join(path, img) 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 @daemon_threaded
def _size_request_change(self, widget = None, rect = None): 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: if self.size_request.width != rect.width or self.size_request.height != rect.height:
self.size_request = rect 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): def on_scroll(self, widget = None, event = None):
accel_mask = Gtk.accelerator_get_default_mod_mask() accel_mask = Gtk.accelerator_get_default_mod_mask()
@@ -141,8 +141,8 @@ class ImageViewScroll(Gtk.ScrolledWindow):
adjustment.set_value(current_val - step_val) adjustment.set_value(current_val - step_val)
else: else:
if direction > 0: if direction > 0:
event_system.emit("zoom_out") event_system.emit("zoom-out")
else: else:
event_system.emit("zoom_in") event_system.emit("zoom-in")
return True # NOTE: Stop event propigation return True # NOTE: Stop event propigation

View File

@@ -23,7 +23,7 @@ class LeftBox(Gtk.Box):
def _setup_styling(self): def _setup_styling(self):
self.set_orientation(Gtk.Orientation.VERTICAL) 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) self.set_vexpand(True)
def _setup_signals(self): def _setup_signals(self):

View File

@@ -8,9 +8,9 @@ from gi.repository import Gtk
# Application imports # Application imports
from .image_view_scroll import ImageViewScroll from .image_view_scroll import ImageViewScroll
from ..widgets.button_controls import ButtonControls from core.widgets.button_controls import ButtonControls
from ..widgets.path_label import PathLabel from core.widgets.path_label import PathLabel
from ..widgets.ocr_window import OCRWindow from core.widgets.ocr_window import OCRWindow
@@ -35,7 +35,7 @@ class RightBox(Gtk.Box):
... ...
def _subscribe_to_events(self): 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): def _load_widgets(self):
window = OCRWindow() window = OCRWindow()

View File

@@ -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) )

View File

@@ -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

View File

@@ -0,0 +1,3 @@
"""
Controllers Package
"""

View File

@@ -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...")

View File

@@ -1,30 +1,37 @@
# Python imports # Python imports
import os import os
import subprocess import subprocess
from shutil import which
# Lib imports # Lib imports
# Application 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: def _collect_files_dirs(self):
''' ControllerData contains most of the state of the app at ay given time. It also has some support methods. ''' args, \
unknownargs = settings_manager.get_starting_args()
files = []
def setup_controller_data(self) -> None: for arg in unknownargs + [args.new_tab,]:
self.window = settings.get_main_window() if os.path.isfile(arg):
self.builder = None files.append(f"{arg}")
self.base_container = None
self.was_midified_key = False
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
self.setup_builder_and_container() if os.path.isdir(arg):
self.plugins = PluginsController() 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: def clear_console(self) -> None:
''' Clears the terminal screen. ''' ''' Clears the terminal screen. '''
@@ -56,14 +63,24 @@ class ControllerData:
for child in widget.get_children(): for child in widget.get_children():
widget.remove(child) widget.remove(child)
def get_clipboard_data(self, encoding="utf-8") -> str: def get_clipboard_data(self, encoding = "utf-8") -> str:
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE) 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() retcode = proc.wait()
data = proc.stdout.read() data = proc.stdout.read()
return data.decode(encoding).strip() return data.decode(encoding).strip()
def set_clipboard_data(self, data: type, encoding="utf-8") -> None: def set_clipboard_data(self, data: type, encoding = "utf-8") -> None:
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE) 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.write(data.encode(encoding))
proc.stdin.close() proc.stdin.close()
retcode = proc.wait() retcode = proc.wait()

View File

@@ -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 _:
...

View File

@@ -1,3 +0,0 @@
"""
Generic Mixins Module
"""

View File

@@ -1,3 +0,0 @@
"""
Signals module
"""

View File

@@ -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],))

View File

@@ -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):
...

View File

@@ -30,7 +30,7 @@ class ButtonControls(Gtk.ButtonBox):
... ...
def _load_widgets(self): def _load_widgets(self):
icons_path = settings.get_icons_path() icons_path = settings_manager.path_manager.get_icons_path()
center_widget = Gtk.ButtonBox() center_widget = Gtk.ButtonBox()
zoomout_button = Gtk.Button() zoomout_button = Gtk.Button()
lrotate_button = Gtk.Button() lrotate_button = Gtk.Button()
@@ -97,32 +97,32 @@ class ButtonControls(Gtk.ButtonBox):
self.set_center_widget(center_widget) self.set_center_widget(center_widget)
def _zoom_out(self, widget = None, eve = None): 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): 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): 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): def _scale_1_two_1(self, widget = None, eve = None):
self._unset_class(self.fit_button) self._unset_class(self.fit_button)
self._set_class(self.one2one_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): def _fit_to_container(self, widget = None, eve = None):
self._unset_class(self.one2one_button) self._unset_class(self.one2one_button)
self._set_class(self.fit_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): 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): 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): def _zoom_in(self, widget = None, eve = None):
event_system.emit("zoom_in") event_system.emit("zoom-in")
def _set_class(self, target): def _set_class(self, target):
ctx = target.get_style_context() ctx = target.get_style_context()
@@ -133,4 +133,4 @@ class ButtonControls(Gtk.ButtonBox):
ctx.remove_class("button-highlighted") ctx.remove_class("button-highlighted")
def _show_ocr(self, widget): def _show_ocr(self, widget):
event_system.emit("show_ocr") event_system.emit("show-ocr")

View File

@@ -18,8 +18,8 @@ class Image(Gtk.EventBox):
def __init__(self, path: str): def __init__(self, path: str):
super(Image, self).__init__() super(Image, self).__init__()
self._thumbnail_with = settings.get_thumbnail_with() self._thumbnail_with = settings_manager.settings.config.thumbnail_with
self._thumbnail_height = settings.get_thumbnail_height() self._thumbnail_height = settings_manager.settings.config.thumbnail_height
self.is_loaded = False self.is_loaded = False
self.image = None self.image = None
self.path = path self.path = path
@@ -45,7 +45,7 @@ class Image(Gtk.EventBox):
def set_image_to_view(self, widget = None, eve = None): def set_image_to_view(self, widget = None, eve = None):
if eve.button == 1: 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): def load_pixbuf(self):
self.set_from_pixbuf( self.get_pixbuf_data(self.path, \ self.set_from_pixbuf( self.get_pixbuf_data(self.path, \
@@ -56,7 +56,7 @@ class Image(Gtk.EventBox):
def set_from_pixbuf(self, pixbuf): def set_from_pixbuf(self, pixbuf):
self.image.set_from_pixbuf(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 path = self.path if not path else path
pixbuf = None pixbuf = None

View File

@@ -33,12 +33,12 @@ class ImageList(Gtk.Box):
... ...
def _subscribe_to_events(self): 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 _load_widgets(self):
... ...
def _clear_children(self, widget: type) -> None: def _clear_children(self, widget: Gtk.Object) -> None:
''' Clear children of a gtk widget. ''' ''' Clear children of a gtk widget. '''
for child in widget.get_children(): for child in widget.get_children():
widget.remove(child) widget.remove(child)
@@ -49,9 +49,10 @@ class ImageList(Gtk.Box):
path = os.path.join(self.path, img) path = os.path.join(self.path, img)
paths.append(path) paths.append(path)
paths.sort()
return paths 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: if not path or len(img_list) == 0:
return return
@@ -64,10 +65,14 @@ class ImageList(Gtk.Box):
for file in paths: for file in paths:
self.add( Image(file) ) 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() 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() children = self.get_children()
if len(children) <= j: if len(children) <= j:
j = len(children) - 1 j = len(children) - 1
@@ -78,7 +83,7 @@ class ImageList(Gtk.Box):
i += 1 i += 1
@daemon_threaded @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.load_pixbuf)
GLib.idle_add(child.show) GLib.idle_add(child.show)
Gtk.main_iteration() Gtk.main_iteration()

View File

@@ -47,18 +47,18 @@ class ImageView(ImageViewMixin, Gtk.Image):
... ...
def _subscribe_to_events(self): def _subscribe_to_events(self):
event_system.subscribe("size_allocate", self._size_allocate) event_system.subscribe("size-allocate", self._size_allocate)
event_system.subscribe("handle_file_from_dnd", self._handle_file_from_dnd) 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("get-active-image-path", self._get_active_image_path)
event_system.subscribe("zoom_out", self._zoom_out) event_system.subscribe("zoom-out", self._zoom_out)
event_system.subscribe("rotate_left", self._rotate_left) event_system.subscribe("rotate-left", self._rotate_left)
event_system.subscribe("vertical_flip", self._vertical_flip) event_system.subscribe("vertical-flip", self._vertical_flip)
event_system.subscribe("scale_1_two_1", self._scale_1_two_1) 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("fit-to-container", self._fit_to_container)
event_system.subscribe("horizontal_flip", self._horizontal_flip) event_system.subscribe("horizontal-flip", self._horizontal_flip)
event_system.subscribe("rotate_right", self._rotate_right) event_system.subscribe("rotate-right", self._rotate_right)
event_system.subscribe("zoom_in", self._zoom_in) event_system.subscribe("zoom-in", self._zoom_in)
def _load_widgets(self): def _load_widgets(self):
... ...
@@ -91,15 +91,22 @@ class ImageView(ImageViewMixin, Gtk.Image):
width = self.pixbuff.get_width() width = self.pixbuff.get_width()
height = self.pixbuff.get_height() height = self.pixbuff.get_height()
size = sizeof_fmt( getsize(path) ) size = self.sizeof_fmt( getsize(path) )
path = f"{path} | {width} x {height} | {size}" 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: if self.fit_to_win:
self._fit_to_container() self._fit_to_container()
else: else:
self._scale_1_two_1() 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): def set_as_gif(self, path):
image = None image = None
try: try:

View File

@@ -19,7 +19,7 @@ class OCRWindow(Gtk.Window):
def __init__(self): def __init__(self):
super(OCRWindow, self).__init__() 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.download_url = "https://github.com/AlexanderP/tesseract-appimage/releases/download/v5.3.3/tesseract-5.3.3-x86_64.AppImage"
self._setup_styling() self._setup_styling()
@@ -30,7 +30,7 @@ class OCRWindow(Gtk.Window):
def _setup_styling(self): def _setup_styling(self):
self.set_title(f"Tesseract OCR") 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_gravity(5) # 5 = CENTER
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
@@ -42,7 +42,7 @@ class OCRWindow(Gtk.Window):
self.connect("delete-event", self._tear_down) self.connect("delete-event", self._tear_down)
def _subscribe_to_events(self): 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): def _load_widgets(self):
scrolled_window = Gtk.ScrolledWindow() scrolled_window = Gtk.ScrolledWindow()

View File

@@ -33,12 +33,12 @@ class PathLabel(Gtk.Label):
self.set_margin_bottom(10) self.set_margin_bottom(10)
def _subscribe_to_events(self): 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 _load_widgets(self):
... ...
def update_path_label(self, path = None): def update_path_label(self, path: str):
if not path: return if not path: return
self.set_label(path) self.set_label(path)

View File

@@ -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):
...

View File

@@ -1,5 +1,4 @@
# Python imports # Python imports
import time
import signal import signal
# Lib imports # Lib imports
@@ -11,93 +10,151 @@ from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GLib from gi.repository import GLib
try:
from gi.repository import GdkX11
except ImportError:
logger.debug("Could not import X11 gir module...")
# Application imports # 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): class Window(Gtk.ApplicationWindow):
""" docstring for Window. """ """ docstring for Window. """
def __init__(self, args, unknownargs): def __init__(self):
super(Window, self).__init__() 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_styling()
self._setup_signals() self._setup_signals()
self._subscribe_to_events() self._subscribe_to_events()
self._load_widgets(args, unknownargs) self._set_window_data()
self.show()
# NOTE: Need to set size after show b/c get_allocation methods are initially incorrect if done beforehand...
self._set_size_constraints() self._set_size_constraints()
self.show()
def _setup_styling(self): def _setup_styling(self):
self.set_title(f"{APP_NAME}") 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_gravity(5) # 5 = CENTER
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
def _set_size_constraints(self): ctx = self.get_style_context()
self.set_default_size(settings.get_main_window_width(), ctx.add_class("main-window")
settings.get_main_window_height()) ctx.add_class(f"mw_transparency_{settings_manager.settings.theming.transparency}")
self.set_size_request(settings.get_main_window_min_width(),
settings.get_main_window_min_height())
def _setup_signals(self): def _setup_signals(self):
self.connect("delete-event", self._tear_down) self.connect("focus-in-event", self._on_focus_in_event)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._tear_down) 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): 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): def _handle_show(self, widget):
if settings.is_debug(): 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.set_interactive_debugging(True)
self._controller = BaseController()
self._controller = Controller(args, unknownargs) self._status_icon = StatusIcon()
if not self._controller: 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() ) 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: def _set_window_data(self) -> None:
screen = self.get_screen() screen = self.get_screen()
visual = screen.get_rgba_visual() 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_visual(visual)
self.set_app_paintable(True) self.set_app_paintable(True)
self.connect("draw", self._area_draw) # self.connect("draw", self._area_draw)
# bind css file # bind css file
cssProvider = Gtk.CssProvider() cssProvider = Gtk.CssProvider()
cssProvider.load_from_path( settings.get_css_file() )
screen = Gdk.Screen.get_default()
styleContext = Gtk.StyleContext() 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) styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def _area_draw(self, widget: Gtk.ApplicationWindow, cr: cairo.Context) -> None: 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.set_operator(cairo.OPERATOR_SOURCE)
cr.paint() cr.paint()
cr.set_operator(cairo.OPERATOR_OVER) cr.set_operator(cairo.OPERATOR_OVER)
def _tear_down(self, widget=None, eve=None): def _on_focus_in_event(self, widget, event):
settings.clear_pid() event_system.emit("pause-dnd-signals")
time.sleep(event_sleep_time)
Gtk.main_quit()
def main(self): 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() 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()

View File

@@ -1,3 +1,3 @@
""" """
Utils module Libs Package
""" """

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
"""
Libs Controllers Package
"""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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...")

View File

@@ -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)

6
src/libs/db/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
Libs DB Package
"""
from .models import User
from .db import DB

42
src/libs/db/db.py Normal file
View File

@@ -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)

15
src/libs/db/models.py Normal file
View File

@@ -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

View File

@@ -18,7 +18,7 @@ def debug_signal_handler(signal, frame):
rpdb2.start_embedded_debugger("foobar", True, True) rpdb2.start_embedded_debugger("foobar", True, True)
rpdb2.setbreak(depth=1) rpdb2.setbreak(depth=1)
return return
except StandardError: except Exception:
... ...
try: try:
@@ -26,7 +26,7 @@ def debug_signal_handler(signal, frame):
logger.debug("\n\nStarting embedded rconsole debugger...\n\n") logger.debug("\n\nStarting embedded rconsole debugger...\n\n")
rconsole.spawn_server() rconsole.spawn_server()
return return
except StandardError as ex: except Exception as ex:
... ...
try: try:
@@ -34,7 +34,15 @@ def debug_signal_handler(signal, frame):
logger.debug("\n\nStarting PuDB debugger...\n\n") logger.debug("\n\nStarting PuDB debugger...\n\n")
set_trace(paused = True) set_trace(paused = True)
return 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: try:
@@ -42,11 +50,11 @@ def debug_signal_handler(signal, frame):
logger.debug("\n\nStarting embedded PDB debugger...\n\n") logger.debug("\n\nStarting embedded PDB debugger...\n\n")
pdb.Pdb(skip=['gi.*']).set_trace() pdb.Pdb(skip=['gi.*']).set_trace()
return return
except StandardError as ex: except Exception as ex:
... ...
try: try:
import code import code
code.interact() code.interact()
except StandardError as ex: except Exception as ex:
logger.debug(f"{ex}, returning to normal program flow...") logger.debug(f"{ex}, returning to normal program flow...")

5
src/libs/dto/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""
Libs DTO(s) Package
"""
from .base_event import BaseEvent

View File

@@ -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

View File

@@ -0,0 +1,3 @@
"""
Libs Plugin DTO(s) Package
"""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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: [])

View File

@@ -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

100
src/libs/event_factory.py Normal file
View File

@@ -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'(?<!^)(?=[A-Z])', '_', base_name).lower()
class EventNamespace:
"""Dynamic namespace for event types."""
def __init__(self):
...
def _is_valid_event_class(self, obj) -> 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()

View File

@@ -13,18 +13,34 @@ class EventSystem(Singleton):
def __init__(self): def __init__(self):
self.subscribers = defaultdict(list) 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) 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) 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) 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: if event_type in self.subscribers:
for fn in self.subscribers[event_type]: for fn in self.subscribers[event_type]:
if data: if data:
@@ -35,7 +51,10 @@ class EventSystem(Singleton):
else: else:
fn() 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 """ """ NOTE: Should be used when signal has only one listener and vis-a-vis """
if event_type in self.subscribers: if event_type in self.subscribers:
response = None response = None

View File

@@ -2,6 +2,7 @@
import os import os
import threading import threading
import time import time
from contextlib import suppress
from multiprocessing.connection import Client from multiprocessing.connection import Client
from multiprocessing.connection import Listener 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. """ """ 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"): def __init__(self, ipc_address: str = '127.0.0.1', conn_type: str = "socket"):
self.is_ipc_alive = False 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._ipc_address = ipc_address
self._conn_type = conn_type self._conn_type = conn_type
self._ipc_authkey = b'' + bytes(f'{APP_NAME}-ipc', 'utf-8') self._ipc_authkey = b'' + bytes(f'{APP_NAME}-ipc', 'utf-8')
@@ -35,13 +36,14 @@ class IPCServer(Singleton):
self._subscribe_to_events() self._subscribe_to_events()
def _subscribe_to_events(self): 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: def create_ipc_listener(self) -> None:
if self._conn_type == "socket": if self._conn_type == "socket":
if os.path.exists(self._ipc_address) and settings.is_dirty_start(): if settings_manager.is_dirty_start():
os.unlink(self._ipc_address) with suppress(FileNotFoundError, PermissionError):
os.unlink(self._ipc_address)
listener = Listener(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey) listener = Listener(address=self._ipc_address, family="AF_UNIX", authkey=self._ipc_authkey)
elif "unsecured" not in self._conn_type: elif "unsecured" not in self._conn_type:
@@ -56,37 +58,55 @@ class IPCServer(Singleton):
@daemon_threaded @daemon_threaded
def _run_ipc_loop(self, listener) -> None: def _run_ipc_loop(self, listener) -> None:
# NOTE: Not thread safe if using with Gtk. Need to import GLib and use idle_add # NOTE: Not thread safe if using with Gtk. Need to import GLib and use idle_add
while True: while self.is_ipc_alive:
try: try:
conn = listener.accept() conn = listener.accept()
start_time = time.perf_counter() start_time = time.perf_counter()
self._handle_ipc_message(conn, start_time) 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: except Exception as e:
... logger.debug( f"Unexpected IPC error: {e}" )
finally:
conn.close()
listener.close() listener.close()
def _handle_ipc_message(self, conn, start_time) -> None: def _handle_ipc_message(self, conn, start_time) -> None:
while True: while self.is_ipc_alive:
msg = conn.recv() msg = conn.recv()
if settings.is_debug(): logger.debug(msg)
print(msg)
if "FILE|" in msg: if "FILE|" in msg:
file = msg.split("FILE|")[1].strip() file = msg.split("FILE|")[1].strip()
if file: 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: if "DIR|" in msg:
file = msg.split("DIR|")[1].strip() file = msg.split("DIR|")[1].strip()
if file: if file:
event_system.emit("handle_dir_from_ipc", file) event_system.emit("handle-dir-from-ipc", file)
conn.close() conn.close()
break break
if msg in ['close connection', 'close server']: if msg in ['close connection', 'close server', 'Empty Data...']:
conn.close() conn.close()
break break
@@ -109,9 +129,11 @@ class IPCServer(Singleton):
conn.send(message) conn.send(message)
conn.close() conn.close()
except ConnectionRefusedError as e: 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: 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: def send_test_ipc_message(self, message: str = "Empty Data...") -> None:
@@ -128,6 +150,9 @@ class IPCServer(Singleton):
except ConnectionRefusedError as e: except ConnectionRefusedError as e:
if self._conn_type == "socket": if self._conn_type == "socket":
logger.error("IPC Socket no longer valid.... Removing.") 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: except Exception as e:
logger.error( repr(e) ) logger.error( f"Unexpected IPC error: {e}" )

View File

@@ -42,6 +42,17 @@ class Keybindings(Singleton):
self.keymap = Gdk.Keymap.get_default() self.keymap = Gdk.Keymap.get_default()
self.configure({}) 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): def configure(self, bindings):
""" Accept new bindings and reconfigure with them """ """ Accept new bindings and reconfigure with them """
self.keys = bindings self.keys = bindings

View File

@@ -0,0 +1,3 @@
"""
Libs Mixins Package
"""

View File

@@ -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,))

View File

@@ -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)

View File

@@ -19,14 +19,28 @@ valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
class KeyboardSignalsMixin: class KeyboardSignalsMixin:
""" KeyboardSignalsMixin keyboard hooks controller. """ """ 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. # 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.ctrl_down = False
self.shift_down = False self.shift_down = False
self.alt_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): 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) modifiers = Gdk.ModifierType(user_data.get_state() & ~Gdk.ModifierType.LOCK_MASK)
self.was_midified_key = True if modifiers != 0 else False 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"]: 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) 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: if should_return:
self.was_midified_key = False self.was_midified_key = False
return return
@@ -65,30 +72,31 @@ class KeyboardSignalsMixin:
logger.debug(f"on_global_key_release_controller > mapping > {mapping}") logger.debug(f"on_global_key_release_controller > mapping > {mapping}")
if mapping: if mapping:
# See if in controller scope self.handle_mapped_key_event(mapping)
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)
else: else:
logger.debug(f"on_global_key_release_controller > key > {keyname}") self.handle_as_key_event_scope(keyname)
if self.ctrl_down: def handle_mapped_key_event(self, mapping):
if not keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]: try:
self.handle_key_event_system(None, mapping) self.handle_as_controller_scope(mapping)
else: 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): def handle_key_event_system(self, sender, eve_type):
event_system.emit(eve_type) event_system.emit(eve_type)
def keyboard_close_tab(self):
...

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
""" """
Settings module Libs Settings Package
""" """
from .settings import Settings from .manager import SettingsManager

View File

@@ -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)

View File

@@ -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

View File

@@ -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"
])

View File

@@ -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

View File

@@ -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"
])

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -2,6 +2,7 @@
import os import os
import json import json
import inspect import inspect
from contextlib import suppress
# Lib imports # Lib imports
@@ -24,8 +25,8 @@ class StartCheckMixin:
self._print_pid(pid) self._print_pid(pid)
return return
if os.path.exists(self._PID_FILE): if os.path.exists(self.path_manager._PID_FILE):
with open(self._PID_FILE, "r") as f: with open(self.path_manager._PID_FILE, "r") as f:
pid = f.readline().strip() pid = f.readline().strip()
if pid not in ("", None): if pid not in ("", None):
if self.is_pid_alive( int(pid) ): if self.is_pid_alive( int(pid) ):
@@ -56,8 +57,9 @@ class StartCheckMixin:
print(f"{APP_NAME} PID: {pid}") print(f"{APP_NAME} PID: {pid}")
def _clean_pid(self): 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): def _write_pid(self, pid):
with open(self._PID_FILE, "w") as _pid: with open(self.path_manager._PID_FILE, "w") as _pid:
_pid.write(f"{pid}") _pid.write(f"{pid}")

View File

@@ -1,4 +1,5 @@
# Python imports # Python imports
from typing import Type, TypeVar, Any
# Lib imports # Lib imports
@@ -11,14 +12,21 @@ class SingletonError(Exception):
T = TypeVar('T', bound = 'Singleton')
class Singleton: class Singleton:
ccount = 0 _instances = {}
def __new__(cls, *args, **kwargs): def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T:
obj = super(Singleton, cls).__new__(cls) if cls in cls._instances: return cls._instances[cls]
cls.ccount += 1
if cls.ccount == 2: instance = super().__new__(cls)
raise SingletonError(f"Exceeded {cls.__name__} instantiation limit...") cls._instances[cls] = instance
return instance
return obj @classmethod
def destroy(cls):
if cls in cls._instances:
del cls._instances[cls]

View File

@@ -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

67
src/libs/status_icon.py Normal file
View File

@@ -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")

View File

@@ -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]

View File

@@ -1,3 +1,5 @@
""" """
Gtk Bound Plugins Module Gtk Bound Plugins Module
""" """
from .controller import plugins_controller

182
src/plugins/controller.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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...")

View File

@@ -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

View File

@@ -0,0 +1,7 @@
"""
Plugin Types Module
"""
from .plugin_base import PluginBase
from .plugin_ui import PluginUI
from .plugin_code import PluginCode

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.")

View File

@@ -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]

122
src/plugins/plugins_ui.py Normal file
View File

@@ -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

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="glade_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="glade_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Loaded Me From Glade!</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
</interface>

View File

@@ -1,2 +1 @@
{ {}
}