From 77a3b71d316334bca1b3c884fa9fd7ef03e10bf7 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sat, 21 Mar 2026 13:22:20 -0500 Subject: [PATCH] feat: Complete plugin lifecycle management with lazy loading and runtime reload Major changes: - Add unload() method to all plugins for proper cleanup (unregister commands/providers/LSP clients, destroy widgets, clear state) - Implement lazy widget loading via "show" signal across all containers - Add autoload: false manifest option for manual/conditional plugin loading - Add Plugins UI with runtime load/unload toggle via Ctrl+Shift+p - Implement controller unregistration system with proper signal disconnection - Add new events: UnregisterCommandEvent, GetFilesEvent, GetSourceViewsEvent, TogglePluginsUiEvent - Fix signal leaks by tracking and disconnecting handlers in widgets (search/replace, LSP manager, tabs, telescope, markdown preview) - Add Save/Save As to tabs context menu - Improve search/replace behavior (selection handling, focus management) - Add telescope file initialization from existing loaded files - Refactor plugin reload watcher to dynamically add/remove plugins on filesystem changes - Add new plugins: file_history, extend_source_view_menu, godot_lsp_client - Fix bug in prettify_json (undefined variable reference) --- TODO.md | 22 +++ plugins/code/commands/autopairs/plugin.py | 18 +++ plugins/code/commands/colorize/plugin.py | 15 +- plugins/code/commands/commentzar/plugin.py | 10 ++ .../code/commands/file_history/__init__.py | 3 + .../code/commands/file_history/__main__.py | 3 + .../code/commands/file_history/autopairs.py | 97 ++++++++++++ .../code/commands/file_history/manifest.json | 7 + plugins/code/commands/file_history/plugin.py | 65 ++++++++ .../commands/nanoesq_temp_buffer/plugin.py | 10 +- .../commands/toggle_source_view/plugin.py | 10 ++ .../completers/example_completer/plugin.py | 10 ++ .../completers/python_completer/plugin.py | 10 ++ .../completers/snippets_completer/plugin.py | 10 ++ .../code/completers/words_completer/plugin.py | 10 ++ .../extend_source_view_menu/__init__.py | 3 + .../extend_source_view_menu/__main__.py | 3 + .../extend_source_view_menu/manifest.json | 7 + .../extend_source_view_menu/plugin.py | 29 ++++ .../source_view_menu.py | 68 +++++++++ .../file_state_watcher/plugin.py | 7 +- .../file_state_watcher/watcher_checks.py | 8 +- .../event-watchers/prettify_json/plugin.py | 3 + .../prettify_json/prettify_json.py | 2 +- .../godot_lsp_client/__init__.py | 3 + .../godot_lsp_client/__main__.py | 3 + .../config/lsp-server-config.json | 52 +++++++ .../godot_lsp_client/manifest.json | 8 + .../godot_lsp_client/plugin.py | 44 ++++++ .../response_handler/__init__.py | 1 + .../response_handler/python.py | 12 ++ .../java_lsp_client/manifest.json | 1 + .../java_lsp_client/plugin.py | 6 + .../lsp_manager/lsp_manager.py | 7 +- .../lsp_manager/lsp_manager_ui.py | 32 +++- .../lsp_manager/manifest.json | 2 +- .../lsp_manager/plugin.py | 25 ++++ .../provider/provider_response_cache.py | 24 +-- .../response_handlers/response_registry.py | 2 +- .../python_lsp_client/manifest.json | 1 + .../python_lsp_client/plugin.py | 6 + plugins/code/ui/code_minimap/plugin.py | 7 + plugins/code/ui/info_bar/info_bar_widget.py | 3 - plugins/code/ui/info_bar/plugin.py | 3 + .../code/ui/markdown_preview/manifest.json | 1 + .../ui/markdown_preview/markdown_preview.py | 10 +- plugins/code/ui/markdown_preview/plugin.py | 24 ++- .../mixins/search_replace_mixin.py | 22 ++- .../code/ui/search_replace/mode_buttons.py | 54 ++++--- plugins/code/ui/search_replace/plugin.py | 14 ++ .../code/ui/search_replace/search_replace.py | 55 +++++-- plugins/code/ui/tabs_bar/plugin.py | 22 ++- plugins/code/ui/tabs_bar/tab_widget.py | 5 - plugins/code/ui/tabs_bar/tabs_controller.py | 19 ++- plugins/code/ui/tabs_bar/tabs_widget.py | 56 +++++-- plugins/code/ui/telescope/list_box.py | 6 +- plugins/code/ui/telescope/plugin.py | 32 +++- plugins/code/ui/telescope/telescope.py | 11 +- plugins/ui/template/plugin.py | 10 +- src/__builtins__.py | 18 +-- src/app.py | 3 +- src/core/containers/base_container.py | 7 +- src/core/containers/body_container.py | 7 +- src/core/containers/center_container.py | 8 +- src/core/containers/code/editors_container.py | 18 +-- src/core/containers/footer_container.py | 8 +- src/core/containers/header_container.py | 7 +- src/core/containers/left_container.py | 7 +- src/core/containers/right_container.py | 7 +- src/core/controllers/base_controller.py | 2 +- .../code/command_system/command_system.py | 139 +++++++++--------- .../commands/toggle_plugins_ui.py | 21 +++ .../code/controllers/files_controller.py | 2 + .../code/controllers/views/signal_mapper.py | 6 +- .../views/source_views_controller.py | 22 +++ src/core/widgets/code/key_mapper.py | 22 +++ .../code/mixins/command_system_mixin.py | 83 +++++++++++ src/core/window.py | 5 + src/libs/controllers/controller_base.py | 7 +- src/libs/controllers/controller_manager.py | 20 ++- .../controllers/controller_message_bus.py | 3 + src/libs/dto/code/events/__init__.py | 4 + src/libs/dto/code/events/get_files_event.py | 13 ++ .../dto/code/events/get_source_views_event.py | 13 ++ .../code/events/toggle_plugins_ui_event.py | 17 +++ .../code/events/unregister_command_event.py | 20 +++ src/libs/dto/plugins/manifest.py | 1 + src/libs/dto/plugins/manifest_meta.py | 7 +- src/libs/event_factory.py | 16 ++ src/libs/singleton.py | 24 +-- src/plugins/controller.py | 79 ++++++---- src/plugins/manifest_manager.py | 22 ++- src/plugins/plugin_context.py | 3 + src/plugins/plugin_reload_mixin.py | 54 +++++-- src/plugins/plugin_types/plugin_base.py | 3 + src/plugins/plugin_types/plugin_code.py | 3 + src/plugins/plugins_ui.py | 100 +++++++++++++ .../usr/share/newton/code-key-bindings.json | 3 + 98 files changed, 1520 insertions(+), 297 deletions(-) create mode 100644 TODO.md create mode 100644 plugins/code/commands/file_history/__init__.py create mode 100644 plugins/code/commands/file_history/__main__.py create mode 100644 plugins/code/commands/file_history/autopairs.py create mode 100644 plugins/code/commands/file_history/manifest.json create mode 100644 plugins/code/commands/file_history/plugin.py create mode 100644 plugins/code/event-watchers/extend_source_view_menu/__init__.py create mode 100644 plugins/code/event-watchers/extend_source_view_menu/__main__.py create mode 100644 plugins/code/event-watchers/extend_source_view_menu/manifest.json create mode 100644 plugins/code/event-watchers/extend_source_view_menu/plugin.py create mode 100644 plugins/code/event-watchers/extend_source_view_menu/source_view_menu.py create mode 100644 plugins/code/language_server_clients/godot_lsp_client/__init__.py create mode 100644 plugins/code/language_server_clients/godot_lsp_client/__main__.py create mode 100644 plugins/code/language_server_clients/godot_lsp_client/config/lsp-server-config.json create mode 100644 plugins/code/language_server_clients/godot_lsp_client/manifest.json create mode 100644 plugins/code/language_server_clients/godot_lsp_client/plugin.py create mode 100644 plugins/code/language_server_clients/godot_lsp_client/response_handler/__init__.py create mode 100644 plugins/code/language_server_clients/godot_lsp_client/response_handler/python.py create mode 100644 src/core/widgets/code/command_system/commands/toggle_plugins_ui.py create mode 100644 src/core/widgets/code/mixins/command_system_mixin.py create mode 100644 src/libs/dto/code/events/get_files_event.py create mode 100644 src/libs/dto/code/events/get_source_views_event.py create mode 100644 src/libs/dto/code/events/toggle_plugins_ui_event.py create mode 100644 src/libs/dto/code/events/unregister_command_event.py create mode 100644 src/plugins/plugins_ui.py diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..95cb5f9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,22 @@ +___ +### Add +1. Add Godot LSP Client +1. Add Collapsable code blocks +1. Add Plugin to | and | to split views up, down, left, right +1. Add i to **lsp_manager** to list who implements xyz + +___ +### Change +1. Make **telescope** plugin a generic base to allow query mode additions through plugins +1. Make **lsp_manager** hard coded values configurable, plus add fields to UI +___ +### Fix +- Fix **telescope** search selection when items hidden such that + hidden items don't get selected on up/down key +- Fix to make acive tab on **tabs_bar** scroll to center +- Fix **file_state_watcher** to prompt refrsh if external changes applied +- Fix on lsp client unload to close files lsp side and unload server endpoint +- Fix multi-select left/right block select movement de-sync + from leader when '_' in word + +___ diff --git a/plugins/code/commands/autopairs/plugin.py b/plugins/code/commands/autopairs/plugin.py index d034cea..1da1ad0 100644 --- a/plugins/code/commands/autopairs/plugin.py +++ b/plugins/code/commands/autopairs/plugin.py @@ -39,6 +39,24 @@ class Plugin(PluginCode): self.emit_to("source_views", event) + def unload(self): + event = Event_Factory.create_event("unregister_command", + command_name = "autopairs", + command = Handler, + binding_mode = "held", + binding = [ + "'", "`", "[", "]", + '"', + '(', + ')', + '{', + '}' + ] + ) + + self.emit_to("source_views", event) + autopairs = None + def run(self): ... diff --git a/plugins/code/commands/colorize/plugin.py b/plugins/code/commands/colorize/plugin.py index 5db3eb7..de7aeb0 100644 --- a/plugins/code/commands/colorize/plugin.py +++ b/plugins/code/commands/colorize/plugin.py @@ -27,7 +27,19 @@ class Plugin(PluginCode): colorize.handle_colorize(event.buffer) def load(self): - event = Event_Factory.create_event("register_command", + self._manage_signals("register_command") + + def unload(self): + self._manage_signals("unregister_command") + event = Event_Factory.create_event("get_source_views") + + self.emit_to("source_views", event) + for view in event.response: + buffer = view.get_buffer() + colorize.clear_color_tags(buffer) + + def _manage_signals(self, action: str): + event = Event_Factory.create_event(action, command_name = "tggle_colorize", command = Handler, binding_mode = "released", @@ -36,6 +48,7 @@ class Plugin(PluginCode): self.emit_to("source_views", event) + def run(self): ... diff --git a/plugins/code/commands/commentzar/plugin.py b/plugins/code/commands/commentzar/plugin.py index c882182..7c85e63 100644 --- a/plugins/code/commands/commentzar/plugin.py +++ b/plugins/code/commands/commentzar/plugin.py @@ -36,6 +36,16 @@ class Plugin(PluginCode): self.emit_to("source_views", event) + def unload(self): + event = Event_Factory.create_event("unregister_command", + command_name = "keyboard_tggl_comment", + command = Handler, + binding_mode = "released", + binding = "slash" + ) + + self.emit_to("source_views", event) + def run(self): ... diff --git a/plugins/code/commands/file_history/__init__.py b/plugins/code/commands/file_history/__init__.py new file mode 100644 index 0000000..e6b1b36 --- /dev/null +++ b/plugins/code/commands/file_history/__init__.py @@ -0,0 +1,3 @@ +""" + Plugin Module +""" diff --git a/plugins/code/commands/file_history/__main__.py b/plugins/code/commands/file_history/__main__.py new file mode 100644 index 0000000..c2e27a7 --- /dev/null +++ b/plugins/code/commands/file_history/__main__.py @@ -0,0 +1,3 @@ +""" + Plugin Package +""" diff --git a/plugins/code/commands/file_history/autopairs.py b/plugins/code/commands/file_history/autopairs.py new file mode 100644 index 0000000..221393a --- /dev/null +++ b/plugins/code/commands/file_history/autopairs.py @@ -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 diff --git a/plugins/code/commands/file_history/manifest.json b/plugins/code/commands/file_history/manifest.json new file mode 100644 index 0000000..8561d56 --- /dev/null +++ b/plugins/code/commands/file_history/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "File History", + "author": "ITDominator", + "version": "0.0.1", + "support": "", + "requests": {} +} diff --git a/plugins/code/commands/file_history/plugin.py b/plugins/code/commands/file_history/plugin.py new file mode 100644 index 0000000..18086c6 --- /dev/null +++ b/plugins/code/commands/file_history/plugin.py @@ -0,0 +1,65 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from plugins.plugin_types import PluginCode + + + +history: list = [] +history_size: int = 30 + + +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.RemovedFileEvent): + if event.file.ftype == "buffer": return + + if len(history) == history_size: + history.pop(0) + + history.append(event.file) + + def load(self): + self._manage_signals("register_command") + + def unload(self): + self._manage_signals("unregister_command") + + def _manage_signals(self, action: str): + event = Event_Factory.create_event(action, + command_name = "file_history_pop", + command = Handler, + binding_mode = "released", + binding = "t" + ) + + self.emit_to("source_views", event) + + def run(self): + ... + + +class Handler: + @staticmethod + def execute( + view: any, + char_str: str, + *args, + **kwargs + ): + logger.debug("Command: File History") + if len(history) == 0: return + + view._on_uri_data_received( + [ + history.pop().replace("file://", "") + ] + ) diff --git a/plugins/code/commands/nanoesq_temp_buffer/plugin.py b/plugins/code/commands/nanoesq_temp_buffer/plugin.py index 392caf0..028c460 100644 --- a/plugins/code/commands/nanoesq_temp_buffer/plugin.py +++ b/plugins/code/commands/nanoesq_temp_buffer/plugin.py @@ -21,7 +21,13 @@ class Plugin(PluginCode): ... def load(self): - event = Event_Factory.create_event("register_command", + self._manage_signals("register_command") + + def load(self): + self._manage_signals("unregister_command") + + def _manage_signals(self, action: str): + event = Event_Factory.create_event(action, command_name = "cut_to_temp_buffer", command = Handler, binding_mode = "held", @@ -30,7 +36,7 @@ class Plugin(PluginCode): self.emit_to("source_views", event) - event = Event_Factory.create_event("register_command", + event = Event_Factory.create_event(action, command_name = "paste_temp_buffer", command = Handler2, binding_mode = "held", diff --git a/plugins/code/commands/toggle_source_view/plugin.py b/plugins/code/commands/toggle_source_view/plugin.py index ff50e86..d8c55d2 100644 --- a/plugins/code/commands/toggle_source_view/plugin.py +++ b/plugins/code/commands/toggle_source_view/plugin.py @@ -26,6 +26,16 @@ class Plugin(PluginCode): self.emit_to("source_views", event) + def unload(self): + event = Event_Factory.create_event("unregister_command", + command_name = "toggle_source_view", + command = Handler, + binding_mode = "released", + binding = "h" + ) + + self.emit_to("source_views", event) + def run(self): ... diff --git a/plugins/code/completers/example_completer/plugin.py b/plugins/code/completers/example_completer/plugin.py index d95e8a4..4e5b6e3 100644 --- a/plugins/code/completers/example_completer/plugin.py +++ b/plugins/code/completers/example_completer/plugin.py @@ -35,5 +35,15 @@ class Plugin(PluginCode): ) self.emit_to("completion", event) + def unload(self): + event = Event_Factory.create_event( + "unregister_provider", + provider_name = "Example Completer" + ) + self.emit_to("completion", event) + + self.provider = None + del self.provider + def run(self): ... diff --git a/plugins/code/completers/python_completer/plugin.py b/plugins/code/completers/python_completer/plugin.py index ca85852..1bc9126 100644 --- a/plugins/code/completers/python_completer/plugin.py +++ b/plugins/code/completers/python_completer/plugin.py @@ -35,5 +35,15 @@ class Plugin(PluginCode): ) self.emit_to("completion", event) + def unload(self): + event = Event_Factory.create_event( + "register_provider", + provider_name = "Python Completer" + ) + self.emit_to("completion", event) + + self.provider = None + del self.provider + def run(self): ... diff --git a/plugins/code/completers/snippets_completer/plugin.py b/plugins/code/completers/snippets_completer/plugin.py index b303184..138571d 100644 --- a/plugins/code/completers/snippets_completer/plugin.py +++ b/plugins/code/completers/snippets_completer/plugin.py @@ -35,5 +35,15 @@ class Plugin(PluginCode): ) self.emit_to("completion", event) + def unload(self): + event = Event_Factory.create_event( + "unregister_provider", + provider_name = "Snippets Completer" + ) + self.emit_to("completion", event) + + self.provider = None + del self.provider + def run(self): ... diff --git a/plugins/code/completers/words_completer/plugin.py b/plugins/code/completers/words_completer/plugin.py index 2db9079..a1ab8a0 100644 --- a/plugins/code/completers/words_completer/plugin.py +++ b/plugins/code/completers/words_completer/plugin.py @@ -35,5 +35,15 @@ class Plugin(PluginCode): ) self.emit_to("completion", event) + def unload(self): + event = Event_Factory.create_event( + "unregister_provider", + provider_name = "Words Completer" + ) + self.emit_to("completion", event) + + self.provider = None + del self.provider + def run(self): ... diff --git a/plugins/code/event-watchers/extend_source_view_menu/__init__.py b/plugins/code/event-watchers/extend_source_view_menu/__init__.py new file mode 100644 index 0000000..e6b1b36 --- /dev/null +++ b/plugins/code/event-watchers/extend_source_view_menu/__init__.py @@ -0,0 +1,3 @@ +""" + Plugin Module +""" diff --git a/plugins/code/event-watchers/extend_source_view_menu/__main__.py b/plugins/code/event-watchers/extend_source_view_menu/__main__.py new file mode 100644 index 0000000..c2e27a7 --- /dev/null +++ b/plugins/code/event-watchers/extend_source_view_menu/__main__.py @@ -0,0 +1,3 @@ +""" + Plugin Package +""" diff --git a/plugins/code/event-watchers/extend_source_view_menu/manifest.json b/plugins/code/event-watchers/extend_source_view_menu/manifest.json new file mode 100644 index 0000000..eb66255 --- /dev/null +++ b/plugins/code/event-watchers/extend_source_view_menu/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Extend Source View Menu", + "author": "ITDominator", + "version": "0.0.1", + "support": "", + "requests": {} +} diff --git a/plugins/code/event-watchers/extend_source_view_menu/plugin.py b/plugins/code/event-watchers/extend_source_view_menu/plugin.py new file mode 100644 index 0000000..4907807 --- /dev/null +++ b/plugins/code/event-watchers/extend_source_view_menu/plugin.py @@ -0,0 +1,29 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from plugins.plugin_types import PluginCode + +from .source_view_menu import extend_source_view_menu + + + +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): + extend_source_view_menu(event.buffer, event.menu) + + def load(self): + ... + + def unload(self): + ... + + def run(self): + ... diff --git a/plugins/code/event-watchers/extend_source_view_menu/source_view_menu.py b/plugins/code/event-watchers/extend_source_view_menu/source_view_menu.py new file mode 100644 index 0000000..af84daf --- /dev/null +++ b/plugins/code/event-watchers/extend_source_view_menu/source_view_menu.py @@ -0,0 +1,68 @@ +# Python imports +import json + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports + + + +def on_case_handle(menuitem, buffer, action): + start_itr, \ + end_itr = buffer.get_selection_bounds() + data = buffer.get_text(start_itr, end_itr, False) + text = data + + if action == "on_all_upper": + text = data.upper() + elif action == "on_all_lower": + text = data.lower() + elif action == "on_invert": + text = data.swapcase() + elif action == "on_title": + text = data.title() + elif action == "on_title_strip": + text = data.title().replace("-", "").replace("_", "").replace(" ", "") + + buffer.begin_user_action() + buffer.delete(start_itr, end_itr) + buffer.insert(start_itr, text) + buffer.end_user_action() + + + +def extend_source_view_menu(buffer, menu): + if not buffer.get_selection_bounds(): return + + for child in menu.get_children(): + if not child.get_label() == "C_hange Case": continue + menu.remove(child) + + change_case_item = Gtk.MenuItem(label = "Change Case") + + case_menu = Gtk.Menu() + au_case_item = Gtk.MenuItem(label = "All Upper Case") + al_case_item = Gtk.MenuItem(label = "All Lower Case") + inver_case_item = Gtk.MenuItem(label = "Invert Case") + title_case_item = Gtk.MenuItem(label = "Title Case") + title_strip_case_item = Gtk.MenuItem(label = "Title Strip Case") + + au_case_item.connect("activate", on_case_handle, buffer, "on_all_upper") + al_case_item.connect("activate", on_case_handle, buffer, "on_all_lower") + inver_case_item.connect("activate", on_case_handle, buffer, "on_invert") + title_case_item.connect("activate", on_case_handle, buffer, "on_title") + title_strip_case_item.connect("activate", on_case_handle, buffer, "on_title_strip") + + case_menu.append(au_case_item) + case_menu.append(al_case_item) + case_menu.append(inver_case_item) + case_menu.append(title_case_item) + case_menu.append(title_strip_case_item) + change_case_item.set_submenu(case_menu) + + menu.append(change_case_item) diff --git a/plugins/code/event-watchers/file_state_watcher/plugin.py b/plugins/code/event-watchers/file_state_watcher/plugin.py index 43faf62..5603f2f 100644 --- a/plugins/code/event-watchers/file_state_watcher/plugin.py +++ b/plugins/code/event-watchers/file_state_watcher/plugin.py @@ -21,12 +21,15 @@ class Plugin(PluginCode): event.file.check_file_on_disk() if event.file.is_deleted(): - file_is_deleted(event) + file_is_deleted(event, self.emit) elif event.file.is_externally_modified(): - file_is_externally_modified(event) + file_is_externally_modified(event, self.emit) def load(self): ... + def unload(self): + ... + def run(self): ... diff --git a/plugins/code/event-watchers/file_state_watcher/watcher_checks.py b/plugins/code/event-watchers/file_state_watcher/watcher_checks.py index 6ec9fea..51dfa78 100644 --- a/plugins/code/event-watchers/file_state_watcher/watcher_checks.py +++ b/plugins/code/event-watchers/file_state_watcher/watcher_checks.py @@ -10,23 +10,23 @@ from libs.event_factory import Event_Factory, Code_Event_Types -def file_is_deleted(event): +def file_is_deleted(event, emit): event.file.was_deleted = True event = Event_Factory.create_event( "file_externally_deleted", file = event.file, buffer = event.buffer ) - self.emit(event) + emit(event) -def file_is_externally_modified(event): +def file_is_externally_modified(event, emit): # event = Event_Factory.create_event( # "file_externally_modified", # file = event.file, # buffer = event.buffer # ) -# self.emit(event) +# emit(event) ... diff --git a/plugins/code/event-watchers/prettify_json/plugin.py b/plugins/code/event-watchers/prettify_json/plugin.py index 4938f24..99afe8d 100644 --- a/plugins/code/event-watchers/prettify_json/plugin.py +++ b/plugins/code/event-watchers/prettify_json/plugin.py @@ -32,5 +32,8 @@ class Plugin(PluginCode): def load(self): ... + def unload(self): + ... + def run(self): ... diff --git a/plugins/code/event-watchers/prettify_json/prettify_json.py b/plugins/code/event-watchers/prettify_json/prettify_json.py index 0e62dc6..ad03e62 100644 --- a/plugins/code/event-watchers/prettify_json/prettify_json.py +++ b/plugins/code/event-watchers/prettify_json/prettify_json.py @@ -13,7 +13,7 @@ from gi.repository import Gtk def add_prettify_json(buffer, menu): - menu.append( Gtk.SeparatorMenuItem() ) + menu.append(separator) def on_prettify_json(menuitem, buffer): start_itr, \ diff --git a/plugins/code/language_server_clients/godot_lsp_client/__init__.py b/plugins/code/language_server_clients/godot_lsp_client/__init__.py new file mode 100644 index 0000000..d36fa8c --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/__init__.py @@ -0,0 +1,3 @@ +""" + Pligin Module +""" diff --git a/plugins/code/language_server_clients/godot_lsp_client/__main__.py b/plugins/code/language_server_clients/godot_lsp_client/__main__.py new file mode 100644 index 0000000..a576329 --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/__main__.py @@ -0,0 +1,3 @@ +""" + Pligin Package +""" diff --git a/plugins/code/language_server_clients/godot_lsp_client/config/lsp-server-config.json b/plugins/code/language_server_clients/godot_lsp_client/config/lsp-server-config.json new file mode 100644 index 0000000..a5268b7 --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/config/lsp-server-config.json @@ -0,0 +1,52 @@ +{ + "info": "https://github.com/godotengine/godot/blob/4a280218fcfdd69408cceb74577c9e69086be23a/editor/settings/editor_settings.cpp#L1132", + "command": "lsp-ws-proxy -- godot", + "alt-command": "godot", + "alt-command2": "lsp-ws-proxy --listen 4114 -- godot --headless", + "alt-command3": "godot --headless --lsp-port 7766", + "socket": "ws://127.0.0.1:9999/gdscript", + "socket-two": "ws://127.0.0.1:9999/?name=gdscript", + "initialization-options": { + "processId": , + "clientInfo": { + "name": "Godot", + "version": "4.4" + }, + "rootUri": "file://{workspace.folder}", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true + } + }, + "textDocument": { + "synchronization": { + "dynamicRegistration": true, + "willSave": false, + "didSave": true, + "willSaveWaitUntil": false + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "hover": { + "dynamicRegistration": true + }, + "definition": { + "dynamicRegistration": true + }, + "references": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true + } + } + }, + "trace": "off" + } +} diff --git a/plugins/code/language_server_clients/godot_lsp_client/manifest.json b/plugins/code/language_server_clients/godot_lsp_client/manifest.json new file mode 100644 index 0000000..b9cac11 --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "Godot LSP Client", + "author": "ITDominator", + "version": "0.0.1", + "support": "", + "autoload": false, + "requests": {} +} diff --git a/plugins/code/language_server_clients/godot_lsp_client/plugin.py b/plugins/code/language_server_clients/godot_lsp_client/plugin.py new file mode 100644 index 0000000..e8d484e --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/plugin.py @@ -0,0 +1,44 @@ +# Python imports +from os import path + +# Lib imports +import gi + +from gi.repository import GLib + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from plugins.plugin_types import PluginCode + +from .response_handler import GodotHandler + + + +class Plugin(PluginCode): + def __init__(self): + super(Plugin, self).__init__() + + + def _controller_message(self, event: Code_Event_Types.CodeEvent): + ... + + def load(self): + dirPth = path.dirname( path.realpath(__file__) ) + with open(f"{dirPth}/config/lsp-server-config.json", "r") as f: + config = f.read() + event = Event_Factory.create_event("register_lsp_client", + lang_id = "gdscript", + lang_config = config, + handler = GodotHandler + ) + self.emit_to("lsp_manager", event) + + def unload(self): + event = Event_Factory.create_event("unregister_lsp_client", + lang_id = "gdscript" + ) + self.emit_to("lsp_manager", event) + + def run(self): + ... diff --git a/plugins/code/language_server_clients/godot_lsp_client/response_handler/__init__.py b/plugins/code/language_server_clients/godot_lsp_client/response_handler/__init__.py new file mode 100644 index 0000000..d5617e6 --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/response_handler/__init__.py @@ -0,0 +1 @@ +from .python import PythonHandler \ No newline at end of file diff --git a/plugins/code/language_server_clients/godot_lsp_client/response_handler/python.py b/plugins/code/language_server_clients/godot_lsp_client/response_handler/python.py new file mode 100644 index 0000000..9c1273f --- /dev/null +++ b/plugins/code/language_server_clients/godot_lsp_client/response_handler/python.py @@ -0,0 +1,12 @@ +# Python imports + +# Lib imports + +# Application imports +from lsp_manager.response_handlers.default import DefaultHandler + + + +class GodotHandler(DefaultHandler): + """Uses default handling, can override if Godot needs special logic.""" + ... diff --git a/plugins/code/language_server_clients/java_lsp_client/manifest.json b/plugins/code/language_server_clients/java_lsp_client/manifest.json index 93f7049..76cfaf8 100644 --- a/plugins/code/language_server_clients/java_lsp_client/manifest.json +++ b/plugins/code/language_server_clients/java_lsp_client/manifest.json @@ -3,5 +3,6 @@ "author": "ITDominator", "version": "0.0.1", "support": "", + "autoload": false, "requests": {} } diff --git a/plugins/code/language_server_clients/java_lsp_client/plugin.py b/plugins/code/language_server_clients/java_lsp_client/plugin.py index e7dd8c2..2320324 100644 --- a/plugins/code/language_server_clients/java_lsp_client/plugin.py +++ b/plugins/code/language_server_clients/java_lsp_client/plugin.py @@ -33,5 +33,11 @@ class Plugin(PluginCode): ) self.emit_to("lsp_manager", event) + def unload(self): + event = Event_Factory.create_event("unregister_lsp_client", + lang_id = "java" + ) + self.emit_to("lsp_manager", event) + def run(self): ... diff --git a/plugins/code/language_server_clients/lsp_manager/lsp_manager.py b/plugins/code/language_server_clients/lsp_manager/lsp_manager.py index effa67e..cbfce28 100644 --- a/plugins/code/language_server_clients/lsp_manager/lsp_manager.py +++ b/plugins/code/language_server_clients/lsp_manager/lsp_manager.py @@ -40,7 +40,7 @@ class LSPManager(ControllerBase): self.lsp_manager_ui.connect('close-client', self._on_close_client) def _do_bind_mapping(self): - self.response_cache.set_lsp_client(self.lsp_manager_client) + self.response_cache.set_lsp_manager_client(self.lsp_manager_client) self.provider.response_cache = self.response_cache self.response_registry.set_event_hub( self.emit, self.emit_to, self.provider @@ -52,6 +52,7 @@ class LSPManager(ControllerBase): self.lsp_manager_ui.add_client_listing(event.lang_id, event.lang_config) elif isinstance(event, Code_Event_Types.UnregisterLspClientEvent): self.response_registry.unregister_handler(event.lang_id) + self.lsp_manager_ui.remove_client_listing(event.lang_id) def _on_create_client(self, ui, lang_id: str, workspace_uri: str) -> bool: init_opts = ui.get_init_opts(lang_id) @@ -66,6 +67,10 @@ class LSPManager(ControllerBase): ui.toggle_client_buttons(show_close=False) return result + def handle_destroy(self): + self.lsp_manager_ui.disconnect_by_func(self._on_create_client) + self.lsp_manager_ui.disconnect_by_func(self._on_close_client) + def create_client( self, lang_id: str = "python", diff --git a/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py b/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py index 79f6db5..740dd0d 100644 --- a/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py +++ b/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py @@ -41,7 +41,8 @@ class LSPManagerUI(Gtk.Dialog): self.set_hexpand(True) def _setup_signals(self): - self.connect("show", self._show) + self.connect("show", self._handle_show) + self.connect("destroy", self._handle_destroy) def _subscribe_to_events(self): ... @@ -68,7 +69,7 @@ class LSPManagerUI(Gtk.Dialog): self.path_bttn.connect("file-set", self._file_set) self.combo_box.connect("changed", self._on_combo_changed) - self.hide_bttn.connect("clicked", lambda widget: self.hide()) + self.hide_bttn_id = self.hide_bttn.connect("clicked", lambda widget: self.hide()) self.create_client_bttn.connect("clicked", self._create_client, self.close_client_bttn) self.close_client_bttn.connect("clicked", self._close_client, self.create_client_bttn) @@ -92,9 +93,18 @@ class LSPManagerUI(Gtk.Dialog): self.close_client_bttn.hide() bttn_box.hide() - def _show(self, widget): + def _handle_show(self, widget): GLib.idle_add(self.path_entry.grab_focus) + def _handle_destroy(self, widget): + self.disconnect_by_func(self._show) + self.disconnect_by_func(self._handle_destroy) + self.path_bttn.disconnect_by_func(self._file_set) + self.combo_box.disconnect_by_func(self._on_combo_changed) + self.hide_bttn.disconnect(self.hide_bttn_id) + self.create_client_bttn.disconnect_by_func(self._create_client) + self.close_client_bttn.disconnect_by_func(self._close_client) + def _map_resize(self, widget, parent): parent_x, parent_y = parent.get_position() parent_width, parent_height = parent.get_size() @@ -163,7 +173,10 @@ class LSPManagerUI(Gtk.Dialog): buffer.set_text(json_str, -1) def map_parent_resize_event(self, parent): - parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent)) + self.size_allocate_id = parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent)) + + def unmap_parent_resize_event(self, parent): + parent.disconnect(self.size_allocate_id) def set_source_view(self, source_view): scrolled_win = Gtk.ScrolledWindow() @@ -187,6 +200,17 @@ class LSPManagerUI(Gtk.Dialog): self.combo_box.append_text(lang_id) self.client_configs[lang_id] = lang_config + def remove_client_listing(self, lang_id: str): + model = self.combo_box.get_model() + + for i, row in enumerate(model): + if row[0] == lang_id: # assuming text is in column 0 + self.combo_box.remove(i) + break + + if lang_id in self.client_configs: + del self.client_configs[lang_id] + def get_init_opts(self, lang_id: str) -> dict: if not lang_id or lang_id not in self.client_configs: return {} diff --git a/plugins/code/language_server_clients/lsp_manager/manifest.json b/plugins/code/language_server_clients/lsp_manager/manifest.json index 830009d..5536e7f 100644 --- a/plugins/code/language_server_clients/lsp_manager/manifest.json +++ b/plugins/code/language_server_clients/lsp_manager/manifest.json @@ -3,6 +3,6 @@ "author": "ITDominator", "version": "0.0.1", "support": "", - "pre_launch": true, + "autoload": false, "requests": {} } diff --git a/plugins/code/language_server_clients/lsp_manager/plugin.py b/plugins/code/language_server_clients/lsp_manager/plugin.py index 75f9c50..4f8612b 100644 --- a/plugins/code/language_server_clients/lsp_manager/plugin.py +++ b/plugins/code/language_server_clients/lsp_manager/plugin.py @@ -62,6 +62,31 @@ class Plugin(PluginCode): source_view = event.response lsp_manager.lsp_manager_ui.set_source_view(source_view) + def unload(self): + Event_Factory.unregister_events( lsp_events.__dict__.items() ) + + self.unregister_controller("lsp_manager") + + window = self.request_ui_element("main-window") + + lsp_manager.lsp_manager_ui.unmap_parent_resize_event(window) + + event = Event_Factory.create_event("unregister_command", + command_name = "LSP Manager", + command = Handler, + binding_mode = "released", + binding = ["l", "g", "i"] + ) + self.emit_to("source_views", event) + + event = Event_Factory.create_event( + "unregister_provider", + provider_name = "LSP Completer" + ) + self.emit_to("completion", event) + + lsp_manager.handle_destroy() + def run(self): ... diff --git a/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py b/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py index 0ef02fa..2686d91 100644 --- a/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py +++ b/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py @@ -15,27 +15,27 @@ class ProviderResponseCache(ProviderResponseCacheBase): def __init__(self): super(ProviderResponseCache, self).__init__() - self.matchers: dict = {} - self._lsp_client = None + self.matchers: dict = {} + self.lsp_manager_client = None - def set_lsp_client(self, lsp_client): - self._lsp_client = lsp_client + def set_lsp_manager_client(self, lsp_client): + self.lsp_manager_client = lsp_client def process_file_load(self, event): - if self._lsp_client: - self._lsp_client.process_file_load(event) + if self.lsp_manager_client: + self.lsp_manager_client.process_file_load(event) def process_file_close(self, event): - if self._lsp_client: - self._lsp_client.process_file_close(event) + if self.lsp_manager_client: + self.lsp_manager_client.process_file_close(event) def process_file_save(self, event): - if self._lsp_client: - self._lsp_client.process_file_save(event) + if self.lsp_manager_client: + self.lsp_manager_client.process_file_save(event) def process_file_change(self, event): - if self._lsp_client: - self._lsp_client.process_file_change(event) + if self.lsp_manager_client: + self.lsp_manager_client.process_file_change(event) def filter(self, word: str) -> list[dict]: return [] diff --git a/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py b/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py index c871486..09403fa 100644 --- a/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py +++ b/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py @@ -33,7 +33,7 @@ class ResponseRegistry: def register_handler(self, lang_id: str, handler_cls: type[BaseHandler]): self._lang_handlers[lang_id] = handler_cls - def unregister_handler(self, lang_id: str, handler_cls: type[BaseHandler]): + def unregister_handler(self, lang_id: str): del self._lang_handlers[lang_id] def get_handler(self, lang_id: str = "", method: str = ""): diff --git a/plugins/code/language_server_clients/python_lsp_client/manifest.json b/plugins/code/language_server_clients/python_lsp_client/manifest.json index beccbb5..dff6d04 100644 --- a/plugins/code/language_server_clients/python_lsp_client/manifest.json +++ b/plugins/code/language_server_clients/python_lsp_client/manifest.json @@ -3,5 +3,6 @@ "author": "ITDominator", "version": "0.0.1", "support": "", + "autoload": false, "requests": {} } diff --git a/plugins/code/language_server_clients/python_lsp_client/plugin.py b/plugins/code/language_server_clients/python_lsp_client/plugin.py index a579da6..5d4f6cf 100644 --- a/plugins/code/language_server_clients/python_lsp_client/plugin.py +++ b/plugins/code/language_server_clients/python_lsp_client/plugin.py @@ -33,5 +33,11 @@ class Plugin(PluginCode): ) self.emit_to("lsp_manager", event) + def unload(self): + event = Event_Factory.create_event("unregister_lsp_client", + lang_id = "python" + ) + self.emit_to("lsp_manager", event) + def run(self): ... diff --git a/plugins/code/ui/code_minimap/plugin.py b/plugins/code/ui/code_minimap/plugin.py index 2b13d74..5c110ef 100644 --- a/plugins/code/ui/code_minimap/plugin.py +++ b/plugins/code/ui/code_minimap/plugin.py @@ -28,5 +28,12 @@ class Plugin(PluginCode): editors_container = self.request_ui_element("editors-container") editors_container.add( code_minimap ) + event = Event_Factory.create_event("get_active_view") + self.emit_to("source_views", event) + code_minimap.set_smini_view(event.response) + + def unload(self): + code_minimap.destroy() + def run(self): ... diff --git a/plugins/code/ui/info_bar/info_bar_widget.py b/plugins/code/ui/info_bar/info_bar_widget.py index 40c48ed..a5d88e2 100644 --- a/plugins/code/ui/info_bar/info_bar_widget.py +++ b/plugins/code/ui/info_bar/info_bar_widget.py @@ -35,7 +35,6 @@ class InfoBarWidget(Gtk.Box): def _subscribe_to_events(self): ... - def _load_widgets(self): self.path_label = Gtk.Label(label = "...") self.line_char_label = Gtk.Label(label = "1:0") @@ -92,5 +91,3 @@ class InfoBarWidget(Gtk.Box): encoding_type = "utf-8" if not encoding_type else encoding_type self.encoding_label.set_text(encoding_type) - - diff --git a/plugins/code/ui/info_bar/plugin.py b/plugins/code/ui/info_bar/plugin.py index 5fe0b15..f1c08e3 100644 --- a/plugins/code/ui/info_bar/plugin.py +++ b/plugins/code/ui/info_bar/plugin.py @@ -28,5 +28,8 @@ class Plugin(PluginCode): header = self.request_ui_element("header-container") header.add( info_bar_widget ) + def unload(self): + info_bar_widget.destroy() + def run(self): ... diff --git a/plugins/code/ui/markdown_preview/manifest.json b/plugins/code/ui/markdown_preview/manifest.json index 6acb594..5b0a0be 100644 --- a/plugins/code/ui/markdown_preview/manifest.json +++ b/plugins/code/ui/markdown_preview/manifest.json @@ -3,5 +3,6 @@ "author": "ITDominator", "version": "0.0.1", "support": "", + "autoload": false, "requests": {} } diff --git a/plugins/code/ui/markdown_preview/markdown_preview.py b/plugins/code/ui/markdown_preview/markdown_preview.py index 709d9ac..fc1b5e0 100644 --- a/plugins/code/ui/markdown_preview/markdown_preview.py +++ b/plugins/code/ui/markdown_preview/markdown_preview.py @@ -7,6 +7,7 @@ gi.require_version('Gdk', '3.0') from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GLib # Application imports from core.widgets.webkit.webkit_ui import WebkitUI @@ -47,6 +48,7 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin): def _setup_signals(self): self.connect("hide", self._handle_hide) self.connect("show", self._handle_show) + self.connect("destroy", self._handle_destroy) def _load_widgets(self): box = Gtk.Box() @@ -71,7 +73,6 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin): box.set_orientation(Gtk.Orientation.VERTICAL) self.start_stop_bttn.connect("clicked", self._tggle_preview_updates) - settings_bttn.connect("clicked", self._handle_settings) bttn_box.pack_end(self.start_stop_bttn, expand = False, fill = False, padding = 1) bttn_box.pack_end(settings_bttn, expand = False, fill = False, padding = 1) @@ -98,5 +99,8 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin): def _tggle_preview_updates(self, widget): self.is_preview_paused = not self.is_preview_paused - def _handle_settings(self, widget): - ... + def _handle_destroy(self): + self.disconnect_by_func(self._handle_hide) + self.disconnect_by_func(self._handle_show) + self.disconnect_by_func(self._handle_destroy) + self.start_stop_bttn.disconnect_by_func(self._tggle_preview_updates) diff --git a/plugins/code/ui/markdown_preview/plugin.py b/plugins/code/ui/markdown_preview/plugin.py index 4fb486d..f0748a5 100644 --- a/plugins/code/ui/markdown_preview/plugin.py +++ b/plugins/code/ui/markdown_preview/plugin.py @@ -51,6 +51,18 @@ class Plugin(PluginCode): self.emit_to("source_views", event) + def unload(self): + event = Event_Factory.create_event("unregister_command", + command_name = "tggle_markdown_preview", + command = Handler, + binding_mode = "released", + binding = "m" + ) + + self.emit_to("source_views", event) + + markdown_preview.destroy() + def run(self): ... @@ -67,4 +79,14 @@ class Handler: if not markdown_preview.can_hide: markdown_preview.can_hide = True - markdown_preview.popdown() if markdown_preview.is_visible() else markdown_preview.popup() + if markdown_preview.is_visible(): + markdown_preview.popdown() + return + + file = view.command.exec("get_current_file") + if not file or not file.get_location(): return + buffer = view.get_buffer() + + markdown_preview.popup() + markdown_preview.fpath = file.get_location().get_path() + markdown_preview._do_markdown_translate(buffer) diff --git a/plugins/code/ui/search_replace/mixins/search_replace_mixin.py b/plugins/code/ui/search_replace/mixins/search_replace_mixin.py index 5b6627c..b0e38bf 100644 --- a/plugins/code/ui/search_replace/mixins/search_replace_mixin.py +++ b/plugins/code/ui/search_replace/mixins/search_replace_mixin.py @@ -17,19 +17,17 @@ class SearchReplaceMixin(SearchMixin, ReplaceMixin): search_text = entry.get_text() buffer = self.active_view.get_buffer() - if buffer.get_has_selection() and not search_text: - if not self.mode_bttn_box.in_selection: - start_itr, end_itr = buffer.get_selection_bounds() + if not buffer.get_has_selection() and search_text: return + if self.mode_bttn_box.in_selection: return - entry.set_text( - buffer.get_text( - start_itr, - end_itr, - include_hidden_chars = False - ) - ) - - return + start_itr, end_itr = buffer.get_selection_bounds() + entry.set_text( + buffer.get_text( + start_itr, + end_itr, + include_hidden_chars = False + ) + ) def _find_entry_search_change(self, entry): search_text = entry.get_text() diff --git a/plugins/code/ui/search_replace/mode_buttons.py b/plugins/code/ui/search_replace/mode_buttons.py index dd1c59a..b758c18 100644 --- a/plugins/code/ui/search_replace/mode_buttons.py +++ b/plugins/code/ui/search_replace/mode_buttons.py @@ -33,44 +33,44 @@ class ModeButtons(Gtk.ButtonBox): ctx.add_class("search-replace-mode-buttons") def _setup_signals(self): - ... + self.connect("destroy", self._handle_destroy) def _load_widgets(self): - use_regex_bttn = Gtk.ToggleButton(label = ".*") - match_case_bttn = Gtk.ToggleButton(label = "Aa") - in_selection_bttn = Gtk.ToggleButton() - whole_word_bttn = Gtk.ToggleButton() - hide_bttn = Gtk.Button(label = "X") + self.use_regex_bttn = Gtk.ToggleButton(label = ".*") + self.match_case_bttn = Gtk.ToggleButton(label = "Aa") + self.in_selection_bttn = Gtk.ToggleButton() + self.whole_word_bttn = Gtk.ToggleButton() + self.hide_bttn = Gtk.Button(label = "X") - use_regex_bttn.set_sensitive(False) + self.use_regex_bttn.set_sensitive(False) - use_regex_bttn.set_tooltip_text("Use Regex") - match_case_bttn.set_tooltip_text("Match Case") - in_selection_bttn.set_tooltip_text("Only In Selection") - whole_word_bttn.set_tooltip_text("Whole Word") + self.use_regex_bttn.set_tooltip_text("Use Regex") + self.match_case_bttn.set_tooltip_text("Match Case") + self.in_selection_bttn.set_tooltip_text("Only In Selection") + self.whole_word_bttn.set_tooltip_text("Whole Word") - use_regex_bttn.connect("toggled", self._toggled_button, "use_regex") - match_case_bttn.connect("toggled", self._toggled_button, "match_case") - in_selection_bttn.connect("toggled", self._toggled_button, "in_selection") - whole_word_bttn.connect("toggled", self._toggled_button, "whole_word") + self.use_regex_bttn.connect("toggled", self._toggled_button, "use_regex") + self.match_case_bttn.connect("toggled", self._toggled_button, "match_case") + self.in_selection_bttn.connect("toggled", self._toggled_button, "in_selection") + self.whole_word_bttn.connect("toggled", self._toggled_button, "whole_word") - hide_bttn.connect( + self.hide_bttn_id = self.hide_bttn.connect( "clicked", lambda widget: self.get_parent().hide() ) - in_selection_bttn.set_image( + self.in_selection_bttn.set_image( Gtk.Image.new_from_file("images/only-in-selection.png") ) - whole_word_bttn.set_image( + self.whole_word_bttn.set_image( Gtk.Image.new_from_file("images/whole-word.png") ) - self.add(use_regex_bttn) - self.add(match_case_bttn) - self.add(in_selection_bttn) - self.add(whole_word_bttn) - self.add(hide_bttn) + self.add(self.use_regex_bttn) + self.add(self.match_case_bttn) + self.add(self.in_selection_bttn) + self.add(self.whole_word_bttn) + self.add(self.hide_bttn) def _toggled_button(self, toggle_button, mode: str): setattr(self, mode, not getattr(self, mode)) @@ -79,4 +79,12 @@ class ModeButtons(Gtk.ButtonBox): def request_update(self): raise ModeException("Must by 'monkey' patched from search_replace.py") + def _handle_destroy(self, widget): + self.disconnect_by_func(self._handle_destroy) + self.use_regex_bttn.disconnect_by_func(self._toggled_button) + self.match_case_bttn.disconnect_by_func(self._toggled_button) + self.in_selection_bttn.disconnect_by_func(self._toggled_button) + self.whole_word_bttn.disconnect_by_func(self._toggled_button) + + self.hide_bttn.disconnect(self.hide_bttn_id) diff --git a/plugins/code/ui/search_replace/plugin.py b/plugins/code/ui/search_replace/plugin.py index af76f11..9fdb97c 100644 --- a/plugins/code/ui/search_replace/plugin.py +++ b/plugins/code/ui/search_replace/plugin.py @@ -49,6 +49,17 @@ class Plugin(PluginCode): self.emit_to("source_views", event) + def unload(self): + event = Event_Factory.create_event("unregister_command", + command_name = "search_replace", + command = Handler, + binding_mode = "released", + binding = ["f", "r"] + ) + + self.emit_to("source_views", event) + search_replace.destroy() + def run(self): ... @@ -63,4 +74,7 @@ class Handler: logger.debug("Command: Search/Replace") search_replace.last_key = args[0] + if not search_replace.active_view: + search_replace.active_view = view + search_replace.hide() if search_replace.is_visible() else search_replace.show() diff --git a/plugins/code/ui/search_replace/search_replace.py b/plugins/code/ui/search_replace/search_replace.py index 6380a63..0321ba9 100644 --- a/plugins/code/ui/search_replace/search_replace.py +++ b/plugins/code/ui/search_replace/search_replace.py @@ -22,7 +22,7 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): self.active_view = None self.highlight_tag: Gtk.TextTag = None self.matches: list[tuple] = [] - self.last_key: str = None + self.last_key: str = "f" self._setup_styling() self._setup_signals() @@ -48,6 +48,7 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): def _setup_signals(self): self.connect("show", self._handle_show) self.connect("hide", self._handle_hide) + self.connect("destroy", self._handle_destroy) def _load_widgets(self): self.status_lbl = Gtk.Label(label = "Find in Current Buffer") @@ -57,10 +58,10 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): self.find_entry = Gtk.SearchEntry() self.replace_entry = Gtk.SearchEntry() - find_bttn = Gtk.Button(label = "Find") - find_all_bttn = Gtk.Button(label = "Find All") - replace_bttn = Gtk.Button(label = "Replace") - replace_all_bttn = Gtk.Button(label = "Replace All") + self.find_bttn = Gtk.Button(label = "Find") + self.find_all_bttn = Gtk.Button(label = "Find All") + self.replace_bttn = Gtk.Button(label = "Replace") + self.replace_all_bttn = Gtk.Button(label = "Replace All") self.find_entry.set_hexpand(True) self.replace_entry.set_hexpand(True) @@ -81,21 +82,24 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): self.replace_entry.connect("key-release-event", self._replace_entry_key_release_event) self.replace_entry.connect("activate", self._replace_entry_activate) - find_bttn.connect( + self.find_handler_id = self.find_bttn.connect( "clicked", lambda button: self._find_entry_next_match(self.find_entry) ) - find_all_bttn.connect( + + self.find_all_handler_id = self.find_all_bttn.connect( "clicked", lambda button: self._find_entry_search_change(self.find_entry) ) - replace_bttn.connect( + + self.replace_handler_id = self.replace_bttn.connect( "clicked", lambda button: self._replace_entry_activate(self.replace_entry) ) - replace_all_bttn.connect( + + self.replace_all_handler_id = self.replace_all_bttn.connect( "clicked", - lambda button: self._replace_all_activate(self.replace_entry) + lambda button: self._replace_all_activate(self.replace_entry) ) self.attach(child = self.status_lbl, left = 0, top = 0, width = 3, height = 1) @@ -103,12 +107,12 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): self.attach(child = self.mode_bttn_box, left = 5, top = 0, width = 2, height = 1) self.attach(child = self.find_entry, left = 0, top = 1, width = 5, height = 1) - self.attach(child = find_bttn, left = 5, top = 1, width = 1, height = 1) - self.attach(child = find_all_bttn, left = 6, top = 1, width = 1, height = 1) + self.attach(child = self.find_bttn, left = 5, top = 1, width = 1, height = 1) + self.attach(child = self.find_all_bttn, left = 6, top = 1, width = 1, height = 1) self.attach(child = self.replace_entry, left = 0, top = 2, width = 5, height = 1) - self.attach(child = replace_bttn, left = 5, top = 2, width = 1, height = 1) - self.attach(child = replace_all_bttn, left = 6, top = 2, width = 1, height = 1) + self.attach(child = self.replace_bttn, left = 5, top = 2, width = 1, height = 1) + self.attach(child = self.replace_all_bttn, left = 6, top = 2, width = 1, height = 1) def _handle_show(self, widget): @@ -117,9 +121,9 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): self.find_entry.grab_focus() return - self.replace_entry.grab_focus() # Fake focus call to prompt search - self._find_entry_focus_in_event(self.find_entry, None) + self._find_entry_focus_in_event(self.find_entry, None) + self.replace_entry.grab_focus() def _handle_hide(self, widget): if not self.active_view: return @@ -197,3 +201,22 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin): start_itr, end_itr = buffer.get_bounds() buffer.remove_tag(self.highlight_tag, start_itr, end_itr) + def _handle_destroy(self, widget): + self.disconnect_by_func(self._handle_show) + self.disconnect_by_func(self._handle_hide) + self.disconnect_by_func(self._handle_destroy) + + self.find_entry.disconnect_by_func(self._find_entry_focus_in_event) + self.find_entry.disconnect_by_func(self._find_entry_key_release_event) + self.find_entry.disconnect_by_func(self._find_entry_activate) + self.find_entry.disconnect_by_func(self._find_entry_search_change) + self.find_entry.disconnect_by_func(self._find_entry_next_match) + self.find_entry.disconnect_by_func(self._find_entry_previous_match) + + self.replace_entry.disconnect_by_func(self._replace_entry_key_release_event) + self.replace_entry.disconnect_by_func(self._replace_entry_activate) + + self.find_bttn.disconnect(self.find_handler_id) + self.find_all_bttn.disconnect(self.find_all_handler_id) + self.replace_bttn.disconnect(self.replace_handler_id) + self.replace_all_bttn.disconnect(self.replace_all_handler_id) diff --git a/plugins/code/ui/tabs_bar/plugin.py b/plugins/code/ui/tabs_bar/plugin.py index 2cec4d4..1911b6b 100644 --- a/plugins/code/ui/tabs_bar/plugin.py +++ b/plugins/code/ui/tabs_bar/plugin.py @@ -20,13 +20,27 @@ class Plugin(PluginCode): ... def load(self): - tabs_controller = TabsController() + self.tabs_controller = TabsController() code_container = self.request_ui_element("code-container") - self.register_controller("tabs", tabs_controller) + self.register_controller("tabs", self.tabs_controller) - code_container.add( tabs_controller.tabs_widget ) - code_container.reorder_child(tabs_controller.tabs_widget, 0) + code_container.add( self.tabs_controller.tabs_widget ) + code_container.reorder_child(self.tabs_controller.tabs_widget, 0) + + event = Event_Factory.create_event("get_files") + self.emit_to("files", event) + for file in event.response: + self.tabs_controller.add_tab(file) + + def unload(self): + self.unregister_controller("tabs") + self.tabs_controller.unload_tabs() + self.tabs_controller.tabs_widget.destroy() + + self.tabs_controller.tabs_widget = None + self.tabs_controller = None + del self.tabs_controller def run(self): ... diff --git a/plugins/code/ui/tabs_bar/tab_widget.py b/plugins/code/ui/tabs_bar/tab_widget.py index 4796548..729cc29 100644 --- a/plugins/code/ui/tabs_bar/tab_widget.py +++ b/plugins/code/ui/tabs_bar/tab_widget.py @@ -32,7 +32,6 @@ class TabWidget(Gtk.Box): self.set_orientation(0) self.set_hexpand(False) self.set_vexpand(False) - self.set_can_focus(False) self.set_size_request(-1, 12) def _setup_signals(self): @@ -44,10 +43,6 @@ class TabWidget(Gtk.Box): self.close_bttn = Gtk.Button() icon = Gtk.Image(stock = Gtk.STOCK_CLOSE) - self.event_box.set_can_focus(False) - self.label.set_can_focus(False) - self.close_bttn.set_can_focus(False) - self.event_box.set_above_child(True) ctx = self.label.get_style_context() ctx.add_class("tab-label") diff --git a/plugins/code/ui/tabs_bar/tabs_controller.py b/plugins/code/ui/tabs_bar/tabs_controller.py index a30ac64..d5cb48f 100644 --- a/plugins/code/ui/tabs_bar/tabs_controller.py +++ b/plugins/code/ui/tabs_bar/tabs_controller.py @@ -16,7 +16,6 @@ from .tab_widget import TabWidget - class TabsController(ControllerBase): def __init__(self): super(TabsController, self).__init__() @@ -35,7 +34,7 @@ class TabsController(ControllerBase): 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) + self.add_tab(event.file) elif isinstance(event, Code_Event_Types.PoppedFileEvent): ... elif isinstance(event, Code_Event_Types.RemovedFileEvent): @@ -53,11 +52,11 @@ class TabsController(ControllerBase): break - def add_tab(self, event: Code_Event_Types.AddedNewFileEvent): + def add_tab(self, file): tab = TabWidget() - tab.file = event.file + tab.file = file - tab.label.set_label(event.file.fname) + tab.label.set_label(file.fname) self.tabs_widget.append_page(Gtk.Separator(), tab) tab.show_all() @@ -73,3 +72,13 @@ class TabsController(ControllerBase): ) break + + def unload_tabs(self): + for page_widget in self.tabs_widget.get_children(): + tab = self.tabs_widget.get_tab_label(page_widget) + + tab.clear_signals_and_data() + self.tabs_widget.remove_page( + self.tabs_widget.page_num(page_widget) + ) + diff --git a/plugins/code/ui/tabs_bar/tabs_widget.py b/plugins/code/ui/tabs_bar/tabs_widget.py index a32826c..9a8b18d 100644 --- a/plugins/code/ui/tabs_bar/tabs_widget.py +++ b/plugins/code/ui/tabs_bar/tabs_widget.py @@ -33,6 +33,7 @@ class TabsWidget(Gtk.Notebook): self.connect("page-added", self._page_added) self.switch_page_id = \ self.connect_after("switch-page", self._switch_page) + self.connect("destroy", self._handle_destroy) def _subscribe_to_events(self): ... @@ -81,25 +82,38 @@ class TabsWidget(Gtk.Notebook): ) def create_menu(self, page_widget) -> Gtk.Menu: - context_menu = Gtk.Menu() + context_menu = Gtk.Menu() + close_submenu = Gtk.Menu() + save_item = Gtk.MenuItem(label = "Save") + save_as_item = Gtk.MenuItem(label = "Save As") - 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_actions_menu = Gtk.MenuItem(label = "Close Actions") + 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) + save_item.connect("activate", self.save_item, page_widget) + save_as_item.connect("activate", self.save_as_item, page_widget) + + 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) + 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) + close_submenu.append(close_item) + close_submenu.append(close_left_item) + close_submenu.append(close_right_item) + close_submenu.append(close_other_item) + close_submenu.append(close_all_item) + + close_actions_menu.set_submenu(close_submenu) + + context_menu.append(save_item) + context_menu.append(save_as_item) + context_menu.append(close_actions_menu) context_menu.show_all() @@ -142,6 +156,14 @@ class TabsWidget(Gtk.Notebook): break + def save_item(self, menu_item, page_widget): + tab = self.get_tab_label(page_widget) + tab.file.save() + + def save_as_item(self, menu_item, page_widget): + tab = self.get_tab_label(page_widget) + tab.file.save_as() + def close_item(self, menu_item, page_widget): tab = self.get_tab_label(page_widget) tab.close_bttn.clicked() @@ -176,3 +198,9 @@ class TabsWidget(Gtk.Notebook): for widget in children[ : ]: tab = self.get_tab_label(widget) tab.close_bttn.clicked() + + def _handle_destroy(self, widget): + self.disconnect_by_func(self._page_added) + self.disconnect_by_func(self._switch_page) + self.disconnect_by_func(self._handle_destroy) + diff --git a/plugins/code/ui/telescope/list_box.py b/plugins/code/ui/telescope/list_box.py index bc64981..3b98ca8 100644 --- a/plugins/code/ui/telescope/list_box.py +++ b/plugins/code/ui/telescope/list_box.py @@ -115,9 +115,9 @@ class ListBox(Gtk.ListBox): self.select_row(next_row) - def add_row(self, event): - label = Gtk.Label(label = event.file.fname) - label.file = event.file + def add_row(self, file): + label = Gtk.Label(label = file.fname) + label.file = file label.show() self.add(label) diff --git a/plugins/code/ui/telescope/plugin.py b/plugins/code/ui/telescope/plugin.py index 1ce6440..885ab3f 100644 --- a/plugins/code/ui/telescope/plugin.py +++ b/plugins/code/ui/telescope/plugin.py @@ -25,7 +25,7 @@ class Plugin(PluginCode): if isinstance(event, Code_Event_Types.FocusedViewEvent): ... elif isinstance(event, Code_Event_Types.AddedNewFileEvent): - telescope.list_box.add_row(event) + telescope.list_box.add_row(event.file) elif isinstance(event, Code_Event_Types.RemovedFileEvent): telescope.list_box.remove_row(event) elif isinstance(event, Code_Event_Types.FilePathSetEvent): @@ -46,7 +46,7 @@ class Plugin(PluginCode): ) self.emit_to("source_views", event) - event = Event_Factory.create_event( + event = Event_Factory.create_event( "create_source_view", state = SourceViewStates.INDEPENDENT ) @@ -55,12 +55,38 @@ class Plugin(PluginCode): source_view = event.response telescope.set_source_view(source_view) - event = Event_Factory.create_event( + event = Event_Factory.create_event( "register_completer", completer = source_view.get_completion() ) self.emit_to("completion", event) + event = Event_Factory.create_event("get_files") + self.emit_to("files", event) + for file in event.response: + telescope.list_box.add_row(file) + + def unload(self): + window = self.request_ui_element("main-window") + + telescope.unmap_parent_resize_event(window) + + event = Event_Factory.create_event("unregister_command", + command_name = "telescope", + command = Handler, + binding_mode = "released", + binding = "b" + ) + self.emit_to("source_views", event) + + event = Event_Factory.create_event( + "unregister_completer", + completer = telescope.source_view.get_completion() + ) + self.emit_to("completion", event) + + telescope.destroy() + def run(self): ... diff --git a/plugins/code/ui/telescope/telescope.py b/plugins/code/ui/telescope/telescope.py index 83ce606..84df41b 100644 --- a/plugins/code/ui/telescope/telescope.py +++ b/plugins/code/ui/telescope/telescope.py @@ -33,6 +33,7 @@ class Telescope(Gtk.Dialog): def _setup_signals(self): self.connect("focus-out-event", self._focus_out_event) self.connect("show", self._show) + self.connect("destroy", self._handle_destroy) def _subscribe_to_events(self): ... @@ -93,7 +94,10 @@ class Telescope(Gtk.Dialog): self.source_view.set_buffer(buffer) def map_parent_resize_event(self, parent): - parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent)) + self.size_allocate_id = parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent)) + + def unmap_parent_resize_event(self, parent): + parent.disconnect(self.size_allocate_id) def set_source_view(self, source_view): scrolled_win = Gtk.ScrolledWindow() @@ -103,3 +107,8 @@ class Telescope(Gtk.Dialog): self.main_box.pack_end(scrolled_win, True, True, 0) scrolled_win.show_all() + + def _handle_destroy(self, widget): + self.disconnect_by_func(self._focus_out_event) + self.disconnect_by_func(self._show) + self.disconnect_by_func(self._handle_destroy) diff --git a/plugins/ui/template/plugin.py b/plugins/ui/template/plugin.py index bd7fcb6..41b3e54 100644 --- a/plugins/ui/template/plugin.py +++ b/plugins/ui/template/plugin.py @@ -24,8 +24,16 @@ class Plugin(PluginUI): ui_element = self.request_ui_element("header-container") ui_element.add( self.generate_plugin_element() ) + def unload(self): + ui_element = self.request_ui_element("header-container") + self.button = self.generate_plugin_element() + + ui_element.add( self.button ) + def run(self): - ... + self.button.disconnect_by_func(self.send_message) + self.button.destroy() + del button def generate_plugin_element(self): button = Gtk.Button(label = "Hello, World!") diff --git a/src/__builtins__.py b/src/__builtins__.py index c43ebec..ecdfe3c 100644 --- a/src/__builtins__.py +++ b/src/__builtins__.py @@ -65,12 +65,12 @@ builtins.call_chain = call_chain_wrapper -# def custom_except_hook(exc_type, exc_value, exc_traceback): -# if issubclass(exc_type, KeyboardInterrupt): -# sys.__excepthook__(exc_type, exc_value, exc_traceback) -# sys.__excepthook__(exc_type, exc_value, exc_traceback) -# return - -# logger.error("Uncaught exception", exc_info = (exc_type, exc_value, exc_traceback)) - -# sys.excepthook = custom_except_hook +def custom_except_hook(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return +# + logger.error("Uncaught exception", exc_info = (exc_type, exc_value, exc_traceback)) +# +sys.excepthook = custom_except_hook \ No newline at end of file diff --git a/src/app.py b/src/app.py index 261c8a6..bcfd4ac 100644 --- a/src/app.py +++ b/src/app.py @@ -29,8 +29,7 @@ class Application: def run(self): if not settings_manager.is_trace_debug(): - if not self.load_ipc(): - return + if not self.load_ipc(): return win = Window() win.start() diff --git a/src/core/containers/base_container.py b/src/core/containers/base_container.py index c8c1d54..648c79b 100644 --- a/src/core/containers/base_container.py +++ b/src/core/containers/base_container.py @@ -19,7 +19,6 @@ class BaseContainer(Gtk.Box): self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -32,12 +31,16 @@ class BaseContainer(Gtk.Box): self._update_transparency() def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): event_system.subscribe("update-transparency", self._update_transparency) event_system.subscribe("remove-transparency", self._remove_transparency) + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("base-container", self) diff --git a/src/core/containers/body_container.py b/src/core/containers/body_container.py index 21252e5..1a1f3d2 100644 --- a/src/core/containers/body_container.py +++ b/src/core/containers/body_container.py @@ -19,7 +19,6 @@ class BodyContainer(Gtk.Box): self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -31,11 +30,15 @@ class BodyContainer(Gtk.Box): self.set_orientation(Gtk.Orientation.HORIZONTAL) def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): ... + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("body-container", self) diff --git a/src/core/containers/center_container.py b/src/core/containers/center_container.py index 7e82f0e..a217201 100644 --- a/src/core/containers/center_container.py +++ b/src/core/containers/center_container.py @@ -19,7 +19,6 @@ class CenterContainer(Gtk.Box): self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -32,13 +31,16 @@ class CenterContainer(Gtk.Box): self.set_hexpand(True) self.set_vexpand(True) - def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): ... + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("center-container", self) diff --git a/src/core/containers/code/editors_container.py b/src/core/containers/code/editors_container.py index 06409a7..19d90ae 100644 --- a/src/core/containers/code/editors_container.py +++ b/src/core/containers/code/editors_container.py @@ -4,7 +4,7 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from gi.repository import GLib +#from gi.repository import GLib # Application imports @@ -31,7 +31,7 @@ class EditorsContainer(Gtk.Paned): self.set_wide_handle(True) def _setup_signals(self): - self.map_id = self.connect("map", self._init_map) + self.connect("map", self._init_map) def _subscribe_to_events(self): ... @@ -59,13 +59,7 @@ class EditorsContainer(Gtk.Paned): return scrolled_win1, scrolled_win2 def _init_map(self, view): - def _first_show_init(): - self.disconnect(self.map_id) - del self.map_id - - self.code_base.first_map_load() - - del self.code_base - return False - - GLib.timeout_add(100, _first_show_init) + self.disconnect_by_func( self._init_map ) + self.code_base.first_map_load() + self.code_base = None + del self.code_base diff --git a/src/core/containers/footer_container.py b/src/core/containers/footer_container.py index 3729c96..e3fd65e 100644 --- a/src/core/containers/footer_container.py +++ b/src/core/containers/footer_container.py @@ -15,11 +15,9 @@ class FooterContainer(Gtk.Box): def __init__(self): super(FooterContainer, self).__init__() - self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -32,11 +30,15 @@ class FooterContainer(Gtk.Box): self.set_hexpand(True) def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): ... + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("footer-container", self) diff --git a/src/core/containers/header_container.py b/src/core/containers/header_container.py index ab1104d..18f2697 100644 --- a/src/core/containers/header_container.py +++ b/src/core/containers/header_container.py @@ -17,7 +17,6 @@ class HeaderContainer(Gtk.Box): self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -30,11 +29,15 @@ class HeaderContainer(Gtk.Box): self.set_hexpand(True) def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): ... + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("header-container", self) diff --git a/src/core/containers/left_container.py b/src/core/containers/left_container.py index 4b9b8d7..b2d5026 100644 --- a/src/core/containers/left_container.py +++ b/src/core/containers/left_container.py @@ -17,7 +17,6 @@ class LeftContainer(Gtk.Box): self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -30,11 +29,15 @@ class LeftContainer(Gtk.Box): self.set_vexpand(True) def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): ... + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("left-container", self) self.add( Separator("separator-left", 1) ) diff --git a/src/core/containers/right_container.py b/src/core/containers/right_container.py index e103b92..f90415a 100644 --- a/src/core/containers/right_container.py +++ b/src/core/containers/right_container.py @@ -17,7 +17,6 @@ class RightContainer(Gtk.Box): self._setup_styling() self._setup_signals() self._subscribe_to_events() - self._load_widgets() self.show() @@ -30,11 +29,15 @@ class RightContainer(Gtk.Box): self.set_vexpand(True) def _setup_signals(self): - ... + self.connect("show", self._handle_show) def _subscribe_to_events(self): ... + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("right-container", self) self.add( Separator("separator-right", 1) ) diff --git a/src/core/controllers/base_controller.py b/src/core/controllers/base_controller.py index af72f9b..87289a0 100644 --- a/src/core/controllers/base_controller.py +++ b/src/core/controllers/base_controller.py @@ -25,6 +25,7 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerMixin) def __init__(self): self._setup_controller_data() + self.plugins_controller.manual_launch_plugins() self._load_plugins(is_pre = True) self._setup_styling() @@ -43,7 +44,6 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerMixin) self.base_container = BaseContainer() self.plugins_controller = plugins_controller - widget_registery.expose_object("main-window", self.window) settings_manager.register_signals_to_builder([self, self.base_container]) self._collect_files_dirs() diff --git a/src/core/widgets/code/command_system/command_system.py b/src/core/widgets/code/command_system/command_system.py index 7e5d8c4..a723f46 100644 --- a/src/core/widgets/code/command_system/command_system.py +++ b/src/core/widgets/code/command_system/command_system.py @@ -5,13 +5,14 @@ # Application imports from libs.event_factory import Event_Factory, Code_Event_Types +from ..mixins.command_system_mixin import CommandSystemMixin from ..source_view import SourceView from . import commands -class CommandSystem: +class CommandSystem(CommandSystemMixin): def __init__(self): super(CommandSystem, self).__init__() @@ -37,6 +38,10 @@ class CommandSystem: def add_command(self, command_name: str, command: callable): setattr(commands, command_name, command) + def remove_command(self, command_name: str, command: callable): + if hasattr(commands, command_name): + delattr(commands, command_name) + def emit(self, event: Code_Event_Types.CodeEvent): """ Monkey patch 'emit' from command controller... """ @@ -47,69 +52,69 @@ class CommandSystem: ... - def filter_out_loaded_files(self, uris: list[str]): - event = Event_Factory.create_event( - "filter_out_loaded_files", - uris = uris - ) - - self.emit_to("files", event) - - return event.response - - 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): - event = Event_Factory.create_event( - "get_file", - view = view, - buffer = view.get_buffer() - ) - - self.emit_to("files", event) - - return event.response - - def get_swap_file(self, view: SourceView): - event = Event_Factory.create_event( - "get_swap_file", - view = view, - buffer = view.get_buffer() - ) - - self.emit_to("files", event) - - return event.response - - def new_file(self, view: SourceView): - event = Event_Factory.create_event("add_new_file", view = view) - - self.emit_to("files", event) - - return event.response - - def remove_file(self, view: SourceView): - event = Event_Factory.create_event( - "remove_file", - view = view, - buffer = view.get_buffer() - ) - - self.emit_to("files", event) - - return event.response - - def request_completion(self, view: SourceView): - event = Event_Factory.create_event( - "request_completion", - view = view, - buffer = view.get_buffer() - ) - - self.emit_to("completion", event) +# def filter_out_loaded_files(self, uris: list[str]): +# event = Event_Factory.create_event( +# "filter_out_loaded_files", +# uris = uris +# ) +# +# self.emit_to("files", event) +# +# return event.response +# +# 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): +# event = Event_Factory.create_event( +# "get_file", +# view = view, +# buffer = view.get_buffer() +# ) +# +# self.emit_to("files", event) +# +# return event.response +# +# def get_swap_file(self, view: SourceView): +# event = Event_Factory.create_event( +# "get_swap_file", +# view = view, +# buffer = view.get_buffer() +# ) +# +# self.emit_to("files", event) +# +# return event.response +# +# def new_file(self, view: SourceView): +# event = Event_Factory.create_event("add_new_file", view = view) +# +# self.emit_to("files", event) +# +# return event.response +# +# def remove_file(self, view: SourceView): +# event = Event_Factory.create_event( +# "remove_file", +# view = view, +# buffer = view.get_buffer() +# ) +# +# self.emit_to("files", event) +# +# return event.response +# +# def request_completion(self, view: SourceView): +# event = Event_Factory.create_event( +# "request_completion", +# view = view, +# buffer = view.get_buffer() +# ) +# +# self.emit_to("completion", event) \ No newline at end of file diff --git a/src/core/widgets/code/command_system/commands/toggle_plugins_ui.py b/src/core/widgets/code/command_system/commands/toggle_plugins_ui.py new file mode 100644 index 0000000..21af296 --- /dev/null +++ b/src/core/widgets/code/command_system/commands/toggle_plugins_ui.py @@ -0,0 +1,21 @@ +# Python imports + +# Lib imports +import gi + +gi.require_version('GtkSource', '4') + +from gi.repository import GtkSource + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + + + +def execute( + view: GtkSource.View, + *args, + **kwargs +): + logger.debug("Command: Toggle Plugins UI") + view.command.toggle_plugins_ui() diff --git a/src/core/widgets/code/controllers/files_controller.py b/src/core/widgets/code/controllers/files_controller.py index 6f7fa00..da73b5e 100644 --- a/src/core/widgets/code/controllers/files_controller.py +++ b/src/core/widgets/code/controllers/files_controller.py @@ -28,6 +28,8 @@ class FilesController(ControllerBase, list): self.remove_file(event) elif isinstance(event, Code_Event_Types.GetFileEvent): self.get_file(event) + elif isinstance(event, Code_Event_Types.GetFilesEvent): + event.response = self elif isinstance(event, Code_Event_Types.GetSwapFileEvent): self.get_swap_file(event) diff --git a/src/core/widgets/code/controllers/views/signal_mapper.py b/src/core/widgets/code/controllers/views/signal_mapper.py index 915acf9..da702a4 100644 --- a/src/core/widgets/code/controllers/views/signal_mapper.py +++ b/src/core/widgets/code/controllers/views/signal_mapper.py @@ -29,7 +29,11 @@ class SourceViewSignalMapper: def connect_signals(self, source_view: SourceView): signal_mappings = self._get_signal_mappings() for signal, handler in signal_mappings.items(): - source_view.connect(signal, handler) + if not signal == "populate-popup": + source_view.connect(signal, handler) + continue + + source_view.connect_after(signal, handler) def disconnect_signals(self, source_view: SourceView): signal_mappings = self._get_signal_mappings() diff --git a/src/core/widgets/code/controllers/views/source_views_controller.py b/src/core/widgets/code/controllers/views/source_views_controller.py index 059f917..cb66763 100644 --- a/src/core/widgets/code/controllers/views/source_views_controller.py +++ b/src/core/widgets/code/controllers/views/source_views_controller.py @@ -33,11 +33,15 @@ class SourceViewsController(ControllerBase, list): self._remove_file(event) elif isinstance(event, Code_Event_Types.RegisterCommandEvent): self._register_command(event) + elif isinstance(event, Code_Event_Types.UnregisterCommandEvent): + self._unregister_command(event) if not self.signal_mapper.active_view: return if isinstance(event, Code_Event_Types.GetActiveViewEvent): event.response = self.signal_mapper.active_view + elif isinstance(event, Code_Event_Types.GetSourceViewsEvent): + event.response = self elif isinstance(event, Code_Event_Types.TextChangedEvent): self.signal_mapper.active_view.command.exec("update_info_bar") elif isinstance(event, Code_Event_Types.SetActiveFileEvent): @@ -63,6 +67,24 @@ class SourceViewsController(ControllerBase, list): event.command ) + def _unregister_command(self, event: Code_Event_Types.UnregisterCommandEvent): + if not isinstance(event.binding, list): + event.binding = [ event.binding ] + + for binding in event.binding: + self.state_manager.key_mapper.unmap_command( + event.command_name, + { + f"{event.binding_mode}": binding + } + ) + + for view in self: + view.command.remove_command( + event.command_name, + event.command + ) + def _get_command_system(self): event = Event_Factory.create_event("get_new_command_system") self.message_to("commands", event) diff --git a/src/core/widgets/code/key_mapper.py b/src/core/widgets/code/key_mapper.py index eb1b473..6f788da 100644 --- a/src/core/widgets/code/key_mapper.py +++ b/src/core/widgets/code/key_mapper.py @@ -95,6 +95,28 @@ class KeyMapper: getattr(self.states[state], press_state)[keyname] = command + def unmap_command(self, command, entry): + press_state = "held" if "held" in entry else "released" + keyname = entry[press_state] + + state = NoKeyState + if "" in keyname: + state = state | CtrlKeyState + if "" in keyname: + state = state | ShiftKeyState + if "" in keyname: + state = state | AltKeyState + + keyname = keyname.replace("", "") \ + .replace("", "") \ + .replace("", "") \ + .lower() + + mapping = getattr(self.states[state], press_state) + + if keyname in mapping and mapping[keyname] == command: + del mapping[keyname] + def _key_press_event(self, eve): keyname = self.get_keyname(eve) char_str = self.get_char(eve) diff --git a/src/core/widgets/code/mixins/command_system_mixin.py b/src/core/widgets/code/mixins/command_system_mixin.py new file mode 100644 index 0000000..6dd7804 --- /dev/null +++ b/src/core/widgets/code/mixins/command_system_mixin.py @@ -0,0 +1,83 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from ..source_view import SourceView + + + +class CommandSystemMixin: + def toggle_plugins_ui(self): + event = Event_Factory.create_event( "toggle_plugins_ui" ) + + self.emit_to("plugins", event) + + def filter_out_loaded_files(self, uris: list[str]): + event = Event_Factory.create_event( + "filter_out_loaded_files", + uris = uris + ) + + self.emit_to("files", event) + + return event.response + + 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): + event = Event_Factory.create_event( + "get_file", + view = view, + buffer = view.get_buffer() + ) + + self.emit_to("files", event) + + return event.response + + def get_swap_file(self, view: SourceView): + event = Event_Factory.create_event( + "get_swap_file", + view = view, + buffer = view.get_buffer() + ) + + self.emit_to("files", event) + + return event.response + + def new_file(self, view: SourceView): + event = Event_Factory.create_event("add_new_file", view = view) + + self.emit_to("files", event) + + return event.response + + def remove_file(self, view: SourceView): + event = Event_Factory.create_event( + "remove_file", + view = view, + buffer = view.get_buffer() + ) + + self.emit_to("files", event) + + return event.response + + def request_completion(self, view: SourceView): + event = Event_Factory.create_event( + "request_completion", + view = view, + buffer = view.get_buffer() + ) + + self.emit_to("completion", event) diff --git a/src/core/window.py b/src/core/window.py index fc03ad8..abd13f1 100644 --- a/src/core/window.py +++ b/src/core/window.py @@ -67,6 +67,7 @@ class Window(Gtk.ApplicationWindow): def _setup_signals(self): self.connect("focus-in-event", self._on_focus_in_event) self.connect("focus-out-event", self._on_focus_out_event) +# self.connect("show", self._handle_show) self.connect("delete-event", self.stop) GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.stop) @@ -75,6 +76,10 @@ class Window(Gtk.ApplicationWindow): event_system.subscribe("tear-down", self.stop) event_system.subscribe("load-interactive-debug", self._load_interactive_debug) + def _handle_show(self, widget): + self.disconnect_by_func( self._handle_show ) + self._load_widgets() + def _load_widgets(self): widget_registery.expose_object("main-window", self) diff --git a/src/libs/controllers/controller_base.py b/src/libs/controllers/controller_base.py index 480c406..3997ba6 100644 --- a/src/libs/controllers/controller_base.py +++ b/src/libs/controllers/controller_base.py @@ -3,7 +3,7 @@ # Lib imports # Application imports -from ..singleton_raised import SingletonRaised +from ..singleton import Singleton from ..dto.base_event import BaseEvent @@ -17,7 +17,7 @@ class ControllerBaseException(Exception): -class ControllerBase(SingletonRaised, EmitDispatcher): +class ControllerBase(Singleton, EmitDispatcher): def __init__(self): super(ControllerBase, self).__init__() @@ -42,3 +42,6 @@ class ControllerBase(SingletonRaised, EmitDispatcher): def register_controller(self, name: str, controller): self.controller_message_bus.register_controller(name, controller) + + def unregister_controller(self, name: str): + self.controller_message_bus.unregister_controller(name) diff --git a/src/libs/controllers/controller_manager.py b/src/libs/controllers/controller_manager.py index c80229f..aa5afb3 100644 --- a/src/libs/controllers/controller_manager.py +++ b/src/libs/controllers/controller_manager.py @@ -31,10 +31,11 @@ class ControllerManager(Singleton, dict): def _crete_controller_message_bus(self) -> ControllerMessageBus: - controller_message_bus = ControllerMessageBus() - controller_message_bus.message_to = self.message_to - controller_message_bus.message = self.message - controller_message_bus.register_controller = self.register_controller + controller_message_bus = ControllerMessageBus() + controller_message_bus.message_to = self.message_to + controller_message_bus.message = self.message + controller_message_bus.register_controller = self.register_controller + controller_message_bus.unregister_controller = self.unregister_controller return controller_message_bus @@ -51,6 +52,17 @@ class ControllerManager(Singleton, dict): self[name] = controller + def unregister_controller(self, name: str): + if not name: + raise ControllerManagerException("Must pass in a 'name'...") + + if not name in self.keys(): + raise ControllerManagerException( + f"Can't find controller registered with name of '{name}'..." + ) + + self.pop(name, None) + def get_controllers_key_list(self) -> list[str]: return self.keys() diff --git a/src/libs/controllers/controller_message_bus.py b/src/libs/controllers/controller_message_bus.py index a7a3e45..29e82a8 100644 --- a/src/libs/controllers/controller_message_bus.py +++ b/src/libs/controllers/controller_message_bus.py @@ -28,3 +28,6 @@ class ControllerMessageBus: def register_controller(self, name: str, controller): raise ControllerMessageBusException("Controller Message Bus 'register_controller' must be overriden by Controller Manager...") + + def unregister_controller(self, name: str): + raise ControllerMessageBusException("Controller Message Bus 'unregister_controller' must be overriden by Controller Manager...") diff --git a/src/libs/dto/code/events/__init__.py b/src/libs/dto/code/events/__init__.py index c473d0f..cfe8223 100644 --- a/src/libs/dto/code/events/__init__.py +++ b/src/libs/dto/code/events/__init__.py @@ -4,18 +4,21 @@ from .code_event import CodeEvent +from .toggle_plugins_ui_event import TogglePluginsUiEvent from .create_source_view_event import CreateSourceViewEvent from .register_completer_event import RegisterCompleterEvent from .unregister_completer_event import UnregisterCompleterEvent from .register_provider_event import RegisterProviderEvent from .unregister_provider_event import UnregisterProviderEvent from .register_command_event import RegisterCommandEvent +from .unregister_command_event import UnregisterCommandEvent from .file_externally_modified_event import FileExternallyModifiedEvent from .file_externally_deleted_event import FileExternallyDeletedEvent from .set_info_labels_event import SetInfoLabelsEvent from .populate_source_view_popup_event import PopulateSourceViewPopupEvent from .filter_out_loaded_files_event import FilterOutLoadedFilesEvent from .get_active_view_event import GetActiveViewEvent +from .get_source_views_event import GetSourceViewsEvent from .get_new_command_system_event import GetNewCommandSystemEvent from .request_completion_event import RequestCompletionEvent @@ -34,6 +37,7 @@ from .removed_file_event import RemovedFileEvent from .saved_file_event import SavedFileEvent from .get_file_event import GetFileEvent +from .get_files_event import GetFilesEvent from .get_swap_file_event import GetSwapFileEvent from .add_new_file_event import AddNewFileEvent from .pop_file_event import PopFileEvent diff --git a/src/libs/dto/code/events/get_files_event.py b/src/libs/dto/code/events/get_files_event.py new file mode 100644 index 0000000..6ac2d3f --- /dev/null +++ b/src/libs/dto/code/events/get_files_event.py @@ -0,0 +1,13 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class GetFilesEvent(CodeEvent): + ... diff --git a/src/libs/dto/code/events/get_source_views_event.py b/src/libs/dto/code/events/get_source_views_event.py new file mode 100644 index 0000000..471e3eb --- /dev/null +++ b/src/libs/dto/code/events/get_source_views_event.py @@ -0,0 +1,13 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class GetSourceViewsEvent(CodeEvent): + ... diff --git a/src/libs/dto/code/events/toggle_plugins_ui_event.py b/src/libs/dto/code/events/toggle_plugins_ui_event.py new file mode 100644 index 0000000..095a015 --- /dev/null +++ b/src/libs/dto/code/events/toggle_plugins_ui_event.py @@ -0,0 +1,17 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports +import gi +gi.require_version('GtkSource', '4') + +from gi.repository import GtkSource + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class TogglePluginsUiEvent(CodeEvent): + ... diff --git a/src/libs/dto/code/events/unregister_command_event.py b/src/libs/dto/code/events/unregister_command_event.py new file mode 100644 index 0000000..0662dd0 --- /dev/null +++ b/src/libs/dto/code/events/unregister_command_event.py @@ -0,0 +1,20 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports +import gi +gi.require_version('GtkSource', '4') + +from gi.repository import GtkSource + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class UnregisterCommandEvent(CodeEvent): + command_name: str = "" + command: callable = None + binding_mode: str = "" + binding: str or list = "" diff --git a/src/libs/dto/plugins/manifest.py b/src/libs/dto/plugins/manifest.py index 54c7f26..03c1a9c 100644 --- a/src/libs/dto/plugins/manifest.py +++ b/src/libs/dto/plugins/manifest.py @@ -17,6 +17,7 @@ class Manifest: version: str = "0.0.1" support: str = "support@mail.com" pre_launch: bool = False + autoload: bool = True requests: Requests = field(default_factory = lambda: Requests()) def __post_init__(self): diff --git a/src/libs/dto/plugins/manifest_meta.py b/src/libs/dto/plugins/manifest_meta.py index 8e1056b..a234a86 100644 --- a/src/libs/dto/plugins/manifest_meta.py +++ b/src/libs/dto/plugins/manifest_meta.py @@ -11,9 +11,10 @@ from .manifest import Manifest @dataclass class ManifestMeta: - folder: str = "" - path: str = "" - manifest: Manifest = field(default_factory = lambda: Manifest()) + folder: str = "" + path: str = "" + manifest: Manifest = field(default_factory = lambda: Manifest()) + instance: object | None = None def as_dict(self): return asdict(self) diff --git a/src/libs/event_factory.py b/src/libs/event_factory.py index 102822b..e8ccc43 100644 --- a/src/libs/event_factory.py +++ b/src/libs/event_factory.py @@ -36,6 +36,19 @@ class EventFactory(Singleton): logger.debug(f"Registered {i} event types:") + def unregister_events(self, events: dict): + i = 0 + for name, obj in events: + if not self._is_valid_event_class(obj): continue + + event_type = self._class_name_to_event_type(name) + + del self._event_classes[event_type] + Code_Event_Types.remove_event_class(name) + i += 1 + + logger.debug(f"Unregistered {i} event types:") + def create_event(self, event_type: str, **kwargs) -> BaseEvent: if event_type not in self._event_classes: raise ValueError(f"Unknown event type: {event_type}") @@ -80,6 +93,9 @@ class EventNamespace: def add_event_class(self, name: str, event_class: Type[BaseEvent]): setattr(self, name, event_class) + def remove_event_class(self, name: str): + delattr(self, name) + Code_Event_Types = EventNamespace() diff --git a/src/libs/singleton.py b/src/libs/singleton.py index 4fa22e2..cfe6534 100644 --- a/src/libs/singleton.py +++ b/src/libs/singleton.py @@ -12,21 +12,21 @@ class SingletonError(Exception): -T = TypeVar('T', bound='Singleton') +T = TypeVar('T', bound = 'Singleton') + + class Singleton: - __instance = None + _instances = {} def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: - if cls.__instance is not None: - logger.debug(f"'{cls.__name__}' is a Singleton. Returning instance...") - return cls.__instance + if cls in cls._instances: return cls._instances[cls] - cls.__instance = super(Singleton, cls).__new__(cls) - return cls.__instance + instance = super().__new__(cls) + cls._instances[cls] = instance + return instance - def __init__(self) -> None: - if self.__instance is not None: - return - - super(Singleton, self).__init__() + @classmethod + def destroy(cls): + if cls in cls._instances: + del cls._instances[cls] diff --git a/src/plugins/controller.py b/src/plugins/controller.py index d09431b..6b327fc 100644 --- a/src/plugins/controller.py +++ b/src/plugins/controller.py @@ -4,25 +4,25 @@ import sys import importlib import traceback -from concurrent.futures import ThreadPoolExecutor from os.path import join from os.path import isdir # Lib imports import gi +from gi.repository import Gtk from gi.repository import GLib # Application imports +from libs.event_factory import Event_Factory, Code_Event_Types from libs.controllers.controller_base import ControllerBase - from libs.dto.plugins.manifest_meta import ManifestMeta - from libs.dto.base_event import BaseEvent from .manifest_manager import ManifestManager from .plugins_controller_mixin import PluginsControllerMixin from .plugin_reload_mixin import PluginReloadMixin from .plugin_context import PluginContext +from .plugins_ui import PluginsUI @@ -40,11 +40,12 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi # path = os.path.dirname(os.path.realpath(__file__)) # sys.path.insert(0, path) # NOTE: I think I'm not using this correctly... - self._plugin_collection: list = [] - - self._plugins_path: str = settings_manager.path_manager.get_plugins_path() + self.plugins_ui: PluginsUI = PluginsUI() self._manifest_manager: ManifestManager = ManifestManager() + self._plugin_collection: list = [] + self._plugins_path: str = settings_manager.path_manager.get_plugins_path() + self._set_plugins_watcher() @@ -52,6 +53,14 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi for manifest_meta in self._plugin_collection: manifest_meta.instance._controller_message(event) + if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent): + event.menu.append( Gtk.SeparatorMenuItem() ) + item = Gtk.MenuItem(label = "Plugins") + item.connect("activate", self.toggle_plugins_ui) + event.menu.append(item) + elif isinstance(event, Code_Event_Types.TogglePluginsUiEvent): + self.toggle_plugins_ui() + def _collect_search_locations(self, path: str, locations: list): locations.append(path) for file in os.listdir(path): @@ -105,33 +114,46 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi ): if not is_pre_launch: GLib.idle_add( - self._run_with_pool, module, manifest_meta + self.execute_plugin, module, manifest_meta ) return - self._run_with_pool(module, manifest_meta) + self.execute_plugin(module, manifest_meta) - 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): logger.info(f"Loading pre-launch plugins...") manifest_metas: list = self._manifest_manager.get_pre_launch_plugins() self._load_plugins(manifest_metas, is_pre_launch = True) - def post_launch_plugins(self) -> None: + for manifest_meta in manifest_metas: + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def post_launch_plugins(self): logger.info(f"Loading post-launch plugins...") manifest_metas: list = self._manifest_manager.get_post_launch_plugins() self._load_plugins(manifest_metas) + for manifest_meta in manifest_metas: + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def manual_launch_plugins(self): + logger.info(f"Collecting manual-launch plugins...") + manifest_metas: list = self._manifest_manager.get_manual_launch_plugins() + + for manifest_meta in manifest_metas: + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def toggle_plugin_load_state(self, widget, manifest_meta): + if manifest_meta.instance: + self._plugin_collection.remove(manifest_meta) + manifest_meta.instance.unload() + manifest_meta.instance = None + widget.set_label("Load") + return + + self._load_plugins( [manifest_meta] ) + widget.set_label("Unload") + def execute_plugin(self, module: type, manifest_meta: ManifestMeta): plugin = module.Plugin() plugin.plugin_context: PluginContext = self.create_plugin_context() @@ -148,15 +170,18 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi self._plugin_collection.append(manifest_meta) def create_plugin_context(self): - plugin_context: PluginContext = PluginContext() + plugin_context: PluginContext = PluginContext() - plugin_context.request_ui_element: callable = self.request_ui_element - plugin_context.emit: callable = self.emit - plugin_context.emit_to: callable = self.emit_to - plugin_context.emit_to_selected: callable = self.emit_to_selected - plugin_context.register_controller: callable = self.register_controller + plugin_context.request_ui_element: callable = self.request_ui_element + plugin_context.emit: callable = self.emit + plugin_context.emit_to: callable = self.emit_to + plugin_context.emit_to_selected: callable = self.emit_to_selected + plugin_context.register_controller: callable = self.register_controller + plugin_context.unregister_controller: callable = self.unregister_controller return plugin_context + def toggle_plugins_ui(self, widget = None): + self.plugins_ui.hide() if self.plugins_ui.is_visible() else self.plugins_ui.show() plugins_controller = PluginsController() diff --git a/src/plugins/manifest_manager.py b/src/plugins/manifest_manager.py index e41b92d..0f7add3 100644 --- a/src/plugins/manifest_manager.py +++ b/src/plugins/manifest_manager.py @@ -19,10 +19,12 @@ class ManifestMapperException(Exception): class ManifestManager: def __init__(self): - self._plugins_path = settings_manager.path_manager.get_plugins_path() + self._plugins_path: str = \ + settings_manager.path_manager.get_plugins_path() - self.pre_launch_manifests: list = [] - self.post_launch_manifests: list = [] + self.pre_launch_manifests: list = [] + self.post_launch_manifests: list = [] + self.manual_launch_manifests: list = [] self.load_manifests() @@ -37,7 +39,7 @@ class ManifestManager: ]: self.load(folder, path) - def load(self, folder, path): + def load(self, folder, path) -> ManifestMeta: manifest_pth = join(path, "manifest.json") if not os.path.exists(manifest_pth): @@ -52,14 +54,22 @@ class ManifestManager: manifest_meta.path = path manifest_meta.manifest = manifest + if not manifest.autoload: + self.manual_launch_manifests.append(manifest_meta) + return + if manifest.pre_launch: self.pre_launch_manifests.append(manifest_meta) else: self.post_launch_manifests.append(manifest_meta) - def get_pre_launch_plugins(self) -> dict: + return manifest_meta + + def get_pre_launch_plugins(self) -> list: return self.pre_launch_manifests - def get_post_launch_plugins(self) -> None: + def get_post_launch_plugins(self) -> list: return self.post_launch_manifests + def get_manual_launch_plugins(self) -> list: + return self.manual_launch_manifests diff --git a/src/plugins/plugin_context.py b/src/plugins/plugin_context.py index 3957c97..eb093e0 100644 --- a/src/plugins/plugin_context.py +++ b/src/plugins/plugin_context.py @@ -37,3 +37,6 @@ class PluginContext: def register_controller(self, name: str, controller): raise PluginContextException("Plugin Context 'register_controller' must be overridden...") + def unregister_controller(self, name: str): + raise PluginContextException("Plugin Context 'unregister_controller' must be overridden...") + diff --git a/src/plugins/plugin_reload_mixin.py b/src/plugins/plugin_reload_mixin.py index 2b256ff..eea9a9e 100644 --- a/src/plugins/plugin_reload_mixin.py +++ b/src/plugins/plugin_reload_mixin.py @@ -12,12 +12,12 @@ class PluginReloadMixin: _plugins_dir_watcher = None def _set_plugins_watcher(self) -> None: - self._plugins_dir_watcher = Gio.File.new_for_path( - self._plugins_path - ).monitor_directory( - Gio.FileMonitorFlags.WATCH_MOVES, - Gio.Cancellable() - ) + self._plugins_dir_watcher = \ + Gio.File.new_for_path( self._plugins_path ) \ + .monitor_directory( + Gio.FileMonitorFlags.WATCH_MOVES, + Gio.Cancellable() + ) self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ()) @@ -27,10 +27,40 @@ class PluginReloadMixin: eve_type = None, data = None ): - if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, - Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, - Gio.FileMonitorEvent.MOVED_OUT]: - self.reload_plugins(file) + if eve_type is Gio.FileMonitorEvent.RENAMED: + ... - def reload_plugins(self, file: str = None) -> None: - logger.info(f"Reloading plugins... stub.") + if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.MOVED_IN]: + self.add_plugin(file) + + if eve_type in [Gio.FileMonitorEvent.DELETED, Gio.FileMonitorEvent.MOVED_OUT]: + self.remove_plugin(file) + + def add_plugin(self, file: str) -> None: + logger.info(f"Adding plugin: {file.get_uri()}") + uri = file.get_uri() + path = uri.replace("file://", "") + folder = path.split("/")[-1] + manifest_meta = self._manifest_manager.load(folder, path) + + self._load_plugins( [manifest_meta] ) + self.plugins_ui.add_row(manifest_meta, self.toggle_plugin_load_state) + + def remove_plugin(self, file: str) -> None: + logger.info(f"Removing plugin: {file.get_uri()}") + for manifest_meta in self._plugin_collection[:]: + if not manifest_meta.folder in file.get_uri(): continue + + manifest_meta.instance.unload() + manifest_meta.instance = None + self._plugin_collection.remove(manifest_meta) + self.plugins_ui.remove_row(manifest_meta) + + if manifest_meta in self._manifest_manager.pre_launch_manifests: + self._manifest_manager.pre_launch_manifests.remove(manifest_meta) + elif manifest_meta in self._manifest_manager.post_launch_manifests: + self._manifest_manager.post_launch_manifests.remove(manifest_meta) + elif manifest_meta in self._manifest_manager.manual_launch_manifests: + self._manifest_manager.manual_launch_manifests.remove(manifest_meta) + + break diff --git a/src/plugins/plugin_types/plugin_base.py b/src/plugins/plugin_types/plugin_base.py index 92a819c..221643f 100644 --- a/src/plugins/plugin_types/plugin_base.py +++ b/src/plugins/plugin_types/plugin_base.py @@ -27,6 +27,9 @@ class PluginBase: def load(self): raise PluginBaseException("Plugin Base 'load' must be overriden by Plugin") + def unload(self): + raise PluginBaseException("Plugin Base 'unload' must be overriden by Plugin") + def run(self): raise PluginBaseException("Plugin Base 'run' must be overriden by Plugin") diff --git a/src/plugins/plugin_types/plugin_code.py b/src/plugins/plugin_types/plugin_code.py index 95aa9e9..3c6188d 100644 --- a/src/plugins/plugin_types/plugin_code.py +++ b/src/plugins/plugin_types/plugin_code.py @@ -34,6 +34,9 @@ class PluginCode(PluginBase): def register_controller(self, name: str, controller): return self.plugin_context.register_controller(name, controller) + def unregister_controller(self, name: str): + return self.plugin_context.unregister_controller(name) + def request_ui_element(self, element_id: str): return self.plugin_context.request_ui_element(element_id) diff --git a/src/plugins/plugins_ui.py b/src/plugins/plugins_ui.py new file mode 100644 index 0000000..34efa56 --- /dev/null +++ b/src/plugins/plugins_ui.py @@ -0,0 +1,100 @@ +# Python imports + +# Lib imports +import gi +from gi.repository import Gtk + +# Application imports + + + +class PluginsUI(Gtk.Dialog): + def __init__(self): + super(PluginsUI, self).__init__() + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def _setup_styling(self): + header = Gtk.HeaderBar() + self.ctx = self.get_style_context() + self.ctx.add_class("plugin-ui") + + self.set_title("Plugins") + self.set_size_request(450, 530) + self.set_deletable(False) + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + + header.set_title("Plugins") + self.set_titlebar(header) + header.show() + + window = widget_registery.get_object("main-window") + self.set_transient_for(window) + + def _setup_signals(self): + ... + + def _subscribe_to_events(self): + ... + + def _load_widgets(self): + widget_registery.expose_object("plugin-ui", self) + + content_area = self.get_content_area() + scrolled_win = Gtk.ScrolledWindow() + viewport = Gtk.Viewport() + self.list_box = Gtk.ListBox() + + self.list_box.set_selection_mode( Gtk.SelectionMode.NONE ) + scrolled_win.set_vexpand(True) + + viewport.add(self.list_box) + scrolled_win.add(viewport) + content_area.add(scrolled_win) + + scrolled_win.show_all() + + def add_row(self, manifest_meta, callback: callable): + box = Gtk.Box() + plugin_lbl = Gtk.Label(label = manifest_meta.manifest.name) + author_lbl = Gtk.Label(label = manifest_meta.manifest.author) + version_lbl = Gtk.Label(label = manifest_meta.manifest.version) + is_autoload = manifest_meta.manifest.autoload + toggle_bttn = Gtk.ToggleButton(label = "Unload" if is_autoload else "Load") + + toggle_bttn.set_active(is_autoload) + plugin_lbl.set_hexpand(True) + box.set_hexpand(True) + version_lbl.set_margin_left(15) + version_lbl.set_margin_right(15) + toggle_bttn.set_size_request(120, -1) + + toggle_bttn.toggle_id = \ + toggle_bttn.connect("toggled", callback, manifest_meta) + + box.add(plugin_lbl) + box.add(author_lbl) + box.add(version_lbl) + box.add(toggle_bttn) + box.manifest_meta = manifest_meta + + box.show_all() + self.list_box.add(box) + + def remove_row(self, manifest_meta): + for row in self.list_box.get_children(): + child = row.get_children()[0] + if not child.manifest_meta == manifest_meta: continue + + child.manifest_meta = None + toggle_bttn = getattr(child, "toggle_bttn", None) + toggle_bttn.disconnect(toggle_bttn.toggle_id) + + self.list_box.remove(row) + box.destroy() + break diff --git a/user_config/usr/share/newton/code-key-bindings.json b/user_config/usr/share/newton/code-key-bindings.json index 7cecd01..65a611b 100644 --- a/user_config/usr/share/newton/code-key-bindings.json +++ b/user_config/usr/share/newton/code-key-bindings.json @@ -42,6 +42,9 @@ "save_file_as": { "released": "s" }, + "toggle_plugins_ui": { + "released": "p" + }, "focus_left_sibling": { "released": "Page_Up" },