Compare commits

..

8 Commits

Author SHA1 Message Date
1447a68fb0 Refactor controller architecture and multi-insert state
- Rename ControllerContext to ControllerMessageBus for clarity
- Switch ControllerBase to use SingletonRaised
- Replace MarkEventsMixin with MarkerManager for multi-cursor editing
- Add populate_popup event support for source view context menus
- Remove unused swap file events
- Moved JSON prettify feature to plugin
- Fix event name: "removed_file" -> "remove_file"
2026-02-28 01:10:28 -06:00
b724d41f6c Remove tabs UI from code editor and move to plugin. Enhance plugin system.
- Remove tabs controller, tab widget, and tabs widget files and move to plugin
- Delete plugins/README.txt
- Add register_controller method to controller system for plugin use
- Add error handling for plugin crashes via futures callback
2026-02-26 21:09:14 -06:00
597ac2b06a Fixed multi file ipc load speeds 2026-02-26 02:00:36 -06:00
2e84ad9fc1 Moved code preview; fixrf plugin code context 2026-02-26 01:11:56 -06:00
a2b8232d5e refactor: remove InfoBarWidget in favor of plugin-based one
- Replace direct InfoBarWidget with set_info_labels command that emits
  events to plugins
- Replace mini view widget in favor of plugin-based one
- Add widget registry exposure for code-container and editors-container
- Fix DND mixin exec_with_args call (tuple args issue)
- Add break statements in tabs_widget loops for efficiency
- Add _update_transparency call to BaseContainer
2026-02-25 23:26:12 -06:00
0b3579d485 Fixed main thread closure issue caused by async.run 2026-02-25 01:04:09 -06:00
ec571ffa90 Added ability to toggle view visibility. 2026-02-25 00:43:37 -06:00
3b205e28e6 Refactor source view states to use base class and fix open-files start path
- Add SourceViewsBaseState inheritance to Command, Insert, MultiInsert, and ReadOnly states
- Extract common focus_in_event logic to base class
- Fix open-files command to use current file's directory as file picker start path
- Extract modifier key state logic into reusable get_modkeys_states() method
- Pass modifier keys to command exec_with_args in key_press_event
- Add type hints to key_mapper methods
2026-02-24 23:32:29 -06:00
80 changed files with 1181 additions and 574 deletions

View File

@@ -1,31 +0,0 @@
### Note
Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with.
Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use.
### Manifest Example (All are required even if empty.)
```
class Manifest:
name: str = "Example Plugin"
author: str = "John Doe"
version: str = "0.0.1"
support: str = ""
pre_launch: bool = False
requests: {} = {
'pass_ui_objects': ["plugin_control_list"],
'pass_events': True,
'bind_keys': []
}
```
### Requests
```
requests: {} = {
'pass_events': true, # If empty or not present will be ignored.
"pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin.
'bind_keys': [f"{name}||send_message:<Control>f"],
f"{name}||do_save:<Control>s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right.
}
```

View File

@@ -0,0 +1,3 @@
"""
Plugin Module
"""

View File

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

View File

@@ -0,0 +1,97 @@
# Python imports
# Lib imports
# Application imports
class Autopairs:
def __init__(self):
...
def handle_word_wrap(self, buffer, char_str: str):
wrap_block = self.get_wrap_block(char_str)
if not wrap_block: return
selection = buffer.get_selection_bounds()
if not selection:
self.insert_pair(buffer, char_str, wrap_block)
return True
self.wrap_selection(buffer, char_str, wrap_block, selection)
return True
def insert_pair(
self, buffer, char_str: str, wrap_block: tuple
):
buffer.begin_user_action()
left_block, right_block = wrap_block
insert_mark = buffer.get_insert()
insert_itr = buffer.get_iter_at_mark(insert_mark)
buffer.insert(insert_itr, f"{left_block}{right_block}")
insert_itr = buffer.get_iter_at_mark( insert_mark )
insert_itr.backward_char()
buffer.place_cursor(insert_itr)
buffer.end_user_action()
def wrap_selection(
self, buffer, char_str: str, wrap_block: tuple, selection
):
left_block, \
right_block = wrap_block
start_itr, \
end_itr = selection
data = buffer.get_text(
start_itr, end_itr, include_hidden_chars = False
)
start_mark = buffer.create_mark("startclose", start_itr, False)
end_mark = buffer.create_mark("endclose", end_itr, True)
buffer.begin_user_action()
buffer.insert(start_itr, left_block)
end_itr = buffer.get_iter_at_mark(end_mark)
buffer.insert(end_itr, right_block)
start = buffer.get_iter_at_mark(start_mark)
end = buffer.get_iter_at_mark(end_mark)
buffer.select_range(start, end)
buffer.delete_mark_by_name("startclose")
buffer.delete_mark_by_name("endclose")
buffer.end_user_action()
def get_wrap_block(self, char_str) -> tuple:
left_block = ""
right_block = ""
match char_str:
case "(" | ")":
left_block = "("
right_block = ")"
case "[" | "]":
left_block = "["
right_block = "]"
case "{" | "}":
left_block = "{"
right_block = "}"
case '"':
left_block = '"'
right_block = '"'
case "'":
left_block = "'"
right_block = "'"
case "`":
left_block = "`"
right_block = "`"
case _:
return ()
return left_block, right_block

View File

@@ -0,0 +1,7 @@
{
"name": "Autopairs",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,55 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .autopairs import Autopairs
autopairs = Autopairs()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
event = Event_Factory.create_event("register_command",
command_name = "autopairs",
command = Handler,
binding_mode = "held",
binding = [
"'", "`", "[", "]",
'<Shift>"',
'<Shift>(',
'<Shift>)',
'<Shift>{',
'<Shift>}'
]
)
self.message_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: Autopairs")
autopairs.handle_word_wrap(view.get_buffer(), char_str)

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

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

View File

@@ -10,15 +10,17 @@ from gi.repository import Pango
class MiniViewWidget(Map): class CodeMiniMap(Map):
def __init__(self): def __init__(self):
super(MiniViewWidget, self).__init__() super(CodeMiniMap, self).__init__()
self._setup_styling() self._setup_styling()
self._setup_signals() self._setup_signals()
self._subscribe_to_events() self._subscribe_to_events()
self._load_widgets() self._load_widgets()
self.show()
def _setup_styling(self): def _setup_styling(self):
ctx = self.get_style_context() ctx = self.get_style_context()

View File

@@ -0,0 +1,7 @@
{
"name": "Code MiniMap",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,32 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .code_minimap import CodeMiniMap
code_minimap = CodeMiniMap()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.FocusedViewEvent):
code_minimap.set_smini_view(event.view)
def load(self):
editors_container = self.requests_ui_element("editors-container")
editors_container.add( code_minimap )
def run(self):
...

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

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

View File

@@ -33,11 +33,7 @@ class InfoBarWidget(Gtk.Box):
... ...
def _subscribe_to_events(self): def _subscribe_to_events(self):
event_system.subscribe("set-info-labels", self._set_info_labels) ...
event_system.subscribe("set-path-label", self._set_path_label)
event_system.subscribe("set-encoding-label", self._set_encoding_label)
event_system.subscribe("set-line-char-label", self._set_line_char_label)
event_system.subscribe("set-file-type-label", self._set_file_type_label)
def _load_widgets(self): def _load_widgets(self):
@@ -63,9 +59,9 @@ class InfoBarWidget(Gtk.Box):
def _set_info_labels( def _set_info_labels(
self, self,
path: Gio.File or str = None, path: Gio.File or str = None,
line_char: str = None, line_char: str = None,
file_type: str = None, file_type: str = None,
encoding_type: str = None encoding_type: str = None
): ):
self._set_path_label(path) self._set_path_label(path)
self._set_line_char_label(line_char) self._set_line_char_label(line_char)

View File

@@ -0,0 +1,7 @@
{
"name": "Info Bar",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,32 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .info_bar_widget import InfoBarWidget
info_bar_widget = InfoBarWidget()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.SetInfoLabelsEvent):
info_bar_widget._set_info_labels(*event.info)
def load(self):
header = self.requests_ui_element("header-container")
header.add( info_bar_widget )
def run(self):
...

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

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

View File

@@ -0,0 +1,7 @@
{
"name": "Prettify JSON",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,55 @@
# Python imports
import json
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
def _load_prettify_json(buffer, menu):
menu.append( Gtk.SeparatorMenuItem() )
def on_prettify_json(menuitem, buffer):
start_itr, \
end_itr = buffer.get_start_iter(), buffer.get_end_iter()
data = buffer.get_text(start_itr, end_itr, False)
text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4)
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
item = Gtk.MenuItem(label = "Prettify JSON")
item.connect("activate", on_prettify_json, buffer)
menu.append(item)
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
language = event.buffer.get_language()
if not language: return
if language.get_id() == "json":
_load_prettify_json(event.buffer, event.menu)
def load(self):
...
def run(self):
...

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

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

View File

@@ -0,0 +1,7 @@
{
"name": "Tabs Bar",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,32 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .tabs_controller import TabsController
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
tabs_controller = TabsController()
code_container = self.requests_ui_element("code-container")
self.register_controller("tabs", tabs_controller)
code_container.add( tabs_controller.tabs_widget )
code_container.reorder_child(tabs_controller.tabs_widget, 0)
def run(self):
...

View File

@@ -9,10 +9,11 @@ from gi.repository import Gtk
from libs.controllers.controller_base import ControllerBase from libs.controllers.controller_base import ControllerBase
from libs.event_factory import Event_Factory, Code_Event_Types from libs.event_factory import Event_Factory, Code_Event_Types
from ..tabs_widget import TabsWidget from core.widgets.code.source_view import SourceView
from ..tab_widget import TabWidget
from .tabs_widget import TabsWidget
from .tab_widget import TabWidget
from ..source_view import SourceView

View File

@@ -23,6 +23,8 @@ class TabsWidget(Gtk.Notebook):
self._subscribe_to_events() self._subscribe_to_events()
self._load_widgets() self._load_widgets()
self.show()
def _setup_styling(self): def _setup_styling(self):
self.set_scrollable(True) self.set_scrollable(True)
@@ -116,6 +118,8 @@ class TabsWidget(Gtk.Notebook):
self.handler_unblock(self.switch_page_id) self.handler_unblock(self.switch_page_id)
break
def modified_changed(self, buffer): def modified_changed(self, buffer):
for page_widget in self.get_children(): for page_widget in self.get_children():
tab = self.get_tab_label(page_widget) tab = self.get_tab_label(page_widget)
@@ -128,12 +132,16 @@ class TabsWidget(Gtk.Notebook):
else: else:
ctx.remove_class("file-changed") ctx.remove_class("file-changed")
break
def externally_deleted(self, buffer): def externally_deleted(self, buffer):
for page_widget in self.get_children(): for page_widget in self.get_children():
tab = self.get_tab_label(page_widget) tab = self.get_tab_label(page_widget)
if not buffer == tab.file.buffer: continue if not buffer == tab.file.buffer: continue
ctx = tab.label.get_style_context() ctx = tab.label.get_style_context()
ctx.add_class("file-deleted") ctx.add_class("file-deleted")
break
def close_item(self, menu_item, page_widget): def close_item(self, menu_item, page_widget):
tab = self.get_tab_label(page_widget) tab = self.get_tab_label(page_widget)

View File

@@ -0,0 +1,3 @@
"""
Plugin Module
"""

View File

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

View File

@@ -0,0 +1,7 @@
{
"name": "Toggle Source View",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,54 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
event = Event_Factory.create_event("register_command",
command_name = "toggle_source_view",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>h"
)
self.message_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
char_str: str,
*args,
**kwargs
):
logger.debug("Command: Toggle Source View")
target = view.get_parent()
target.hide() if target.is_visible() else target.show()
if view.sibling_left:
target = view.sibling_left.get_parent()
target.show()
view.sibling_left.grab_focus()
if view.sibling_right:
target = view.sibling_right.get_parent()
target.show()
view.sibling_right.grab_focus()

View File

@@ -2,6 +2,7 @@
from contextlib import suppress from contextlib import suppress
import signal import signal
import os import os
import json
# Lib imports # Lib imports
@@ -44,25 +45,25 @@ class Application:
return True return True
logger.warning(f"{APP_NAME} IPC Server Exists: Have sent path(s) to it and closing...") logger.warning(f"{APP_NAME} IPC Server Exists: Have sent path(s) to it and closing...")
files: list = []
for arg in unknownargs + [args.new_tab,]: for arg in unknownargs + [args.new_tab,]:
if os.path.isfile(arg): if os.path.isfile(arg):
message = f"FILE|{arg}" files.append(f"file://{arg}")
ipc_server.send_ipc_message(message)
if os.path.isdir(arg): if os.path.isdir(arg):
message = f"DIR|{arg}" message = f"DIR|{arg}"
ipc_server.send_ipc_message(message) ipc_server.send_ipc_message(message)
if files:
message = f"FILES|{json.dumps(files)}"
ipc_server.send_ipc_message(message)
return False return False
def ipc_realization_check(self, ipc_server): def ipc_realization_check(self, ipc_server):
try: try:
ipc_server.create_ipc_listener() ipc_server.create_ipc_listener()
except (OSError, PermissionError) as e: except Exception:
logger.info(f"IPC listener creation failed: {e}, falling back to test message")
ipc_server.send_test_ipc_message()
except Exception as e:
logger.error(f"Unexpected IPC setup error: {e}")
ipc_server.send_test_ipc_message() ipc_server.send_test_ipc_message()
def setup_debug_hook(self): def setup_debug_hook(self):

View File

@@ -29,6 +29,7 @@ class BaseContainer(Gtk.Box):
self.ctx.add_class("base-container") self.ctx.add_class("base-container")
self.set_orientation(Gtk.Orientation.VERTICAL) self.set_orientation(Gtk.Orientation.VERTICAL)
self._update_transparency()
def _setup_signals(self): def _setup_signals(self):
... ...

View File

@@ -9,7 +9,6 @@ from gi.repository import Gtk
from ...widgets.code.code_base import CodeBase from ...widgets.code.code_base import CodeBase
from ...widgets.separator_widget import Separator from ...widgets.separator_widget import Separator
from ...widgets.code.mini_view_widget import MiniViewWidget
from .editors_container import EditorsContainer from .editors_container import EditorsContainer
@@ -37,20 +36,19 @@ class CodeContainer(Gtk.Box):
... ...
def _load_widgets(self): def _load_widgets(self):
widget_registery.expose_object("code-container", self)
code_base = CodeBase() code_base = CodeBase()
self.add( self._create_tabs_widgets(code_base) )
self.add( self._create_editor_widget(code_base) ) self.add( self._create_editor_widget(code_base) )
def _create_tabs_widgets(self, code_base: CodeBase):
return code_base.get_tabs_widget()
def _create_editor_widget(self, code_base: CodeBase): def _create_editor_widget(self, code_base: CodeBase):
editors_container = Gtk.Box() editors_container = Gtk.Box()
widget_registery.expose_object("editors-container", editors_container)
editors_container.add( Separator("separator_left") ) editors_container.add( Separator("separator_left") )
editors_container.add( EditorsContainer(code_base) ) editors_container.add( EditorsContainer(code_base) )
editors_container.add( Separator("separator_right") ) editors_container.add( Separator("separator_right") )
editors_container.add( code_base.get_mini_view_widget() )
return editors_container return editors_container

View File

@@ -26,7 +26,6 @@ class EditorsContainer(Gtk.Paned):
self.ctx = self.get_style_context() self.ctx = self.get_style_context()
self.ctx.add_class("paned-editors-container") self.ctx.add_class("paned-editors-container")
self.set_size_request(-1, 300)
self.set_hexpand(True) self.set_hexpand(True)
self.set_vexpand(True) self.set_vexpand(True)
self.set_wide_handle(True) self.set_wide_handle(True)

View File

@@ -7,7 +7,6 @@ from gi.repository import Gtk
# Application imports # Application imports
from ..widgets.separator_widget import Separator from ..widgets.separator_widget import Separator
from .code.code_container import CodeContainer
@@ -39,6 +38,4 @@ class FooterContainer(Gtk.Box):
def _load_widgets(self): def _load_widgets(self):
widget_registery.expose_object("footer-container", self) widget_registery.expose_object("footer-container", self)
self.add( CodeContainer() )
self.add( Separator("separator-footer", 0) ) self.add( Separator("separator-footer", 0) )

View File

@@ -7,6 +7,7 @@ from gi.repository import Gtk
# Application imports # Application imports
from ..widgets.separator_widget import Separator from ..widgets.separator_widget import Separator
from .code.code_container import CodeContainer
@@ -39,3 +40,4 @@ class LeftContainer(Gtk.Box):
widget_registery.expose_object("left-container", self) widget_registery.expose_object("left-container", self)
self.add( Separator("separator-left", 1) ) self.add( Separator("separator-left", 1) )
self.add( CodeContainer() )

View File

@@ -59,6 +59,7 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerMixin)
def _subscribe_to_events(self): def _subscribe_to_events(self):
event_system.subscribe("shutting-down", lambda: print("Shutting down...")) 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-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("handle-dir-from-ipc", self.handle_dir_from_ipc)
event_system.subscribe("tggl-top-main-menubar", self._tggl_top_main_menubar) event_system.subscribe("tggl-top-main-menubar", self._tggl_top_main_menubar)

View File

@@ -8,13 +8,10 @@ from plugins import plugins_controller
from libs.controllers.controller_manager import ControllerManager from libs.controllers.controller_manager import ControllerManager
from .controllers.files_controller import FilesController from .controllers.files_controller import FilesController
from .controllers.tabs_controller import TabsController
from .controllers.commands_controller import CommandsController from .controllers.commands_controller import CommandsController
from .controllers.completion_controller import CompletionController from .controllers.completion_controller import CompletionController
from .controllers.views.source_views_controller import SourceViewsController from .controllers.views.source_views_controller import SourceViewsController
from .mini_view_widget import MiniViewWidget
class CodeBase: class CodeBase:
@@ -22,7 +19,6 @@ class CodeBase:
super(CodeBase, self).__init__() super(CodeBase, self).__init__()
self.controller_manager: ControllerManager = ControllerManager() self.controller_manager: ControllerManager = ControllerManager()
self.miniview_widget: MiniViewWidget = MiniViewWidget()
self._subscribe_to_events() self._subscribe_to_events()
self._load_controllers() self._load_controllers()
@@ -30,29 +26,22 @@ class CodeBase:
def _subscribe_to_events(self): def _subscribe_to_events(self):
event_system.subscribe("handle-file", self._load_ipc_file) event_system.subscribe("handle-file", self._load_ipc_file)
event_system.subscribe("handle-files", self._load_ipc_files)
def _load_controllers(self): def _load_controllers(self):
files_controller = FilesController() files_controller = FilesController()
tabs_controller = TabsController()
commands_controller = CommandsController() commands_controller = CommandsController()
completion_controller = CompletionController() completion_controller = CompletionController()
source_views_controller = SourceViewsController() source_views_controller = SourceViewsController()
# self.controller_manager.register_controller("base", self) # self.controller_manager.register_controller("base", self)
self.controller_manager.register_controller("files", files_controller) self.controller_manager.register_controller("files", files_controller)
self.controller_manager.register_controller("tabs", tabs_controller)
self.controller_manager.register_controller("commands", commands_controller) self.controller_manager.register_controller("commands", commands_controller)
self.controller_manager.register_controller("completion", completion_controller) self.controller_manager.register_controller("completion", completion_controller)
self.controller_manager.register_controller("source_views", source_views_controller) self.controller_manager.register_controller("source_views", source_views_controller)
self.controller_manager.register_controller("plugins", plugins_controller) self.controller_manager.register_controller("plugins", plugins_controller)
self.controller_manager.register_controller("widgets", widget_registery) self.controller_manager.register_controller("widgets", widget_registery)
def get_tabs_widget(self):
return self.controller_manager["tabs"].get_tabs_widget()
def get_mini_view_widget(self):
return self.miniview_widget
def create_source_view(self): def create_source_view(self):
source_view = self.controller_manager["source_views"].create_source_view() source_view = self.controller_manager["source_views"].create_source_view()
self.controller_manager["completion"].register_completer( self.controller_manager["completion"].register_completer(
@@ -68,3 +57,7 @@ class CodeBase:
active_view = self.controller_manager["source_views"].signal_mapper.active_view active_view = self.controller_manager["source_views"].signal_mapper.active_view
uris = [ f"file://{fpath}" ] uris = [ f"file://{fpath}" ]
active_view._on_uri_data_received(uris) active_view._on_uri_data_received(uris)
def _load_ipc_files(self, uris: list):
active_view = self.controller_manager["source_views"].signal_mapper.active_view
active_view._on_uri_data_received(uris)

View File

@@ -47,6 +47,14 @@ class CommandSystem:
... ...
def set_info_labels(self, data: tuple[str]):
event = Event_Factory.create_event(
"set_info_labels",
info = data
)
self.emit_to("plugins", event)
def get_file(self, view: SourceView): def get_file(self, view: SourceView):
event = Event_Factory.create_event( event = Event_Factory.create_event(
"get_file", "get_file",
@@ -78,7 +86,7 @@ class CommandSystem:
def remove_file(self, view: SourceView): def remove_file(self, view: SourceView):
event = Event_Factory.create_event( event = Event_Factory.create_event(
"removed_file", "remove_file",
view = view, view = view,
buffer = view.get_buffer() buffer = view.get_buffer()
) )

View File

@@ -18,4 +18,5 @@ def execute(
): ):
logger.debug("Command: Focus Left Sibling") logger.debug("Command: Focus Left Sibling")
if not view.sibling_left: return if not view.sibling_left: return
view.sibling_left.get_parent().show()
view.sibling_left.grab_focus() view.sibling_left.grab_focus()

View File

@@ -18,4 +18,5 @@ def execute(
): ):
logger.debug("Command: Focus Right Sibling") logger.debug("Command: Focus Right Sibling")
if not view.sibling_right: return if not view.sibling_right: return
view.sibling_right.get_parent().show()
view.sibling_right.grab_focus() view.sibling_right.grab_focus()

View File

@@ -23,6 +23,7 @@ def execute(
popped_file, next_file = view.command.get_swap_file(view) popped_file, next_file = view.command.get_swap_file(view)
view.sibling_left.set_buffer(buffer) view.sibling_left.set_buffer(buffer)
view.sibling_left.get_parent().show()
view.sibling_left.grab_focus() view.sibling_left.grab_focus()
if next_file: if next_file:

View File

@@ -23,6 +23,7 @@ def execute(
popped_file, next_file = view.command.get_swap_file(view) popped_file, next_file = view.command.get_swap_file(view)
view.sibling_right.set_buffer(buffer) view.sibling_right.set_buffer(buffer)
view.sibling_right.get_parent().show()
view.sibling_right.grab_focus() view.sibling_right.grab_focus()
if next_file: if next_file:

View File

@@ -18,10 +18,14 @@ def execute(
**kwargs **kwargs
): ):
logger.debug("Command: Open File(s)") logger.debug("Command: Open File(s)")
gfiles = event_system.emit_and_await("open-files") file = view.command.get_file(view)
start_path = None
if not file.ftype == "buffer":
start_path = file.get_location()
gfiles = event_system.emit_and_await("open-files", (None, None, start_path))
if not gfiles: return if not gfiles: return
file = view.command.get_file(view)
if file.ftype == "buffer": if file.ftype == "buffer":
gfile = gfiles.pop() gfile = gfiles.pop()
view.command.exec_with_args("load_file", view, gfile, file) view.command.exec_with_args("load_file", view, gfile, file)

View File

@@ -27,7 +27,6 @@ def execute(
column = iter.get_line_offset() column = iter.get_line_offset()
ftype = file.ftype.get_id() if hasattr(file.ftype, "get_id") else file.ftype ftype = file.ftype.get_id() if hasattr(file.ftype, "get_id") else file.ftype
event_system.emit( view.command.set_info_labels(
"set-info-labels",
(file.fpath, f"{line}:{column}", ftype, file.encoding) (file.fpath, f"{line}:{column}", ftype, file.encoding)
) )

View File

@@ -20,8 +20,6 @@ class FilesController(ControllerBase, list):
def _controller_message(self, event: Code_Event_Types.CodeEvent): def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.AddNewFileEvent): if isinstance(event, Code_Event_Types.AddNewFileEvent):
self.new_file(event) self.new_file(event)
elif isinstance(event, Code_Event_Types.SwapFileEvent):
self.swap_file(event)
elif isinstance(event, Code_Event_Types.PopFileEvent): elif isinstance(event, Code_Event_Types.PopFileEvent):
self.pop_file(event) self.pop_file(event)
elif isinstance(event, Code_Event_Types.RemoveFileEvent): elif isinstance(event, Code_Event_Types.RemoveFileEvent):
@@ -31,29 +29,6 @@ class FilesController(ControllerBase, list):
elif isinstance(event, Code_Event_Types.GetSwapFileEvent): elif isinstance(event, Code_Event_Types.GetSwapFileEvent):
self.get_swap_file(event) self.get_swap_file(event)
def get_file(self, event: Code_Event_Types.GetFileEvent):
if not event.buffer: return
for file in self:
if not event.buffer == file.buffer: continue
event.response = file
return file
def get_swap_file(self, event: Code_Event_Types.GetSwapFileEvent):
if not event.buffer: return
for i, file in enumerate(self):
if not event.buffer == file.buffer: continue
j = self.next_index(i)
next_file = self[j]
swapped_file = self[j] if not j == -1 else None
event.response = [swapped_file, next_file]
return swapped_file, next_file
def new_file(self, event: Code_Event_Types.AddNewFileEvent): def new_file(self, event: Code_Event_Types.AddNewFileEvent):
file = SourceFile() file = SourceFile()
@@ -73,20 +48,6 @@ class FilesController(ControllerBase, list):
return file return file
def swap_file(self, event: Code_Event_Types.GetSwapFileEvent):
if not event.buffer: return
for i, file in enumerate(self):
if not event.buffer == file.buffer: continue
j = self.next_index(i)
next_file = self[j]
swapped_file = self[j] if not j == -1 else None
event.response = [swapped_file, next_file]
return swapped_file, next_file
def pop_file(self, event: Code_Event_Types.PopFileEvent): def pop_file(self, event: Code_Event_Types.PopFileEvent):
if not event.buffer: return if not event.buffer: return
@@ -134,6 +95,30 @@ class FilesController(ControllerBase, list):
return next_file return next_file
def get_file(self, event: Code_Event_Types.GetFileEvent):
if not event.buffer: return
for file in self:
if not event.buffer == file.buffer: continue
event.response = file
return file
def get_swap_file(self, event: Code_Event_Types.GetSwapFileEvent):
if not event.buffer: return
for i, file in enumerate(self):
if not event.buffer == file.buffer: continue
j = self.next_index(i)
next_file = self[j]
swapped_file = self[j] if not j == -1 else None
event.response = [swapped_file, next_file]
return swapped_file, next_file
def next_index(self, i): def next_index(self, i):
size = len(self) size = len(self)

View File

@@ -0,0 +1,137 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from ...mixins.mark_support_mixin import MarkSupportMixin
class MarkerManager(MarkSupportMixin):
def __init__(self):
super().__init__()
self.buffer_markers: dict = {}
self.selection_tag: Gtk.TextTag = Gtk.TextTag.new("selection")
self.selection_tag.props.background = "rgba(111, 168, 220, 0.64)"
self.selection_tag.props.foreground = "#ffffff"
def move_by_char(self, buffer, is_forward: bool, is_selection: bool):
self._move(buffer, is_forward, is_selection, mode = "char")
def move_by_word(self, buffer, is_forward: bool, is_selection: bool):
self._move(buffer, is_forward, is_selection, mode = "word")
def move_by_line(self, buffer, is_forward: bool, is_selection: bool):
self._move(buffer, is_forward, is_selection, mode = "line")
def _move(self, buffer, is_forward: bool, is_selection: bool, mode: str):
self.clear_highlight(buffer)
self.insert_selection_tag(buffer)
for mark_hash in self.buffer_markers:
marker = self.buffer_markers[mark_hash]
start_mark = marker["start_mark"]
end_mark = marker["end_mark"]
has_selection = marker["is_selection"]
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
if is_selection:
self.buffer_markers[mark_hash]["is_selection"] = True
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(end_mark, end_itr)
self._apply_selection(buffer, start_itr, end_itr)
continue
if has_selection:
self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward)
continue
# No selection — move both anchor and caret together
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(start_mark, end_itr)
buffer.move_mark(end_mark, end_itr)
def collapse_selection(self,
buffer, mark_hash, start_mark, end_mark, is_forward: bool
):
self.buffer_markers[mark_hash]["is_selection"] = False
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
# Determine which side is visually the caret
if start_itr.compare(end_itr) <= 0:
left = start_itr
right = end_itr
else:
left = end_itr
right = start_itr
# If moving forward → collapse to right edge
collapse_itr = right if is_forward else left
buffer.move_mark(start_mark, collapse_itr)
buffer.move_mark(end_mark, collapse_itr)
def _move_iter(self, buffer, itr_, mode: str, is_forward: bool):
if mode == "char":
itr_.forward_char() if is_forward else itr_.backward_char()
elif mode == "word":
itr_.forward_word_end() if is_forward else itr_.backward_word_start()
elif mode == "line":
line = itr_.get_line()
offset = itr_.get_line_offset()
max_line = buffer.get_line_count() - 1
new_line = line + 1 if is_forward else line - 1
new_line = max(0, min(max_line, new_line))
itr_.set_line(new_line)
self.move_to_offset(offset, itr_)
def _apply_selection(self, buffer, start_itr, end_itr):
if start_itr.compare(end_itr) <= 0:
buffer.apply_tag(self.selection_tag, start_itr, end_itr)
else:
buffer.apply_tag(self.selection_tag, end_itr, start_itr)
def button_release_event(self, source_view, event):
buffer = source_view.get_buffer()
coords = source_view.window_to_buffer_coords(
Gtk.TextWindowType.TEXT,
event.x,
event.y,
)
is_over_text, target_itr, _ = source_view.get_iter_at_position(
coords.buffer_x,
coords.buffer_y,
)
if not is_over_text:
target_itr.forward_visible_line()
target_itr.backward_char()
if self.remove_mark_set(target_itr, buffer):
return
self.insert_mark_set(target_itr, buffer)
def key_press_event(self, source_view, event, key_mapper):
...

View File

@@ -42,7 +42,8 @@ class SourceViewSignalMapper:
"key-press-event": self._key_press_event, "key-press-event": self._key_press_event,
"key-release-event": self._key_release_event, "key-release-event": self._key_release_event,
"button-press-event": self._button_press_event, "button-press-event": self._button_press_event,
"button-release-event": self._button_release_event "button-release-event": self._button_release_event,
"populate-popup": self._populate_popup
} }
def _focus_in_event(self, source_view: SourceView, eve): def _focus_in_event(self, source_view: SourceView, eve):
@@ -54,14 +55,17 @@ class SourceViewSignalMapper:
source_view, step, count, extend_selection, self.emit source_view, step, count, extend_selection, self.emit
) )
def _key_press_event(self, source_view: SourceView, eve):
return self.state_manager.handle_key_press_event(source_view, eve)
def _key_release_event(self, source_view: SourceView, eve):
return self.state_manager.handle_key_release_event(source_view, eve)
def _button_press_event(self, source_view: SourceView, eve): def _button_press_event(self, source_view: SourceView, eve):
return self.state_manager.handle_button_press_event(source_view, eve) return self.state_manager.handle_button_press_event(source_view, eve)
def _button_release_event(self, source_view: SourceView, eve): def _button_release_event(self, source_view: SourceView, eve):
return self.state_manager.handle_button_release_event(source_view, eve) return self.state_manager.handle_button_release_event(source_view, eve)
def _key_press_event(self, source_view: SourceView, eve): def _populate_popup(self, source_view, menu):
return self.state_manager.handle_key_press_event(source_view, eve) return self.state_manager.handle_populate_popup(source_view, menu, self.emit)
def _key_release_event(self, source_view: SourceView, eve):
return self.state_manager.handle_key_release_event(source_view, eve)

View File

@@ -87,6 +87,8 @@ class SourceViewsController(ControllerBase, list):
def first_map_load(self): def first_map_load(self):
for source_view in self: for source_view in self:
source_view.command.exec("new_file") source_view.command.exec("new_file")
if not source_view.sibling_left: continue
source_view.get_parent().hide()
source_view = self[0] source_view = self[0]
source_view.grab_focus() source_view.grab_focus()

View File

@@ -34,15 +34,6 @@ class SourceViewStateManager:
source_view, step, count, extend_selection, emit source_view, step, count, extend_selection, emit
) )
def handle_button_press_event(self, source_view, eve):
return self.states[source_view.state].button_press_event(source_view, eve)
def handle_button_release_event(self, source_view, eve):
# Handle state transitions (multi-insert toggling)
self._handle_multi_insert_toggle(source_view, eve)
return self.states[source_view.state].button_release_event(source_view, eve)
def handle_key_press_event(self, source_view, eve): def handle_key_press_event(self, source_view, eve):
return self.states[source_view.state].key_press_event( return self.states[source_view.state].key_press_event(
source_view, eve, self.key_mapper source_view, eve, self.key_mapper
@@ -53,6 +44,17 @@ class SourceViewStateManager:
source_view, eve, self.key_mapper source_view, eve, self.key_mapper
) )
def handle_button_press_event(self, source_view, eve):
self._handle_multi_insert_toggle(source_view, eve)
return self.states[source_view.state].button_press_event(source_view, eve)
def handle_button_release_event(self, source_view, eve):
return self.states[source_view.state].button_release_event(source_view, eve)
def handle_populate_popup(self, source_view, menu, emit):
return self.states[source_view.state].populate_popup(source_view, menu, emit)
def _handle_multi_insert_toggle(self, source_view, eve): def _handle_multi_insert_toggle(self, source_view, eve):
is_control = self.key_mapper.is_control(eve) is_control = self.key_mapper.is_control(eve)
if is_control and not source_view.state == SourceViewStates.MULTIINSERT: if is_control and not source_view.state == SourceViewStates.MULTIINSERT:
@@ -61,6 +63,7 @@ class SourceViewStateManager:
if not is_control and source_view.state == SourceViewStates.MULTIINSERT: if not is_control and source_view.state == SourceViewStates.MULTIINSERT:
logger.debug("Entered Regular Insert Mode...") logger.debug("Entered Regular Insert Mode...")
self.states[source_view.state].clear_markers(source_view) self.states[source_view.state].marker_manager.clear_mark_sets(source_view)
source_view.state = SourceViewStates.INSERT source_view.state = SourceViewStates.INSERT

View File

@@ -0,0 +1,92 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from libs.dto.states import SourceViewStates
class SourceViewsBaseState:
def __init__(self):
super(SourceViewsBaseState, self).__init__()
def focus_in_event(self, source_view, eve, emit):
source_view.command.exec("set_miniview")
source_view.command.exec("set_focus_border")
source_view.command.exec("update_info_bar")
event = Event_Factory.create_event("focused_view", view = source_view)
emit(event)
def insert_text(self, file, text: str):
return True
def move_cursor(self, source_view, step, count, extend_selection, emit):
buffer = source_view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() )
line = itr.get_line()
char = itr.get_line_offset()
event = Event_Factory.create_event(
"cursor_moved",
view = source_view,
buffer = buffer,
line = line,
char = char
)
emit(event)
source_view.command.exec("update_info_bar")
def button_press_event(self, source_view, eve):
source_view.command.exec("update_info_bar")
def button_release_event(self, source_view, eve):
source_view.command.exec("update_info_bar")
def key_press_event(self, source_view, eve, key_mapper):
command = key_mapper._key_press_event(eve)
is_future = key_mapper._key_release_event(eve)
char_str = key_mapper.get_char(eve)
modkeys_states = key_mapper.get_modkeys_states(eve)
if is_future: return True
if not command: return False
response = source_view.command.exec_with_args(
command, source_view, char_str, modkeys_states
)
return True if not response else response
def key_release_event(self, source_view, eve, key_mapper):
command = key_mapper._key_release_event(eve)
is_past = key_mapper._key_press_event(eve)
char_str = key_mapper.get_char(eve)
modkeys_states = key_mapper.get_modkeys_states(eve)
if is_past: return True
if not command: return False
response = source_view.command.exec_with_args(
command, source_view, char_str, modkeys_states
)
return True if not response else response
def populate_popup(self, source_view, menu, emit):
buffer = source_view.get_buffer()
event = Event_Factory.create_event(
"populate_source_view_popup",
buffer = buffer,
menu = menu
)
emit(event)
menu.show_all()

View File

@@ -7,9 +7,11 @@ from libs.event_factory import Event_Factory, Code_Event_Types
from libs.dto.states import SourceViewStates from libs.dto.states import SourceViewStates
from .source_view_base_state import SourceViewsBaseState
class SourceViewsCommandState:
class SourceViewsCommandState(SourceViewsBaseState):
def __init__(self): def __init__(self):
super(SourceViewsCommandState, self).__init__() super(SourceViewsCommandState, self).__init__()
@@ -17,10 +19,10 @@ class SourceViewsCommandState:
def focus_in_event(self, source_view, eve, emit): def focus_in_event(self, source_view, eve, emit):
return True return True
def move_cursor(self, source_view, step, count, extend_selection, emit): def insert_text(self, file, text):
return True return True
def insert_text(self, file, text): def move_cursor(self, source_view, step, count, extend_selection, emit):
return True return True
def button_press_event(self, source_view, eve): def button_press_event(self, source_view, eve):

View File

@@ -7,68 +7,10 @@ from libs.event_factory import Event_Factory, Code_Event_Types
from libs.dto.states import SourceViewStates from libs.dto.states import SourceViewStates
from .source_view_base_state import SourceViewsBaseState
class SourceViewsInsertState:
class SourceViewsInsertState(SourceViewsBaseState):
def __init__(self): def __init__(self):
super(SourceViewsInsertState, self).__init__() super(SourceViewsInsertState, self).__init__()
def focus_in_event(self, source_view, eve, emit):
source_view.command.exec("set_miniview")
source_view.command.exec("set_focus_border")
source_view.command.exec("update_info_bar")
event = Event_Factory.create_event("focused_view", view = source_view)
emit(event)
def insert_text(self, file, text: str):
return True
def move_cursor(self, source_view, step, count, extend_selection, emit):
buffer = source_view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() )
line = itr.get_line()
char = itr.get_line_offset()
event = Event_Factory.create_event(
"cursor_moved",
view = source_view,
buffer = buffer,
line = line,
char = char
)
emit(event)
source_view.command.exec("update_info_bar")
def button_press_event(self, source_view, eve):
source_view.command.exec("update_info_bar")
def button_release_event(self, source_view, eve):
source_view.command.exec("update_info_bar")
def key_press_event(self, source_view, eve, key_mapper):
command = key_mapper._key_press_event(eve)
is_future = key_mapper._key_release_event(eve)
char_str = key_mapper.get_char(eve)
if is_future: return True
if not command: return False
source_view.command.exec_with_args(command, source_view, char_str)
return True
def key_release_event(self, source_view, eve, key_mapper):
command = key_mapper._key_release_event(eve)
is_past = key_mapper._key_press_event(eve)
char_str = key_mapper.get_char(eve)
if is_past: return True
if not command: return False
source_view.command.exec_with_args(command, source_view, char_str)
return True

View File

@@ -2,153 +2,108 @@
# Lib imports # Lib imports
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version("Gtk", "3.0")
from gi.repository import Gtk from gi.repository import Gtk
# Application imports # Application imports
from libs.event_factory import Event_Factory, Code_Event_Types from libs.event_factory import Event_Factory
from libs.dto.states import SourceViewStates, MoveDirection, CursorAction from libs.dto.states import CursorAction
from ....mixins.source_mark_events_mixin import MarkEventsMixin from ..marker_manager import MarkerManager
from .source_view_base_state import SourceViewsBaseState
class SourceViewsMultiInsertState(MarkEventsMixin): class SourceViewsMultiInsertState(SourceViewsBaseState):
def __init__(self): def __init__(self):
super(SourceViewsMultiInsertState, self).__init__() super(SourceViewsMultiInsertState, self).__init__()
self.cursor_action: CursorAction = 0 self.cursor_action: CursorAction = None
self.move_direction: MoveDirection = 0 self.marker_manager: MarkerManager = MarkerManager()
self.insert_markers: list = []
def focus_in_event(self, source_view, eve, emit): def insert_text(self, file, text: str) -> bool:
source_view.command.exec("set_miniview") if not self.marker_manager.buffer_markers: return False
source_view.command.exec("set_focus_border")
source_view.command.exec("update_info_bar")
event = Event_Factory.create_event("focused_view", view = source_view)
emit(event)
def insert_text(self, file, text):
if not self.insert_markers: return False
buffer = file.buffer buffer = file.buffer
if buffer.is_processing_completion: if buffer.is_processing_completion:
self.insert_completion_text(buffer, text) return self._insert_completion_text(buffer, text)
return True
# freeze buffer and insert to each mark (if any) def insert_text(start_itr, end_itr = None):
buffer.block_insert_after_signal() if not end_itr:
buffer.begin_user_action() buffer.insert(start_itr, text, -1)
return
with buffer.freeze_notify(): buffer.delete(start_itr, end_itr)
for mark in self.insert_markers: buffer.insert(start_itr, text, -1)
itr = buffer.get_iter_at_mark(mark)
buffer.insert(itr, text, -1)
buffer.end_user_action()
buffer.unblock_insert_after_signal()
self.marker_manager.apply_to_marks(buffer, insert_text)
return True return True
def insert_completion_text(self, buffer, text): def _insert_completion_text(self, buffer, text: str) -> bool:
buffer.is_processing_completion = False buffer.is_processing_completion = False
# freeze buffer and insert to each mark (if any) def replace_word(start_itr, end_itr = None):
buffer.block_insert_after_signal() if not end_itr:
buffer.begin_user_action() end_itr = start_itr.copy()
with buffer.freeze_notify(): if not start_itr.starts_word():
for mark in self.insert_markers: start_itr.backward_word_start()
end_itr = buffer.get_iter_at_mark(mark)
start_itr = end_itr.copy()
if not start_itr.starts_word(): if not end_itr.ends_word():
start_itr.backward_word_start() end_itr.forward_word_end()
if not end_itr.ends_word(): buffer.delete(start_itr, end_itr)
end_itr.forward_word_end() buffer.insert(start_itr, text, -1)
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text, -1)
buffer.end_user_action()
buffer.unblock_insert_after_signal()
self.marker_manager.apply_to_marks(buffer, replace_word)
return True return True
def move_cursor(self, source_view, step, count, extend_selection, emit):
buffer = source_view.get_buffer()
self._process_move_direction(buffer) def move_cursor(self, source_view, step, count, is_selection, emit):
is_forward = count > 0
buffer = source_view.get_buffer()
if step in [
Gtk.MovementStep.LOGICAL_POSITIONS,
Gtk.MovementStep.VISUAL_POSITIONS
]:
self.marker_manager.move_by_char(buffer, is_forward, is_selection)
elif step == Gtk.MovementStep.WORDS:
self.marker_manager.move_by_word(buffer, is_forward, is_selection)
elif step == Gtk.MovementStep.DISPLAY_LINES:
self.marker_manager.move_by_line(buffer, is_forward, is_selection)
self._signal_cursor_moved(source_view, emit) self._signal_cursor_moved(source_view, emit)
source_view.command.exec("update_info_bar")
def button_press_event(self, source_view, eve): def key_press_event(self, source_view, event, key_mapper):
source_view.command.exec("update_info_bar") char = key_mapper.get_raw_keyname(event).upper()
return True self.is_control = key_mapper.is_control(event)
self.is_shift = key_mapper.is_shift(event)
def button_release_event(self, source_view, eve):
buffer = source_view.get_buffer()
insert_iter = buffer.get_iter_at_mark( buffer.get_insert() )
data = source_view.window_to_buffer_coords(
Gtk.TextWindowType.TEXT,
eve.x,
eve.y
)
is_over_text, \
target_iter, \
is_trailing = source_view.get_iter_at_position(data.buffer_x, data.buffer_y)
if not is_over_text:
# NOTE: Trying to put at very end of line if not over text (aka, clicking right of text)
target_iter.forward_visible_line()
target_iter.backward_char()
self._insert_mark(insert_iter, target_iter, buffer)
def key_press_event(self, source_view, eve, key_mapper):
char = key_mapper.get_raw_keyname(eve)
for action in CursorAction:
if not action.name == char.upper(): continue
self.cursor_action = action.value
self._process_cursor_action(source_view.get_buffer())
if char.upper() in ["BACKSPACE", "DELETE", "ENTER"]:
self.marker_manager.process_cursor_action(
source_view.get_buffer(),
char.upper()
)
return False return False
for direction in MoveDirection: return super().key_press_event(source_view, event, key_mapper)
if not direction.name == char.upper(): continue
self.move_direction = direction.value
return False
is_future = key_mapper._key_release_event(eve)
if is_future: return True
command = key_mapper._key_press_event(eve)
if not command: return False
source_view.command.exec(command)
def button_press_event(self, source_view, event):
return True return True
def key_release_event(self, source_view, eve, key_mapper): def button_release_event(self, source_view, event):
command = key_mapper._key_release_event(eve) self.marker_manager.button_release_event(source_view, event)
is_past = key_mapper._key_press_event(eve)
if is_past: return True
if not command: return False
source_view.command.exec(command)
return True
def _signal_cursor_moved(self, source_view, emit): def _signal_cursor_moved(self, source_view, emit):
buffer = source_view.get_buffer() buffer = source_view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() ) itr = buffer.get_iter_at_mark( buffer.get_insert() )
line = itr.get_line() line = itr.get_line()
char = itr.get_line_offset() char = itr.get_line_offset()
event = Event_Factory.create_event( event = Event_Factory.create_event(
"cursor_moved", "cursor_moved",
view = source_view, view = source_view,
@@ -158,3 +113,4 @@ class SourceViewsMultiInsertState(MarkEventsMixin):
) )
emit(event) emit(event)

View File

@@ -7,9 +7,11 @@ from libs.event_factory import Event_Factory, Code_Event_Types
from libs.dto.states import SourceViewStates from libs.dto.states import SourceViewStates
from .source_view_base_state import SourceViewsBaseState
class SourceViewsReadOnlyState:
class SourceViewsReadOnlyState(SourceViewsBaseState):
def __init__(self): def __init__(self):
super(SourceViewsReadOnlyState, self).__init__() super(SourceViewsReadOnlyState, self).__init__()
@@ -17,10 +19,10 @@ class SourceViewsReadOnlyState:
def focus_in_event(self, source_view, eve, emit): def focus_in_event(self, source_view, eve, emit):
return True return True
def move_cursor(self, source_view, step, count, extend_selection, emit): def insert_text(self, file, text):
return True return True
def insert_text(self, file, text): def move_cursor(self, source_view, step, count, extend_selection, emit):
return True return True
def button_press_event(self, source_view, eve): def button_press_event(self, source_view, eve):

View File

@@ -119,14 +119,9 @@ class KeyMapper:
return self.states[self.state].released[char_str] return self.states[self.state].released[char_str]
def _set_key_state(self, eve): def _set_key_state(self, eve):
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK) is_control, \
is_control = modifiers & Gdk.ModifierType.CONTROL_MASK is_shift, \
is_shift = modifiers & Gdk.ModifierType.SHIFT_MASK is_alt = self.get_modkeys_states(eve)
try:
is_alt = modifiers & Gdk.ModifierType.ALT_MASK
except:
is_alt = modifiers & Gdk.ModifierType.MOD1_MASK
self.state = NoKeyState self.state = NoKeyState
if is_control: if is_control:
@@ -144,11 +139,23 @@ class KeyMapper:
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK) modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
return modifiers & Gdk.ModifierType.SHIFT_MASK return modifiers & Gdk.ModifierType.SHIFT_MASK
def get_raw_keyname(self, eve): def get_raw_keyname(self, eve) -> str:
return Gdk.keyval_name(eve.keyval) return Gdk.keyval_name(eve.keyval)
def get_keyname(self, eve): def get_modkeys_states(self, eve) -> tuple:
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
is_control = modifiers & Gdk.ModifierType.CONTROL_MASK
is_shift = modifiers & Gdk.ModifierType.SHIFT_MASK
try:
is_alt = modifiers & Gdk.ModifierType.ALT_MASK
except:
is_alt = modifiers & Gdk.ModifierType.MOD1_MASK
return is_control, is_shift, is_alt
def get_keyname(self, eve) -> str:
return Gdk.keyval_name(eve.keyval).lower() return Gdk.keyval_name(eve.keyval).lower()
def get_char(self, eve): def get_char(self, eve) -> str:
return chr( Gdk.keyval_to_unicode(eve.keyval) ) return chr( Gdk.keyval_to_unicode(eve.keyval) )

View File

@@ -0,0 +1,134 @@
# Python imports
import random
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class MarkSupportMixin:
def clear_mark_sets(self, source_view):
buffer = source_view.get_buffer()
self.clear_highlight(buffer)
for mark_set in self.buffer_markers.values():
start_mark, end_mark, is_selection = mark_set.values()
start_mark.set_visible(False)
buffer.delete_mark(start_mark)
buffer.delete_mark(end_mark)
self.buffer_markers.clear()
def insert_selection_tag(self, buffer):
tag_table = buffer.get_tag_table()
if not tag_table.lookup("selection"):
tag_table.add(self.selection_tag)
def clear_highlight(self, buffer):
if not self.selection_tag: return
start_itr, end_itr = buffer.get_bounds()
buffer.remove_tag(self.selection_tag, start_itr, end_itr)
def apply_to_marks(self, buffer, operation):
buffer.block_insert_after_signal()
buffer.begin_user_action()
try:
with buffer.freeze_notify():
for mark_hash in self.buffer_markers:
marker = self.buffer_markers[mark_hash]
start_mark = marker["start_mark"]
end_mark = marker["end_mark"]
has_selection = marker["is_selection"]
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
if has_selection:
operation(start_itr, end_itr)
self.collapse_selection(
buffer, mark_hash, start_mark, end_mark, False
)
else:
operation(start_itr)
finally:
buffer.end_user_action()
buffer.unblock_insert_after_signal()
def process_cursor_action(self, buffer, action):
def remove_text(start_itr, end_itr = None):
if end_itr:
buffer.delete(start_itr, end_itr)
return
buffer.backspace(start_itr, interactive = True, default_editable = True)
def delete_text(start_itr, end_itr = None):
if end_itr:
buffer.delete(start_itr, end_itr)
return
start_itr.forward_char()
buffer.backspace(start_itr, interactive = True, default_editable = True)
if action == "BACKSPACE":
self.apply_to_marks(buffer, remove_text)
elif action == "DELETE":
self.apply_to_marks(buffer, delete_text)
elif action == "ENTER":
...
def move_to_offset(self, offset, start_itr):
line_itr = start_itr.copy()
line_itr.forward_to_line_end()
next_line_length = line_itr.get_line_offset()
new_offset = min(offset, next_line_length)
start_itr.set_line_offset(new_offset)
def insert_mark_set(self, target_iter, buffer):
random_bits = random.getrandbits(128)
hash = "%032x" % random_bits
start_mark = Gtk.TextMark.new(
name = f"multi-insert-start-{hash}",
left_gravity = False
)
end_mark = Gtk.TextMark.new(
name = f"multi-insert-end-{hash}",
left_gravity = False
)
# left_gravity = True
buffer.add_mark(start_mark, target_iter)
buffer.add_mark(end_mark, target_iter)
start_mark.set_visible(True)
self.buffer_markers[f"{hash}"] = {
"start_mark": start_mark,
"end_mark": end_mark,
"is_selection": False
}
def remove_mark_set(self, target_iter, buffer) -> bool:
marks = target_iter.get_marks()
for mark_hash in self.buffer_markers:
start_mark, end_mark, is_selection = self.buffer_markers[mark_hash].values()
if not start_mark in marks: continue
start_mark.set_visible(False)
buffer.delete_mark(start_mark)
buffer.delete_mark(end_mark)
del self.buffer_markers[mark_hash]
return True
return False

View File

@@ -1,107 +0,0 @@
# Python imports
import random
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from libs.dto.states import SourceViewStates, MoveDirection, CursorAction
class MarkEventsMixin:
def clear_markers(self, source_view):
buffer = source_view.get_buffer()
for mark in self.insert_markers:
mark.set_visible(False)
buffer.delete_mark(mark)
self.insert_markers.clear()
def _insert_mark(self, insert_iter, target_iter, buffer):
mark_found = self._check_for_insert_marks(target_iter, buffer)
if mark_found: return
random_bits = random.getrandbits(128)
hash = "%032x" % random_bits
mark = Gtk.TextMark.new(
name = f"multi_insert_{hash}",
left_gravity = False
)
buffer.add_mark(mark, target_iter)
mark.set_visible(True)
self.insert_markers.append(mark)
def _check_for_insert_marks(self, target_iter, buffer):
marks = target_iter.get_marks()
for mark in marks:
if mark in self.insert_markers[:]:
mark.set_visible(False)
self.insert_markers.remove(mark)
buffer.delete_mark(mark)
return True
insert_itr = buffer.get_iter_at_mark( buffer.get_insert() )
if target_iter.equal(insert_itr): return True
return False
def _process_cursor_action(self, buffer):
if not self.insert_markers: return
if self.cursor_action == CursorAction.NONE.value: return
action = self.cursor_action
for mark in self.insert_markers:
itr = buffer.get_iter_at_mark(mark)
if action == CursorAction.BACKSPACE.value:
buffer.backspace(itr, interactive = True, default_editable = True)
elif action == CursorAction.DELETE.value:
itr.forward_char()
buffer.backspace(itr, interactive = True, default_editable = True)
elif action == CursorAction.ENTER.value:
...
self.cursor_action = CursorAction.NONE.value
def _process_move_direction(self, buffer):
if not self.insert_markers: return
if self.move_direction == MoveDirection.NONE.value: return
direction = self.move_direction
for mark in self.insert_markers:
itr = buffer.get_iter_at_mark(mark)
if direction == MoveDirection.UP.value:
new_line = itr.get_line() - 1
new_itr = buffer.get_iter_at_line_offset(
new_line,
itr.get_line_index()
)
elif direction == MoveDirection.DOWN.value:
new_line = itr.get_line() + 1
new_itr = buffer.get_iter_at_line_offset(
new_line,
itr.get_line_index()
)
elif direction == MoveDirection.LEFT.value:
if not itr.backward_char(): break
new_itr = itr
elif direction == MoveDirection.RIGHT.value:
if not itr.forward_char(): break
new_itr = itr
else:
continue
buffer.move_mark_by_name(mark.get_name(), new_itr)
self.move_direction = MoveDirection.NONE

View File

@@ -33,8 +33,9 @@ class SourceViewDnDMixin:
def _on_uri_data_received(self, uris: []): def _on_uri_data_received(self, uris: []):
uri = uris.pop(0) uri = uris.pop(0)
self.command.exec_with_args("dnd_load_file_to_buffer", self, uri) self.command.exec_with_args("dnd_load_file_to_buffer", self, uri)
if not uris: return if not uris: return
self.command.exec_with_args("dnd_load_files", (self, uris)) self.command.exec_with_args("dnd_load_files", self, uris)

View File

@@ -58,7 +58,6 @@ class SourceView(GtkSource.View, SourceViewDnDMixin):
def _setup_signals(self): def _setup_signals(self):
self.connect("drag-data-received", self._on_drag_data_received) self.connect("drag-data-received", self._on_drag_data_received)
self.connect("populate-popup", self._on_populate_popup)
def _subscribe_to_events(self): def _subscribe_to_events(self):
... ...
@@ -76,37 +75,6 @@ class SourceView(GtkSource.View, SourceViewDnDMixin):
self._set_up_dnd() self._set_up_dnd()
def _on_populate_popup(self, view, menu):
buffer = self.get_buffer()
language = buffer.get_language()
if language.get_id() == "json":
self._load_prettify_json(view, menu)
menu.show_all()
def _load_prettify_json(self, view, menu):
menu.append( Gtk.SeparatorMenuItem() )
def on_prettify_json(menuitem):
import json
buffer = self.get_buffer()
start_itr, \
end_itr = buffer.get_start_iter(), buffer.get_end_iter()
data = buffer.get_text(start_itr, end_itr, False)
text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4)
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
item = Gtk.MenuItem(label = "Prettify JSON")
item.connect("activate", on_prettify_json)
menu.append(item)
def clear_temp_cut_buffer_delayed(self): def clear_temp_cut_buffer_delayed(self):
if self._cut_temp_timeout_id: if self._cut_temp_timeout_id:
GLib.source_remove(self._cut_temp_timeout_id) GLib.source_remove(self._cut_temp_timeout_id)

View File

@@ -3,12 +3,12 @@
# Lib imports # Lib imports
# Application imports # Application imports
from ..singleton import Singleton from ..singleton_raised import SingletonRaised
from ..dto.base_event import BaseEvent from ..dto.base_event import BaseEvent
from .emit_dispatcher import EmitDispatcher from .emit_dispatcher import EmitDispatcher
from .controller_context import ControllerContext from .controller_message_bus import ControllerMessageBus
@@ -17,25 +17,28 @@ class ControllerBaseException(Exception):
class ControllerBase(Singleton, EmitDispatcher): class ControllerBase(SingletonRaised, EmitDispatcher):
def __init__(self): def __init__(self):
super(ControllerBase, self).__init__() super(ControllerBase, self).__init__()
self.controller_context: ControllerContext = None self.controller_message_bus: ControllerMessageBus = None
def _controller_message(self, event: BaseEvent): def _controller_message(self, event: BaseEvent):
raise ControllerBaseException("Controller Base '_controller_message' must be overridden...") raise ControllerBaseException("Controller Base '_controller_message' must be overridden...")
def set_controller_context(self, controller_context: ControllerContext): def set_controller_message_bus(self, controller_message_bus: ControllerMessageBus):
self.controller_context = controller_context self.controller_message_bus = controller_message_bus
def message(self, event: BaseEvent): def message(self, event: BaseEvent):
return self.controller_context.message(event) return self.controller_message_bus.message(event)
def message_to(self, name: str, event: BaseEvent): def message_to(self, name: str, event: BaseEvent):
return self.controller_context.message_to(name, event) return self.controller_message_bus.message_to(name, event)
def message_to_selected(self, names: list[str], event: BaseEvent): def message_to_selected(self, names: list[str], event: BaseEvent):
for name in names: for name in names:
self.controller_context.message_to_selected(name, event) self.controller_message_bus.message_to_selected(name, event)
def register_controller(self, name: str, controller):
self.controller_message_bus.register_controller(name, controller)

View File

@@ -1,27 +0,0 @@
# Python imports
# Lib imports
# Application imports
from ..dto.base_event import BaseEvent
class ControllerContextException(Exception):
...
class ControllerContext:
def __init__(self):
super(ControllerContext, self).__init__()
def message(self, event: BaseEvent):
raise ControllerContextException("Controller Context 'message' must be overriden by Controller Manager...")
def message_to(self, name: str, event: BaseEvent):
raise ControllerContextException("Controller Context 'message_to' must be overriden by Controller Manager...")
def message_to_selected(self, name: list, event: BaseEvent):
raise ControllerContextException("Controller Context 'message_to_selected' must be overriden by Controller Manager...")

View File

@@ -7,7 +7,7 @@ from ..singleton import Singleton
from ..event_factory import Code_Event_Types from ..event_factory import Code_Event_Types
from .controller_base import ControllerBase from .controller_base import ControllerBase
from .controller_context import ControllerContext from .controller_message_bus import ControllerMessageBus
@@ -17,16 +17,26 @@ class ControllerManagerException(Exception):
class ControllerManager(Singleton, dict): 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): def __init__(self):
super(ControllerManager, self).__init__() super(ControllerManager, self).__init__()
self.message_bus: ControllerMessageBus \
= self._crete_controller_message_bus()
def _crete_controller_context(self) -> ControllerContext:
controller_context = ControllerContext()
controller_context.message_to = self.message_to
controller_context.message = self.message
return controller_context 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
return controller_message_bus
def register_controller(self, name: str, controller: ControllerBase): def register_controller(self, name: str, controller: ControllerBase):
if not name or controller == None: if not name or controller == None:
@@ -37,7 +47,7 @@ class ControllerManager(Singleton, dict):
f"Can't bind controller to existing registered name of '{name}'..." f"Can't bind controller to existing registered name of '{name}'..."
) )
controller.set_controller_context( self._crete_controller_context() ) controller.set_controller_message_bus( self.message_bus )
self[name] = controller self[name] = controller

View File

@@ -0,0 +1,30 @@
# 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...")

View File

@@ -8,6 +8,13 @@ from ..dto.base_event import BaseEvent
class EmitDispatcher: 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): def __init__(self):
super(EmitDispatcher, self).__init__() super(EmitDispatcher, self).__init__()

View File

@@ -9,6 +9,8 @@ from dataclasses import dataclass, field
@dataclass(slots = True) @dataclass(slots = True)
class BaseEvent: class BaseEvent:
topic: str = None topic: str = None
content: any = None content: any = None
raw_content: any = None raw_content: any = None
success: callable = None
fail: callable = None

View File

@@ -6,8 +6,10 @@
from .code_event import CodeEvent from .code_event import CodeEvent
from .register_provider_event import RegisterProviderEvent from .register_provider_event import RegisterProviderEvent
from .register_command_event import RegisterCommandEvent from .register_command_event import RegisterCommandEvent
from .file_externally_modified_event import FileExternallyModifiedEvent from .file_externally_modified_event import FileExternallyModifiedEvent
from .file_externally_deleted_event import FileExternallyDeletedEvent from .file_externally_deleted_event import FileExternallyDeletedEvent
from .set_info_labels_event import SetInfoLabelsEvent
from .populate_source_view_popup_event import PopulateSourceViewPopupEvent
from .get_new_command_system_event import GetNewCommandSystemEvent from .get_new_command_system_event import GetNewCommandSystemEvent
from .request_completion_event import RequestCompletionEvent from .request_completion_event import RequestCompletionEvent
@@ -20,7 +22,6 @@ from .set_active_file_event import SetActiveFileEvent
from .file_path_set_event import FilePathSetEvent from .file_path_set_event import FilePathSetEvent
from .added_new_file_event import AddedNewFileEvent from .added_new_file_event import AddedNewFileEvent
from .swapped_file_event import SwappedFileEvent
from .popped_file_event import PoppedFileEvent from .popped_file_event import PoppedFileEvent
from .removed_file_event import RemovedFileEvent from .removed_file_event import RemovedFileEvent
from .saved_file_event import SavedFileEvent from .saved_file_event import SavedFileEvent
@@ -28,6 +29,5 @@ from .saved_file_event import SavedFileEvent
from .get_file_event import GetFileEvent from .get_file_event import GetFileEvent
from .get_swap_file_event import GetSwapFileEvent from .get_swap_file_event import GetSwapFileEvent
from .add_new_file_event import AddNewFileEvent from .add_new_file_event import AddNewFileEvent
from .swap_file_event import SwapFileEvent
from .pop_file_event import PopFileEvent from .pop_file_event import PopFileEvent
from .remove_file_event import RemoveFileEvent from .remove_file_event import RemoveFileEvent

View File

@@ -0,0 +1,18 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .code_event import CodeEvent
@dataclass
class PopulateSourceViewPopupEvent(CodeEvent):
menu: Gtk.Widget = None

View File

@@ -2,6 +2,8 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
# Lib imports # Lib imports
import gi
from gi.repository import Gio
# Application imports # Application imports
from .code_event import CodeEvent from .code_event import CodeEvent
@@ -9,5 +11,5 @@ from .code_event import CodeEvent
@dataclass @dataclass
class SwapFileEvent(CodeEvent): class SetInfoLabelsEvent(CodeEvent):
... info: tuple[str or Gio.File] = None

View File

@@ -1,13 +0,0 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
# Application imports
from .code_event import CodeEvent
@dataclass
class SwappedFileEvent(CodeEvent):
...

View File

@@ -4,4 +4,3 @@
from .source_view_states import SourceViewStates from .source_view_states import SourceViewStates
from .cursor_action import CursorAction from .cursor_action import CursorAction
from .move_direction import MoveDirection

View File

@@ -1,15 +0,0 @@
# Python imports
from enum import Enum
# Lib imports
# Application imports
class MoveDirection(Enum):
NONE = 0
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4

View File

@@ -87,6 +87,16 @@ class IPCServer(Singleton):
conn.close() conn.close()
break 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:

View File

@@ -21,6 +21,12 @@ class IPCSignalsMixin:
self.broadcast_message, "handle-file", (fpath,) self.broadcast_message, "handle-file", (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: def handle_dir_from_ipc(self, fpath: str) -> None:
logger.debug(f"Dir From IPC: {fpath}") logger.debug(f"Dir From IPC: {fpath}")
GLib.idle_add( GLib.idle_add(

View File

@@ -15,18 +15,18 @@ class SingletonError(Exception):
T = TypeVar('T', bound='Singleton') T = TypeVar('T', bound='Singleton')
class Singleton: class Singleton:
_instance = None __instance = None
def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T:
if cls._instance is not None: if cls.__instance is not None:
logger.debug(f"'{cls.__name__}' is a Singleton. Returning instance...") logger.debug(f"'{cls.__name__}' is a Singleton. Returning instance...")
return cls._instance return cls.__instance
cls._instance = super(Singleton, cls).__new__(cls) cls.__instance = super(Singleton, cls).__new__(cls)
return cls._instance return cls.__instance
def __init__(self) -> None: def __init__(self) -> None:
if self._instance is not None: if self.__instance is not None:
return return
super(Singleton, self).__init__() super(Singleton, self).__init__()

View File

@@ -15,15 +15,15 @@ class SingletonError(Exception):
T = TypeVar('T', bound='SingletonRaised') T = TypeVar('T', bound='SingletonRaised')
class SingletonRaised: class SingletonRaised:
_instance = None __instance = None
def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T:
if cls._instance is not None: if cls.__instance is not None:
raise SingletonError(f"'{cls.__name__}' is a Singleton. Cannot create a new instance...") raise SingletonError(f"'{cls.__name__}' is a Singleton. Cannot create a new instance...")
cls._instance = super(SingletonRaised, cls).__new__(cls) cls.__instance = super(SingletonRaised, cls).__new__(cls)
return cls._instance return cls.__instance
def __init__(self) -> None: def __init__(self) -> None:
if self._instance is not None: if self.__instance is not None:
return return

View File

@@ -3,7 +3,8 @@ import os
import sys import sys
import importlib import importlib
import traceback import traceback
import asyncio
from concurrent.futures import ThreadPoolExecutor
from os.path import join from os.path import join
from os.path import isdir from os.path import isdir
@@ -66,26 +67,22 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
parent_path = os.getcwd() parent_path = os.getcwd()
for manifest_meta in manifest_metas: for manifest_meta in manifest_metas:
path, folder, manifest = manifest_meta.path, manifest_meta.folder, manifest_meta.manifest
try: try:
target = join(path, "plugin.py") path, \
folder, \
manifest = manifest_meta.path, manifest_meta.folder, manifest_meta.manifest
target = join(path, "plugin.py")
if not os.path.exists(target): if not os.path.exists(target):
raise PluginsControllerException("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...") raise PluginsControllerException(
"Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load..."
)
module = self._load_plugin_module(path, folder, target) module = self._load_plugin_module(path, folder, target)
if is_pre_launch: self._handle_plugin_execute(is_pre_launch, module, manifest_meta)
asyncio.run( except PluginsControllerException as e:
self.execute_plugin(module, manifest_meta) logger.info(f"Malformed Plugin: Not loading -->: '{manifest_meta.folder}' !")
)
else:
GLib.idle_add(
asyncio.run,
self.execute_plugin(module, manifest_meta)
)
except Exception as e:
logger.info(f"Malformed Plugin: Not loading -->: '{folder}' !")
logger.debug(f"Trace: {traceback.print_exc()}") logger.debug(f"Trace: {traceback.print_exc()}")
os.chdir(parent_path) os.chdir(parent_path)
@@ -103,17 +100,27 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
return module return module
def create_plugin_context(self): def _handle_plugin_execute(
plugin_context: PluginContext = PluginContext() self, is_pre_launch: bool, module, manifest_meta
):
if not is_pre_launch:
GLib.idle_add(
self._run_with_pool, module, manifest_meta
)
return
plugin_context.requests_ui_element: callable = self.requests_ui_element self._run_with_pool(module, manifest_meta)
plugin_context.message: callable = self.message
plugin_context.message_to: callable = self.message_to
plugin_context.message_to_selected: callable = self.message_to_selected
plugin_context.emit: callable = event_system.emit
plugin_context.emit_and_await: callable = event_system.emit_and_await
return plugin_context def _run_with_pool(self, module: type, manifest_meta: ManifestMeta):
with ThreadPoolExecutor(max_workers = 1) as executor:
future = executor.submit(self.execute_plugin, module, manifest_meta)
future.add_done_callback(self._handle_future_exception)
def _handle_future_exception(self, future):
try:
future.result()
except Exception:
logger.exception("Plugin crashed during execution...")
def pre_launch_plugins(self) -> None: def pre_launch_plugins(self) -> None:
logger.info(f"Loading pre-launch plugins...") logger.info(f"Loading pre-launch plugins...")
@@ -125,7 +132,7 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
manifest_metas: list = self._manifest_manager.get_post_launch_plugins() manifest_metas: list = self._manifest_manager.get_post_launch_plugins()
self._load_plugins(manifest_metas) self._load_plugins(manifest_metas)
async def execute_plugin(self, module: type, manifest_meta: ManifestMeta): def execute_plugin(self, module: type, manifest_meta: ManifestMeta):
plugin = module.Plugin() plugin = module.Plugin()
plugin.plugin_context: PluginContext = self.create_plugin_context() plugin.plugin_context: PluginContext = self.create_plugin_context()
@@ -140,6 +147,18 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
self._plugin_collection.append(manifest_meta) self._plugin_collection.append(manifest_meta)
def create_plugin_context(self):
plugin_context: PluginContext = PluginContext()
plugin_context.requests_ui_element: callable = self.requests_ui_element
plugin_context.message: callable = self.message
plugin_context.message_to: callable = self.message_to
plugin_context.message_to_selected: callable = self.message_to_selected
plugin_context.emit: callable = event_system.emit
plugin_context.emit_and_await: callable = event_system.emit_and_await
plugin_context.register_controller: callable = self.register_controller
return plugin_context
plugins_controller = PluginsController() plugins_controller = PluginsController()

View File

@@ -38,3 +38,7 @@ class PluginContext:
def emit_and_await(self, event_type: str, data: tuple = ()): def emit_and_await(self, event_type: str, data: tuple = ()):
raise PluginContextException("Plugin Context 'emit_and_await' must be overridden...") raise PluginContextException("Plugin Context 'emit_and_await' must be overridden...")
def register_controller(self, name: str, controller):
raise PluginContextException("Plugin Context 'register_controller' must be overridden...")

View File

@@ -31,6 +31,9 @@ class PluginCode(PluginBase):
def run(self): def run(self):
raise PluginCodeException("Plugin Code 'run' must be overriden by Plugin") raise PluginCodeException("Plugin Code 'run' must be overriden by Plugin")
def requests_ui_element(self, element_id: str):
return self.plugin_context.requests_ui_element(element_id)
def message(self, event: BaseEvent): def message(self, event: BaseEvent):
return self.plugin_context.message(event) return self.plugin_context.message(event)
@@ -39,3 +42,6 @@ class PluginCode(PluginBase):
def message_to_selected(self, names: list[str], event: BaseEvent): def message_to_selected(self, names: list[str], event: BaseEvent):
return self.plugin_context.message_to_selected(names, event) return self.plugin_context.message_to_selected(names, event)
def register_controller(self, name: str, controller):
return self.plugin_context.register_controller(name, controller)