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
This commit is contained in:
2026-02-26 21:09:00 -06:00
parent 597ac2b06a
commit b724d41f6c
16 changed files with 112 additions and 73 deletions

View File

@@ -36,18 +36,15 @@ class CodeContainer(Gtk.Box):
...
def _load_widgets(self):
widget_registery.expose_object("code-container", self)
code_base = CodeBase()
self.add( self._create_tabs_widgets(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):
editors_container = Gtk.Box()
widget_registery.expose_object("code-container", self)
widget_registery.expose_object("editors-container", editors_container)
editors_container.add( Separator("separator_left") )

View File

@@ -8,7 +8,6 @@ from plugins import plugins_controller
from libs.controllers.controller_manager import ControllerManager
from .controllers.files_controller import FilesController
from .controllers.tabs_controller import TabsController
from .controllers.commands_controller import CommandsController
from .controllers.completion_controller import CompletionController
from .controllers.views.source_views_controller import SourceViewsController
@@ -31,23 +30,18 @@ class CodeBase:
def _load_controllers(self):
files_controller = FilesController()
tabs_controller = TabsController()
commands_controller = CommandsController()
completion_controller = CompletionController()
source_views_controller = SourceViewsController()
# self.controller_manager.register_controller("base", self)
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("completion", completion_controller)
self.controller_manager.register_controller("source_views", source_views_controller)
self.controller_manager.register_controller("plugins", plugins_controller)
self.controller_manager.register_controller("widgets", widget_registery)
def get_tabs_widget(self):
return self.controller_manager["tabs"].get_tabs_widget()
def create_source_view(self):
source_view = self.controller_manager["source_views"].create_source_view()
self.controller_manager["completion"].register_completer(

View File

@@ -1,74 +0,0 @@
# Python imports
# Lib imports
import gi
from gi.repository import Gtk
# Application imports
from libs.controllers.controller_base import ControllerBase
from libs.event_factory import Event_Factory, Code_Event_Types
from ..tabs_widget import TabsWidget
from ..tab_widget import TabWidget
from ..source_view import SourceView
class TabsController(ControllerBase):
def __init__(self):
super(TabsController, self).__init__()
self.tabs_widget: TabsWidget = TabsWidget()
self.tabs_widget.message = self.message
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.FocusedViewEvent):
self.tabs_widget.view_changed( event.view.get_buffer() )
elif isinstance(event, Code_Event_Types.FilePathSetEvent):
self.update_tab_label(event)
elif isinstance(event, Code_Event_Types.ModifiedChangedEvent):
self.tabs_widget.modified_changed( event.buffer )
elif isinstance(event, Code_Event_Types.FileExternallyDeletedEvent):
self.tabs_widget.externally_deleted( event.buffer )
elif isinstance(event, Code_Event_Types.AddedNewFileEvent):
self.add_tab(event)
elif isinstance(event, Code_Event_Types.PoppedFileEvent):
...
elif isinstance(event, Code_Event_Types.RemovedFileEvent):
self.remove_tab(event)
def get_tabs_widget(self):
return self.tabs_widget
def update_tab_label(self, event: Code_Event_Types.FilePathSetEvent):
for page_widget in self.tabs_widget.get_children():
tab = self.tabs_widget.get_tab_label(page_widget)
if not event.file == tab.file: continue
tab.label.set_label(event.file.fname)
break
def add_tab(self, event: Code_Event_Types.AddedNewFileEvent):
tab = TabWidget()
tab.file = event.file
tab.label.set_label(event.file.fname)
self.tabs_widget.append_page(Gtk.Separator(), tab)
tab.show_all()
def remove_tab(self, event: Code_Event_Types.RemovedFileEvent):
for page_widget in self.tabs_widget.get_children():
tab = self.tabs_widget.get_tab_label(page_widget)
if not event.file == tab.file: continue
tab.clear_signals_and_data()
self.tabs_widget.remove_page(
self.tabs_widget.page_num(page_widget)
)
break

View File

@@ -1,79 +0,0 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class TabWidget(Gtk.Box):
"""docstring for TabWidget"""
def __init__(self):
super(TabWidget, self).__init__()
self.file = None
self._handler_id = None
self._eve_handler_id = None
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("tab-widget")
self.set_orientation(0)
self.set_hexpand(False)
self.set_vexpand(False)
self.set_size_request(-1, 12)
def _setup_signals(self):
...
def _load_widgets(self):
self.event_box = Gtk.EventBox()
self.label = Gtk.Label()
self.close_bttn = Gtk.Button()
icon = Gtk.Image(stock = Gtk.STOCK_CLOSE)
self.event_box.set_above_child(True)
ctx = self.label.get_style_context()
ctx.add_class("tab-label")
ctx = self.close_bttn.get_style_context()
ctx.add_class("tab-close-bttn")
self.label.set_xalign(0.0)
self.label.set_margin_left(25)
self.label.set_margin_right(25)
self.label.set_hexpand(True)
self.close_bttn.add(icon)
self.event_box.add(self.label)
self.add(self.event_box)
self.add(self.close_bttn)
self.show_all()
def clear_signals_and_data(self):
self.close_bttn.disconnect(self._handler_id)
self.event_box.disconnect(self._eve_handler_id)
self._handler_id = None
for child in self.get_children():
child.unparent()
child.run_dispose()
child.destroy()
def set_close_signal(self, callback):
self._handler_id = self.close_bttn.connect(
'clicked',
callback,
self.file
)

View File

@@ -1,177 +0,0 @@
# 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
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .tab_widget import TabWidget
class TabsWidget(Gtk.Notebook):
def __init__(self):
super(TabsWidget, self).__init__()
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
def _setup_styling(self):
self.set_scrollable(True)
def _setup_signals(self):
self.connect("page-added", self._page_added)
self.switch_page_id = \
self.connect_after("switch-page", self._switch_page)
def _subscribe_to_events(self):
...
def _load_widgets(self):
...
def _page_added(self, notebook, page_widget, page_num):
tab = self.get_tab_label(page_widget)
tab.set_close_signal(self._close_tab)
self._bind_tab_menu(tab, page_widget)
page_widget.show()
self.set_tab_detachable(page_widget, True)
self.set_tab_reorderable(page_widget, True)
def _close_tab(self, button, file):
event = Event_Factory.create_event(
"remove_file",
buffer = file.buffer
)
self.message(event)
def _switch_page(self, notebook, page_widget, page_num):
tab = self.get_tab_label(page_widget)
event = Event_Factory.create_event(
"set_active_file",
buffer = tab.file.buffer
)
self.message(event)
def _bind_tab_menu(self, tab, page_widget):
def do_context_menu(tab, eve, page_widget):
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
menu = self.create_menu(page_widget)
menu.popup_at_pointer(eve)
tab._eve_handler_id = \
tab.event_box.connect(
"button-release-event",
do_context_menu,
page_widget
)
def create_menu(self, page_widget) -> Gtk.Menu:
context_menu = Gtk.Menu()
close_item = Gtk.MenuItem(label = "Close Tab")
close_left_item = Gtk.MenuItem(label = "Close Tabs Left")
close_right_item = Gtk.MenuItem(label = "Close Tabs Right")
close_other_item = Gtk.MenuItem(label = "Close Other Tabs")
close_all_item = Gtk.MenuItem(label = "Close All Tabs")
close_item.connect("activate", self.close_item, page_widget)
close_left_item.connect("activate", self.close_left_items, page_widget)
close_right_item.connect("activate", self.close_right_items, page_widget)
close_other_item.connect("activate", self.close_other_items, page_widget)
close_all_item.connect("activate", self.close_all_items, page_widget)
context_menu.append(close_item)
context_menu.append(close_left_item)
context_menu.append(close_right_item)
context_menu.append(close_other_item)
context_menu.append(close_all_item)
context_menu.show_all()
return context_menu
def view_changed(self, buffer):
for page_widget in self.get_children():
tab = self.get_tab_label(page_widget)
if not buffer == tab.file.buffer: continue
self.handler_block(self.switch_page_id)
self.set_current_page(
self.page_num(page_widget)
)
self.handler_unblock(self.switch_page_id)
break
def modified_changed(self, buffer):
for page_widget in self.get_children():
tab = self.get_tab_label(page_widget)
if not buffer == tab.file.buffer: continue
ctx = tab.label.get_style_context()
ctx.remove_class("file-deleted")
if buffer.get_modified():
ctx.add_class("file-changed")
else:
ctx.remove_class("file-changed")
break
def externally_deleted(self, buffer):
for page_widget in self.get_children():
tab = self.get_tab_label(page_widget)
if not buffer == tab.file.buffer: continue
ctx = tab.label.get_style_context()
ctx.add_class("file-deleted")
break
def close_item(self, menu_item, page_widget):
tab = self.get_tab_label(page_widget)
tab.close_bttn.clicked()
def close_left_items(self, menu_item, page_widget):
children = self.get_children()
i = children.index(page_widget)
if i == 0: return
for widget in children[ : i]:
tab = self.get_tab_label(widget)
tab.close_bttn.clicked()
def close_right_items(self, menu_item, page_widget):
children = self.get_children()
i = children.index(page_widget) + 1
if i == len(children): return
for widget in children[i : ]:
tab = self.get_tab_label(widget)
tab.close_bttn.clicked()
def close_other_items(self, menu_item, page_widget):
self.close_left_items(menu_item, page_widget)
self.close_right_items(menu_item, page_widget)
def close_all_items(self, menu_item, page_widget):
children = self.get_children()
for widget in children[ : ]:
tab = self.get_tab_label(widget)
tab.close_bttn.clicked()

View File

@@ -39,3 +39,6 @@ class ControllerBase(Singleton, EmitDispatcher):
def message_to_selected(self, names: list[str], event: BaseEvent):
for name in names:
self.controller_context.message_to_selected(name, event)
def register_controller(self, name: str, controller):
self.controller_context.register_controller(name, controller)

View File

@@ -25,3 +25,6 @@ class ControllerContext:
def message_to_selected(self, name: list, event: BaseEvent):
raise ControllerContextException("Controller Context 'message_to_selected' must be overriden by Controller Manager...")
def register_controller(self, name: str, controller):
raise ControllerContextException("Controller Context 'register_controller' must be overriden by Controller Manager...")

View File

@@ -22,9 +22,10 @@ class ControllerManager(Singleton, dict):
def _crete_controller_context(self) -> ControllerContext:
controller_context = ControllerContext()
controller_context.message_to = self.message_to
controller_context.message = self.message
controller_context = ControllerContext()
controller_context.message_to = self.message_to
controller_context.message = self.message
controller_context.register_controller = self.register_controller
return controller_context

View File

@@ -67,31 +67,26 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
parent_path = os.getcwd()
for manifest_meta in manifest_metas:
path, folder, manifest = manifest_meta.path, manifest_meta.folder, manifest_meta.manifest
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):
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)
if is_pre_launch:
self._run_with_pool(module, manifest_meta)
else:
GLib.idle_add(
self._run_with_pool, module, manifest_meta
)
except Exception as e:
logger.info(f"Malformed Plugin: Not loading -->: '{folder}' !")
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 _run_with_pool(self, module: type, manifest_meta: ManifestMeta):
with ThreadPoolExecutor(max_workers = 1) as executor:
executor.submit(self.execute_plugin, module, manifest_meta)
def _load_plugin_module(self, path, folder, target):
os.chdir(path)
@@ -105,17 +100,27 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
return module
def create_plugin_context(self):
plugin_context: PluginContext = PluginContext()
def _handle_plugin_execute(
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
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
self._run_with_pool(module, manifest_meta)
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:
logger.info(f"Loading pre-launch plugins...")
@@ -142,6 +147,18 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi
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()

View File

@@ -38,3 +38,7 @@ class PluginContext:
def emit_and_await(self, event_type: str, data: tuple = ()):
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

@@ -42,3 +42,6 @@ class PluginCode(PluginBase):
def message_to_selected(self, names: list[str], event: BaseEvent):
return self.plugin_context.message_to_selected(names, event)
def register_controller(self, name: str, controller):
return self.plugin_context.register_controller(name, controller)