From 1447a68fb0e3df4617169457275d0807135a5b51 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sat, 28 Feb 2026 01:10:28 -0600 Subject: [PATCH] Refactor controller architecture and multi-insert state - Rename ControllerContext to ControllerMessageBus for clarity - Switch ControllerBase to use SingletonRaised - Replace MarkEventsMixin with MarkerManager for multi-cursor editing - Add populate_popup event support for source view context menus - Remove unused swap file events - Moved JSON prettify feature to plugin - Fix event name: "removed_file" -> "remove_file" --- plugins/code/prettify_json/__init__.py | 3 + plugins/code/prettify_json/__main__.py | 3 + plugins/code/prettify_json/manifest.json | 7 + plugins/code/prettify_json/plugin.py | 55 ++++++ .../code/command_system/command_system.py | 2 +- .../code/controllers/files_controller.py | 63 +++---- .../code/controllers/views/marker_manager.py | 137 +++++++++++++++ .../code/controllers/views/signal_mapper.py | 16 +- .../code/controllers/views/state_manager.py | 23 +-- .../views/states/source_view_base_state.py | 12 ++ .../states/source_view_multi_insert_state.py | 157 +++++++----------- .../widgets/code/mixins/mark_support_mixin.py | 134 +++++++++++++++ .../code/mixins/source_mark_events_mixin.py | 107 ------------ src/core/widgets/code/source_view.py | 32 ---- src/libs/controllers/controller_base.py | 20 +-- src/libs/controllers/controller_context.py | 30 ---- src/libs/controllers/controller_manager.py | 25 ++- .../controllers/controller_message_bus.py | 30 ++++ src/libs/controllers/emit_dispatcher.py | 7 + src/libs/dto/base_event.py | 8 +- src/libs/dto/code/__init__.py | 3 +- .../code/populate_source_view_popup_event.py | 18 ++ src/libs/dto/code/swap_file_event.py | 13 -- src/libs/dto/code/swapped_file_event.py | 13 -- src/libs/dto/states/__init__.py | 1 - src/libs/dto/states/move_direction.py | 15 -- src/libs/singleton.py | 12 +- src/libs/singleton_raised.py | 10 +- 28 files changed, 561 insertions(+), 395 deletions(-) create mode 100644 plugins/code/prettify_json/__init__.py create mode 100644 plugins/code/prettify_json/__main__.py create mode 100644 plugins/code/prettify_json/manifest.json create mode 100644 plugins/code/prettify_json/plugin.py create mode 100644 src/core/widgets/code/controllers/views/marker_manager.py create mode 100644 src/core/widgets/code/mixins/mark_support_mixin.py delete mode 100644 src/core/widgets/code/mixins/source_mark_events_mixin.py delete mode 100644 src/libs/controllers/controller_context.py create mode 100644 src/libs/controllers/controller_message_bus.py create mode 100644 src/libs/dto/code/populate_source_view_popup_event.py delete mode 100644 src/libs/dto/code/swap_file_event.py delete mode 100644 src/libs/dto/code/swapped_file_event.py delete mode 100644 src/libs/dto/states/move_direction.py diff --git a/plugins/code/prettify_json/__init__.py b/plugins/code/prettify_json/__init__.py new file mode 100644 index 0000000..d36fa8c --- /dev/null +++ b/plugins/code/prettify_json/__init__.py @@ -0,0 +1,3 @@ +""" + Pligin Module +""" diff --git a/plugins/code/prettify_json/__main__.py b/plugins/code/prettify_json/__main__.py new file mode 100644 index 0000000..a576329 --- /dev/null +++ b/plugins/code/prettify_json/__main__.py @@ -0,0 +1,3 @@ +""" + Pligin Package +""" diff --git a/plugins/code/prettify_json/manifest.json b/plugins/code/prettify_json/manifest.json new file mode 100644 index 0000000..9444c81 --- /dev/null +++ b/plugins/code/prettify_json/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Prettify JSON", + "author": "ITDominator", + "version": "0.0.1", + "support": "", + "requests": {} +} diff --git a/plugins/code/prettify_json/plugin.py b/plugins/code/prettify_json/plugin.py new file mode 100644 index 0000000..de44951 --- /dev/null +++ b/plugins/code/prettify_json/plugin.py @@ -0,0 +1,55 @@ +# Python imports +import json + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from plugins.plugin_types import PluginCode + + + +def _load_prettify_json(buffer, menu): + menu.append( Gtk.SeparatorMenuItem() ) + + def on_prettify_json(menuitem, buffer): + start_itr, \ + end_itr = buffer.get_start_iter(), buffer.get_end_iter() + data = buffer.get_text(start_itr, end_itr, False) + text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4) + + buffer.begin_user_action() + buffer.delete(start_itr, end_itr) + buffer.insert(start_itr, text) + buffer.end_user_action() + + item = Gtk.MenuItem(label = "Prettify JSON") + item.connect("activate", on_prettify_json, buffer) + menu.append(item) + + + +class Plugin(PluginCode): + def __init__(self): + super(Plugin, self).__init__() + + + def _controller_message(self, event: Code_Event_Types.CodeEvent): + if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent): + language = event.buffer.get_language() + if not language: return + + if language.get_id() == "json": + _load_prettify_json(event.buffer, event.menu) + + def load(self): + ... + + def run(self): + ... diff --git a/src/core/widgets/code/command_system/command_system.py b/src/core/widgets/code/command_system/command_system.py index d5bcdc3..e92a60e 100644 --- a/src/core/widgets/code/command_system/command_system.py +++ b/src/core/widgets/code/command_system/command_system.py @@ -86,7 +86,7 @@ class CommandSystem: def remove_file(self, view: SourceView): event = Event_Factory.create_event( - "removed_file", + "remove_file", view = view, buffer = view.get_buffer() ) diff --git a/src/core/widgets/code/controllers/files_controller.py b/src/core/widgets/code/controllers/files_controller.py index 0184f66..18d66da 100644 --- a/src/core/widgets/code/controllers/files_controller.py +++ b/src/core/widgets/code/controllers/files_controller.py @@ -20,8 +20,6 @@ class FilesController(ControllerBase, list): def _controller_message(self, event: Code_Event_Types.CodeEvent): if isinstance(event, Code_Event_Types.AddNewFileEvent): self.new_file(event) - elif isinstance(event, Code_Event_Types.SwapFileEvent): - self.swap_file(event) elif isinstance(event, Code_Event_Types.PopFileEvent): self.pop_file(event) elif isinstance(event, Code_Event_Types.RemoveFileEvent): @@ -31,29 +29,6 @@ class FilesController(ControllerBase, list): elif isinstance(event, Code_Event_Types.GetSwapFileEvent): self.get_swap_file(event) - def get_file(self, event: Code_Event_Types.GetFileEvent): - if not event.buffer: return - - for file in self: - if not event.buffer == file.buffer: continue - - event.response = file - - return file - - def get_swap_file(self, event: Code_Event_Types.GetSwapFileEvent): - if not event.buffer: return - - for i, file in enumerate(self): - if not event.buffer == file.buffer: continue - - j = self.next_index(i) - next_file = self[j] - swapped_file = self[j] if not j == -1 else None - - event.response = [swapped_file, next_file] - - return swapped_file, next_file def new_file(self, event: Code_Event_Types.AddNewFileEvent): file = SourceFile() @@ -73,20 +48,6 @@ class FilesController(ControllerBase, list): return file - def swap_file(self, event: Code_Event_Types.GetSwapFileEvent): - if not event.buffer: return - - for i, file in enumerate(self): - if not event.buffer == file.buffer: continue - - j = self.next_index(i) - next_file = self[j] - swapped_file = self[j] if not j == -1 else None - - event.response = [swapped_file, next_file] - - return swapped_file, next_file - def pop_file(self, event: Code_Event_Types.PopFileEvent): if not event.buffer: return @@ -134,6 +95,30 @@ class FilesController(ControllerBase, list): return next_file + def get_file(self, event: Code_Event_Types.GetFileEvent): + if not event.buffer: return + + for file in self: + if not event.buffer == file.buffer: continue + + event.response = file + + return file + + def get_swap_file(self, event: Code_Event_Types.GetSwapFileEvent): + if not event.buffer: return + + for i, file in enumerate(self): + if not event.buffer == file.buffer: continue + + j = self.next_index(i) + next_file = self[j] + swapped_file = self[j] if not j == -1 else None + + event.response = [swapped_file, next_file] + + return swapped_file, next_file + def next_index(self, i): size = len(self) diff --git a/src/core/widgets/code/controllers/views/marker_manager.py b/src/core/widgets/code/controllers/views/marker_manager.py new file mode 100644 index 0000000..a11cac3 --- /dev/null +++ b/src/core/widgets/code/controllers/views/marker_manager.py @@ -0,0 +1,137 @@ +# Python imports + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports +from ...mixins.mark_support_mixin import MarkSupportMixin + + + +class MarkerManager(MarkSupportMixin): + + def __init__(self): + super().__init__() + + self.buffer_markers: dict = {} + + self.selection_tag: Gtk.TextTag = Gtk.TextTag.new("selection") + self.selection_tag.props.background = "rgba(111, 168, 220, 0.64)" + self.selection_tag.props.foreground = "#ffffff" + + + def move_by_char(self, buffer, is_forward: bool, is_selection: bool): + self._move(buffer, is_forward, is_selection, mode = "char") + + def move_by_word(self, buffer, is_forward: bool, is_selection: bool): + self._move(buffer, is_forward, is_selection, mode = "word") + + def move_by_line(self, buffer, is_forward: bool, is_selection: bool): + self._move(buffer, is_forward, is_selection, mode = "line") + + def _move(self, buffer, is_forward: bool, is_selection: bool, mode: str): + self.clear_highlight(buffer) + self.insert_selection_tag(buffer) + + for mark_hash in self.buffer_markers: + marker = self.buffer_markers[mark_hash] + start_mark = marker["start_mark"] + end_mark = marker["end_mark"] + has_selection = marker["is_selection"] + + start_itr = buffer.get_iter_at_mark(start_mark) + end_itr = buffer.get_iter_at_mark(end_mark) + + if is_selection: + self.buffer_markers[mark_hash]["is_selection"] = True + + self._move_iter(buffer, end_itr, mode, is_forward) + buffer.move_mark(end_mark, end_itr) + + self._apply_selection(buffer, start_itr, end_itr) + continue + + if has_selection: + self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward) + continue + + # No selection — move both anchor and caret together + self._move_iter(buffer, end_itr, mode, is_forward) + + buffer.move_mark(start_mark, end_itr) + buffer.move_mark(end_mark, end_itr) + + def collapse_selection(self, + buffer, mark_hash, start_mark, end_mark, is_forward: bool + ): + self.buffer_markers[mark_hash]["is_selection"] = False + + start_itr = buffer.get_iter_at_mark(start_mark) + end_itr = buffer.get_iter_at_mark(end_mark) + + # Determine which side is visually the caret + if start_itr.compare(end_itr) <= 0: + left = start_itr + right = end_itr + else: + left = end_itr + right = start_itr + + # If moving forward → collapse to right edge + collapse_itr = right if is_forward else left + + buffer.move_mark(start_mark, collapse_itr) + buffer.move_mark(end_mark, collapse_itr) + + def _move_iter(self, buffer, itr_, mode: str, is_forward: bool): + if mode == "char": + itr_.forward_char() if is_forward else itr_.backward_char() + elif mode == "word": + itr_.forward_word_end() if is_forward else itr_.backward_word_start() + elif mode == "line": + line = itr_.get_line() + offset = itr_.get_line_offset() + + max_line = buffer.get_line_count() - 1 + new_line = line + 1 if is_forward else line - 1 + new_line = max(0, min(max_line, new_line)) + + itr_.set_line(new_line) + self.move_to_offset(offset, itr_) + + def _apply_selection(self, buffer, start_itr, end_itr): + if start_itr.compare(end_itr) <= 0: + buffer.apply_tag(self.selection_tag, start_itr, end_itr) + else: + buffer.apply_tag(self.selection_tag, end_itr, start_itr) + + + def button_release_event(self, source_view, event): + buffer = source_view.get_buffer() + + coords = source_view.window_to_buffer_coords( + Gtk.TextWindowType.TEXT, + event.x, + event.y, + ) + + is_over_text, target_itr, _ = source_view.get_iter_at_position( + coords.buffer_x, + coords.buffer_y, + ) + + if not is_over_text: + target_itr.forward_visible_line() + target_itr.backward_char() + + if self.remove_mark_set(target_itr, buffer): + return + + self.insert_mark_set(target_itr, buffer) + + def key_press_event(self, source_view, event, key_mapper): + ... diff --git a/src/core/widgets/code/controllers/views/signal_mapper.py b/src/core/widgets/code/controllers/views/signal_mapper.py index 8e64d9d..599c9a3 100644 --- a/src/core/widgets/code/controllers/views/signal_mapper.py +++ b/src/core/widgets/code/controllers/views/signal_mapper.py @@ -42,7 +42,8 @@ class SourceViewSignalMapper: "key-press-event": self._key_press_event, "key-release-event": self._key_release_event, "button-press-event": self._button_press_event, - "button-release-event": self._button_release_event + "button-release-event": self._button_release_event, + "populate-popup": self._populate_popup } def _focus_in_event(self, source_view: SourceView, eve): @@ -54,14 +55,17 @@ class SourceViewSignalMapper: source_view, step, count, extend_selection, self.emit ) + def _key_press_event(self, source_view: SourceView, eve): + return self.state_manager.handle_key_press_event(source_view, eve) + + def _key_release_event(self, source_view: SourceView, eve): + return self.state_manager.handle_key_release_event(source_view, eve) + def _button_press_event(self, source_view: SourceView, eve): return self.state_manager.handle_button_press_event(source_view, eve) def _button_release_event(self, source_view: SourceView, eve): return self.state_manager.handle_button_release_event(source_view, eve) - def _key_press_event(self, source_view: SourceView, eve): - return self.state_manager.handle_key_press_event(source_view, eve) - - def _key_release_event(self, source_view: SourceView, eve): - return self.state_manager.handle_key_release_event(source_view, eve) + def _populate_popup(self, source_view, menu): + return self.state_manager.handle_populate_popup(source_view, menu, self.emit) diff --git a/src/core/widgets/code/controllers/views/state_manager.py b/src/core/widgets/code/controllers/views/state_manager.py index 4aa3201..51c4276 100644 --- a/src/core/widgets/code/controllers/views/state_manager.py +++ b/src/core/widgets/code/controllers/views/state_manager.py @@ -34,15 +34,6 @@ class SourceViewStateManager: source_view, step, count, extend_selection, emit ) - def handle_button_press_event(self, source_view, eve): - return self.states[source_view.state].button_press_event(source_view, eve) - - def handle_button_release_event(self, source_view, eve): - # Handle state transitions (multi-insert toggling) - self._handle_multi_insert_toggle(source_view, eve) - - return self.states[source_view.state].button_release_event(source_view, eve) - def handle_key_press_event(self, source_view, eve): return self.states[source_view.state].key_press_event( source_view, eve, self.key_mapper @@ -53,6 +44,17 @@ class SourceViewStateManager: source_view, eve, self.key_mapper ) + def handle_button_press_event(self, source_view, eve): + self._handle_multi_insert_toggle(source_view, eve) + + return self.states[source_view.state].button_press_event(source_view, eve) + + def handle_button_release_event(self, source_view, eve): + return self.states[source_view.state].button_release_event(source_view, eve) + + def handle_populate_popup(self, source_view, menu, emit): + return self.states[source_view.state].populate_popup(source_view, menu, emit) + def _handle_multi_insert_toggle(self, source_view, eve): is_control = self.key_mapper.is_control(eve) if is_control and not source_view.state == SourceViewStates.MULTIINSERT: @@ -61,6 +63,7 @@ class SourceViewStateManager: if not is_control and source_view.state == SourceViewStates.MULTIINSERT: logger.debug("Entered Regular Insert Mode...") - self.states[source_view.state].clear_markers(source_view) + self.states[source_view.state].marker_manager.clear_mark_sets(source_view) source_view.state = SourceViewStates.INSERT + diff --git a/src/core/widgets/code/controllers/views/states/source_view_base_state.py b/src/core/widgets/code/controllers/views/states/source_view_base_state.py index 72cbbfe..a0bdbe4 100644 --- a/src/core/widgets/code/controllers/views/states/source_view_base_state.py +++ b/src/core/widgets/code/controllers/views/states/source_view_base_state.py @@ -78,3 +78,15 @@ class SourceViewsBaseState: ) return True if not response else response + + def populate_popup(self, source_view, menu, emit): + buffer = source_view.get_buffer() + event = Event_Factory.create_event( + "populate_source_view_popup", + buffer = buffer, + menu = menu + ) + + emit(event) + + menu.show_all() diff --git a/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py b/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py index 8f9babd..5460b38 100644 --- a/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py +++ b/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py @@ -2,140 +2,108 @@ # Lib imports import gi -gi.require_version('Gtk', '3.0') +gi.require_version("Gtk", "3.0") from gi.repository import Gtk # Application imports -from libs.event_factory import Event_Factory, Code_Event_Types -from libs.dto.states import SourceViewStates, MoveDirection, CursorAction +from libs.event_factory import Event_Factory +from libs.dto.states import CursorAction -from ....mixins.source_mark_events_mixin import MarkEventsMixin +from ..marker_manager import MarkerManager from .source_view_base_state import SourceViewsBaseState -class SourceViewsMultiInsertState(SourceViewsBaseState, MarkEventsMixin): +class SourceViewsMultiInsertState(SourceViewsBaseState): def __init__(self): super(SourceViewsMultiInsertState, self).__init__() - self.cursor_action: CursorAction = 0 - self.move_direction: MoveDirection = 0 - self.insert_markers: list = [] + self.cursor_action: CursorAction = None + self.marker_manager: MarkerManager = MarkerManager() - def insert_text(self, file, text): - if not self.insert_markers: return False + def insert_text(self, file, text: str) -> bool: + if not self.marker_manager.buffer_markers: return False buffer = file.buffer + if buffer.is_processing_completion: - self.insert_completion_text(buffer, text) - return True + return self._insert_completion_text(buffer, text) - # freeze buffer and insert to each mark (if any) - buffer.block_insert_after_signal() - buffer.begin_user_action() + def insert_text(start_itr, end_itr = None): + if not end_itr: + buffer.insert(start_itr, text, -1) + return - with buffer.freeze_notify(): - for mark in self.insert_markers: - itr = buffer.get_iter_at_mark(mark) - buffer.insert(itr, text, -1) - - buffer.end_user_action() - buffer.unblock_insert_after_signal() + buffer.delete(start_itr, end_itr) + buffer.insert(start_itr, text, -1) + self.marker_manager.apply_to_marks(buffer, insert_text) return True - def insert_completion_text(self, buffer, text): + def _insert_completion_text(self, buffer, text: str) -> bool: buffer.is_processing_completion = False - # freeze buffer and insert to each mark (if any) - buffer.block_insert_after_signal() - buffer.begin_user_action() + def replace_word(start_itr, end_itr = None): + if not end_itr: + end_itr = start_itr.copy() - with buffer.freeze_notify(): - for mark in self.insert_markers: - end_itr = buffer.get_iter_at_mark(mark) - start_itr = end_itr.copy() + if not start_itr.starts_word(): + start_itr.backward_word_start() - if not start_itr.starts_word(): - start_itr.backward_word_start() + if not end_itr.ends_word(): + end_itr.forward_word_end() - if not end_itr.ends_word(): - end_itr.forward_word_end() - - buffer.delete(start_itr, end_itr) - buffer.insert(start_itr, text, -1) - - buffer.end_user_action() - buffer.unblock_insert_after_signal() + buffer.delete(start_itr, end_itr) + buffer.insert(start_itr, text, -1) + self.marker_manager.apply_to_marks(buffer, replace_word) return True - def move_cursor(self, source_view, step, count, extend_selection, emit): - buffer = source_view.get_buffer() - self._process_move_direction(buffer) + def move_cursor(self, source_view, step, count, is_selection, emit): + is_forward = count > 0 + buffer = source_view.get_buffer() + + if step in [ + Gtk.MovementStep.LOGICAL_POSITIONS, + Gtk.MovementStep.VISUAL_POSITIONS + ]: + self.marker_manager.move_by_char(buffer, is_forward, is_selection) + elif step == Gtk.MovementStep.WORDS: + self.marker_manager.move_by_word(buffer, is_forward, is_selection) + elif step == Gtk.MovementStep.DISPLAY_LINES: + self.marker_manager.move_by_line(buffer, is_forward, is_selection) + self._signal_cursor_moved(source_view, emit) - source_view.command.exec("update_info_bar") - def button_press_event(self, source_view, eve): - super().button_press_event(source_view, eve) + def key_press_event(self, source_view, event, key_mapper): + char = key_mapper.get_raw_keyname(event).upper() + self.is_control = key_mapper.is_control(event) + self.is_shift = key_mapper.is_shift(event) + + if char.upper() in ["BACKSPACE", "DELETE", "ENTER"]: + self.marker_manager.process_cursor_action( + source_view.get_buffer(), + char.upper() + ) + return False + + return super().key_press_event(source_view, event, key_mapper) + + def button_press_event(self, source_view, event): return True - def button_release_event(self, source_view, eve): - buffer = source_view.get_buffer() - insert_iter = buffer.get_iter_at_mark( buffer.get_insert() ) - data = source_view.window_to_buffer_coords( - Gtk.TextWindowType.TEXT, - eve.x, - eve.y - ) - is_over_text, \ - target_iter, \ - is_trailing = source_view.get_iter_at_position(data.buffer_x, data.buffer_y) - - if not is_over_text: - # NOTE: Trying to put at very end of line if not over text (aka, clicking right of text) - target_iter.forward_visible_line() - target_iter.backward_char() - - self._insert_mark(insert_iter, target_iter, buffer) - - def key_press_event(self, source_view, eve, key_mapper): - char = key_mapper.get_raw_keyname(eve) - - for action in CursorAction: - if not action.name == char.upper(): continue - self.cursor_action = action.value - self._process_cursor_action(source_view.get_buffer()) - - return False - - for direction in MoveDirection: - if not direction.name == char.upper(): continue - self.move_direction = direction.value - return False - - is_future = key_mapper._key_release_event(eve) - if is_future: return True - - command = key_mapper._key_press_event(eve) - if not command: return False - - char_str = key_mapper.get_char(eve) - modkeys_states = key_mapper.get_modkeys_states(eve) - response = source_view.command.exec_with_args( - command, source_view, char_str, modkeys_states - ) - - return True if not response else response + def button_release_event(self, source_view, event): + self.marker_manager.button_release_event(source_view, event) def _signal_cursor_moved(self, source_view, emit): buffer = source_view.get_buffer() - itr = buffer.get_iter_at_mark( buffer.get_insert() ) + itr = buffer.get_iter_at_mark( buffer.get_insert() ) line = itr.get_line() char = itr.get_line_offset() + event = Event_Factory.create_event( "cursor_moved", view = source_view, @@ -145,3 +113,4 @@ class SourceViewsMultiInsertState(SourceViewsBaseState, MarkEventsMixin): ) emit(event) + diff --git a/src/core/widgets/code/mixins/mark_support_mixin.py b/src/core/widgets/code/mixins/mark_support_mixin.py new file mode 100644 index 0000000..b6f9b50 --- /dev/null +++ b/src/core/widgets/code/mixins/mark_support_mixin.py @@ -0,0 +1,134 @@ +# Python imports +import random + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports + + + +class MarkSupportMixin: + def clear_mark_sets(self, source_view): + buffer = source_view.get_buffer() + self.clear_highlight(buffer) + + for mark_set in self.buffer_markers.values(): + start_mark, end_mark, is_selection = mark_set.values() + start_mark.set_visible(False) + buffer.delete_mark(start_mark) + buffer.delete_mark(end_mark) + + self.buffer_markers.clear() + + def insert_selection_tag(self, buffer): + tag_table = buffer.get_tag_table() + if not tag_table.lookup("selection"): + tag_table.add(self.selection_tag) + + def clear_highlight(self, buffer): + if not self.selection_tag: return + start_itr, end_itr = buffer.get_bounds() + buffer.remove_tag(self.selection_tag, start_itr, end_itr) + + def apply_to_marks(self, buffer, operation): + buffer.block_insert_after_signal() + buffer.begin_user_action() + + try: + with buffer.freeze_notify(): + for mark_hash in self.buffer_markers: + marker = self.buffer_markers[mark_hash] + start_mark = marker["start_mark"] + end_mark = marker["end_mark"] + has_selection = marker["is_selection"] + + start_itr = buffer.get_iter_at_mark(start_mark) + end_itr = buffer.get_iter_at_mark(end_mark) + + if has_selection: + operation(start_itr, end_itr) + self.collapse_selection( + buffer, mark_hash, start_mark, end_mark, False + ) + else: + operation(start_itr) + finally: + buffer.end_user_action() + buffer.unblock_insert_after_signal() + + def process_cursor_action(self, buffer, action): + def remove_text(start_itr, end_itr = None): + if end_itr: + buffer.delete(start_itr, end_itr) + return + + buffer.backspace(start_itr, interactive = True, default_editable = True) + + def delete_text(start_itr, end_itr = None): + if end_itr: + buffer.delete(start_itr, end_itr) + return + + start_itr.forward_char() + buffer.backspace(start_itr, interactive = True, default_editable = True) + + if action == "BACKSPACE": + self.apply_to_marks(buffer, remove_text) + elif action == "DELETE": + self.apply_to_marks(buffer, delete_text) + elif action == "ENTER": + ... + + def move_to_offset(self, offset, start_itr): + line_itr = start_itr.copy() + + line_itr.forward_to_line_end() + + next_line_length = line_itr.get_line_offset() + new_offset = min(offset, next_line_length) + start_itr.set_line_offset(new_offset) + + def insert_mark_set(self, target_iter, buffer): + random_bits = random.getrandbits(128) + hash = "%032x" % random_bits + + start_mark = Gtk.TextMark.new( + name = f"multi-insert-start-{hash}", + left_gravity = False + ) + + end_mark = Gtk.TextMark.new( + name = f"multi-insert-end-{hash}", + left_gravity = False + ) +# left_gravity = True + + buffer.add_mark(start_mark, target_iter) + buffer.add_mark(end_mark, target_iter) + start_mark.set_visible(True) + self.buffer_markers[f"{hash}"] = { + "start_mark": start_mark, + "end_mark": end_mark, + "is_selection": False + } + + def remove_mark_set(self, target_iter, buffer) -> bool: + marks = target_iter.get_marks() + + for mark_hash in self.buffer_markers: + start_mark, end_mark, is_selection = self.buffer_markers[mark_hash].values() + if not start_mark in marks: continue + + start_mark.set_visible(False) + buffer.delete_mark(start_mark) + buffer.delete_mark(end_mark) + del self.buffer_markers[mark_hash] + + return True + + return False diff --git a/src/core/widgets/code/mixins/source_mark_events_mixin.py b/src/core/widgets/code/mixins/source_mark_events_mixin.py deleted file mode 100644 index 460d70c..0000000 --- a/src/core/widgets/code/mixins/source_mark_events_mixin.py +++ /dev/null @@ -1,107 +0,0 @@ -# Python imports -import random - -# Lib imports -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk - -# Application imports -from libs.dto.states import SourceViewStates, MoveDirection, CursorAction - - - -class MarkEventsMixin: - def clear_markers(self, source_view): - buffer = source_view.get_buffer() - - for mark in self.insert_markers: - mark.set_visible(False) - buffer.delete_mark(mark) - - self.insert_markers.clear() - - def _insert_mark(self, insert_iter, target_iter, buffer): - mark_found = self._check_for_insert_marks(target_iter, buffer) - - if mark_found: return - - random_bits = random.getrandbits(128) - hash = "%032x" % random_bits - - mark = Gtk.TextMark.new( - name = f"multi_insert_{hash}", - left_gravity = False - ) - - buffer.add_mark(mark, target_iter) - mark.set_visible(True) - self.insert_markers.append(mark) - - - def _check_for_insert_marks(self, target_iter, buffer): - marks = target_iter.get_marks() - - for mark in marks: - if mark in self.insert_markers[:]: - mark.set_visible(False) - self.insert_markers.remove(mark) - buffer.delete_mark(mark) - return True - - insert_itr = buffer.get_iter_at_mark( buffer.get_insert() ) - if target_iter.equal(insert_itr): return True - - return False - - def _process_cursor_action(self, buffer): - if not self.insert_markers: return - if self.cursor_action == CursorAction.NONE.value: return - - action = self.cursor_action - for mark in self.insert_markers: - itr = buffer.get_iter_at_mark(mark) - - if action == CursorAction.BACKSPACE.value: - buffer.backspace(itr, interactive = True, default_editable = True) - elif action == CursorAction.DELETE.value: - itr.forward_char() - buffer.backspace(itr, interactive = True, default_editable = True) - elif action == CursorAction.ENTER.value: - ... - - self.cursor_action = CursorAction.NONE.value - - def _process_move_direction(self, buffer): - if not self.insert_markers: return - if self.move_direction == MoveDirection.NONE.value: return - - direction = self.move_direction - for mark in self.insert_markers: - itr = buffer.get_iter_at_mark(mark) - - if direction == MoveDirection.UP.value: - new_line = itr.get_line() - 1 - new_itr = buffer.get_iter_at_line_offset( - new_line, - itr.get_line_index() - ) - - elif direction == MoveDirection.DOWN.value: - new_line = itr.get_line() + 1 - new_itr = buffer.get_iter_at_line_offset( - new_line, - itr.get_line_index() - ) - elif direction == MoveDirection.LEFT.value: - if not itr.backward_char(): break - new_itr = itr - elif direction == MoveDirection.RIGHT.value: - if not itr.forward_char(): break - new_itr = itr - else: - continue - - buffer.move_mark_by_name(mark.get_name(), new_itr) - - self.move_direction = MoveDirection.NONE diff --git a/src/core/widgets/code/source_view.py b/src/core/widgets/code/source_view.py index ce751f6..9dc749f 100644 --- a/src/core/widgets/code/source_view.py +++ b/src/core/widgets/code/source_view.py @@ -58,7 +58,6 @@ class SourceView(GtkSource.View, SourceViewDnDMixin): def _setup_signals(self): self.connect("drag-data-received", self._on_drag_data_received) - self.connect("populate-popup", self._on_populate_popup) def _subscribe_to_events(self): ... @@ -76,37 +75,6 @@ class SourceView(GtkSource.View, SourceViewDnDMixin): self._set_up_dnd() - def _on_populate_popup(self, view, menu): - buffer = self.get_buffer() - language = buffer.get_language() - - if language.get_id() == "json": - self._load_prettify_json(view, menu) - - menu.show_all() - - def _load_prettify_json(self, view, menu): - menu.append( Gtk.SeparatorMenuItem() ) - - def on_prettify_json(menuitem): - import json - - buffer = self.get_buffer() - start_itr, \ - end_itr = buffer.get_start_iter(), buffer.get_end_iter() - data = buffer.get_text(start_itr, end_itr, False) - text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4) - - buffer.begin_user_action() - buffer.delete(start_itr, end_itr) - buffer.insert(start_itr, text) - buffer.end_user_action() - - item = Gtk.MenuItem(label = "Prettify JSON") - item.connect("activate", on_prettify_json) - menu.append(item) - - def clear_temp_cut_buffer_delayed(self): if self._cut_temp_timeout_id: GLib.source_remove(self._cut_temp_timeout_id) diff --git a/src/libs/controllers/controller_base.py b/src/libs/controllers/controller_base.py index 39bd74f..480c406 100644 --- a/src/libs/controllers/controller_base.py +++ b/src/libs/controllers/controller_base.py @@ -3,12 +3,12 @@ # Lib imports # Application imports -from ..singleton import Singleton +from ..singleton_raised import SingletonRaised from ..dto.base_event import BaseEvent from .emit_dispatcher import EmitDispatcher -from .controller_context import ControllerContext +from .controller_message_bus import ControllerMessageBus @@ -17,28 +17,28 @@ class ControllerBaseException(Exception): -class ControllerBase(Singleton, EmitDispatcher): +class ControllerBase(SingletonRaised, EmitDispatcher): def __init__(self): super(ControllerBase, self).__init__() - self.controller_context: ControllerContext = None + self.controller_message_bus: ControllerMessageBus = None def _controller_message(self, event: BaseEvent): raise ControllerBaseException("Controller Base '_controller_message' must be overridden...") - def set_controller_context(self, controller_context: ControllerContext): - self.controller_context = controller_context + def set_controller_message_bus(self, controller_message_bus: ControllerMessageBus): + self.controller_message_bus = controller_message_bus def message(self, event: BaseEvent): - return self.controller_context.message(event) + return self.controller_message_bus.message(event) def message_to(self, name: str, event: BaseEvent): - return self.controller_context.message_to(name, event) + return self.controller_message_bus.message_to(name, event) def message_to_selected(self, names: list[str], event: BaseEvent): for name in names: - self.controller_context.message_to_selected(name, event) + self.controller_message_bus.message_to_selected(name, event) def register_controller(self, name: str, controller): - self.controller_context.register_controller(name, controller) + self.controller_message_bus.register_controller(name, controller) diff --git a/src/libs/controllers/controller_context.py b/src/libs/controllers/controller_context.py deleted file mode 100644 index dd33f91..0000000 --- a/src/libs/controllers/controller_context.py +++ /dev/null @@ -1,30 +0,0 @@ -# Python imports - -# Lib imports - -# Application imports -from ..dto.base_event import BaseEvent - - - -class ControllerContextException(Exception): - ... - - - -class ControllerContext: - def __init__(self): - super(ControllerContext, self).__init__() - - - def message(self, event: BaseEvent): - raise ControllerContextException("Controller Context 'message' must be overriden by Controller Manager...") - - def message_to(self, name: str, event: BaseEvent): - raise ControllerContextException("Controller Context 'message_to' must be overriden by Controller Manager...") - - def message_to_selected(self, name: list, event: BaseEvent): - raise ControllerContextException("Controller Context 'message_to_selected' must be overriden by Controller Manager...") - - def register_controller(self, name: str, controller): - raise ControllerContextException("Controller Context 'register_controller' must be overriden by Controller Manager...") diff --git a/src/libs/controllers/controller_manager.py b/src/libs/controllers/controller_manager.py index abb4b08..c80229f 100644 --- a/src/libs/controllers/controller_manager.py +++ b/src/libs/controllers/controller_manager.py @@ -7,7 +7,7 @@ from ..singleton import Singleton from ..event_factory import Code_Event_Types from .controller_base import ControllerBase -from .controller_context import ControllerContext +from .controller_message_bus import ControllerMessageBus @@ -17,17 +17,26 @@ class ControllerManagerException(Exception): class ControllerManager(Singleton, dict): + """ + ControllerManager registers controllers by key/value pair. + It binds the message bus methods methods each controller has + due to extending ControllerBase. + """ + def __init__(self): super(ControllerManager, self).__init__() + self.message_bus: ControllerMessageBus \ + = self._crete_controller_message_bus() - def _crete_controller_context(self) -> ControllerContext: - controller_context = ControllerContext() - controller_context.message_to = self.message_to - controller_context.message = self.message - controller_context.register_controller = self.register_controller - return controller_context + def _crete_controller_message_bus(self) -> ControllerMessageBus: + controller_message_bus = ControllerMessageBus() + controller_message_bus.message_to = self.message_to + controller_message_bus.message = self.message + controller_message_bus.register_controller = self.register_controller + + return controller_message_bus def register_controller(self, name: str, controller: ControllerBase): if not name or controller == None: @@ -38,7 +47,7 @@ class ControllerManager(Singleton, dict): f"Can't bind controller to existing registered name of '{name}'..." ) - controller.set_controller_context( self._crete_controller_context() ) + controller.set_controller_message_bus( self.message_bus ) self[name] = controller diff --git a/src/libs/controllers/controller_message_bus.py b/src/libs/controllers/controller_message_bus.py new file mode 100644 index 0000000..a7a3e45 --- /dev/null +++ b/src/libs/controllers/controller_message_bus.py @@ -0,0 +1,30 @@ +# Python imports + +# Lib imports + +# Application imports +from ..dto.base_event import BaseEvent + + + +class ControllerMessageBusException(Exception): + ... + + + +class ControllerMessageBus: + def __init__(self): + super(ControllerMessageBus, self).__init__() + + + def message(self, event: BaseEvent): + raise ControllerMessageBusException("Controller Message Bus 'message' must be overriden by Controller Manager...") + + def message_to(self, name: str, event: BaseEvent): + raise ControllerMessageBusException("Controller Message Bus 'message_to' must be overriden by Controller Manager...") + + def message_to_selected(self, name: list, event: BaseEvent): + raise ControllerMessageBusException("Controller Message Bus 'message_to_selected' must be overriden by Controller Manager...") + + def register_controller(self, name: str, controller): + raise ControllerMessageBusException("Controller Message Bus 'register_controller' must be overriden by Controller Manager...") diff --git a/src/libs/controllers/emit_dispatcher.py b/src/libs/controllers/emit_dispatcher.py index e8e67f4..0a19427 100644 --- a/src/libs/controllers/emit_dispatcher.py +++ b/src/libs/controllers/emit_dispatcher.py @@ -8,6 +8,13 @@ from ..dto.base_event import BaseEvent class EmitDispatcher: + """ + EmitDispatcher is used for allowing controllers to pass/hook in + their message system to children that need to signal events. + Note how we are not handling return info from the 'message' methods + whereas a controller would or could do so. + """ + def __init__(self): super(EmitDispatcher, self).__init__() diff --git a/src/libs/dto/base_event.py b/src/libs/dto/base_event.py index ae92a29..a91c7db 100644 --- a/src/libs/dto/base_event.py +++ b/src/libs/dto/base_event.py @@ -9,6 +9,8 @@ from dataclasses import dataclass, field @dataclass(slots = True) class BaseEvent: - topic: str = None - content: any = None - raw_content: any = None \ No newline at end of file + topic: str = None + content: any = None + raw_content: any = None + success: callable = None + fail: callable = None diff --git a/src/libs/dto/code/__init__.py b/src/libs/dto/code/__init__.py index fbba5b7..e87fa32 100644 --- a/src/libs/dto/code/__init__.py +++ b/src/libs/dto/code/__init__.py @@ -9,6 +9,7 @@ from .register_command_event import RegisterCommandEvent 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 .get_new_command_system_event import GetNewCommandSystemEvent from .request_completion_event import RequestCompletionEvent @@ -21,7 +22,6 @@ from .set_active_file_event import SetActiveFileEvent from .file_path_set_event import FilePathSetEvent from .added_new_file_event import AddedNewFileEvent -from .swapped_file_event import SwappedFileEvent from .popped_file_event import PoppedFileEvent from .removed_file_event import RemovedFileEvent from .saved_file_event import SavedFileEvent @@ -29,6 +29,5 @@ from .saved_file_event import SavedFileEvent from .get_file_event import GetFileEvent from .get_swap_file_event import GetSwapFileEvent from .add_new_file_event import AddNewFileEvent -from .swap_file_event import SwapFileEvent from .pop_file_event import PopFileEvent from .remove_file_event import RemoveFileEvent diff --git a/src/libs/dto/code/populate_source_view_popup_event.py b/src/libs/dto/code/populate_source_view_popup_event.py new file mode 100644 index 0000000..a3c0ac9 --- /dev/null +++ b/src/libs/dto/code/populate_source_view_popup_event.py @@ -0,0 +1,18 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class PopulateSourceViewPopupEvent(CodeEvent): + menu: Gtk.Widget = None diff --git a/src/libs/dto/code/swap_file_event.py b/src/libs/dto/code/swap_file_event.py deleted file mode 100644 index 04c8a6c..0000000 --- a/src/libs/dto/code/swap_file_event.py +++ /dev/null @@ -1,13 +0,0 @@ -# Python imports -from dataclasses import dataclass, field - -# Lib imports - -# Application imports -from .code_event import CodeEvent - - - -@dataclass -class SwapFileEvent(CodeEvent): - ... diff --git a/src/libs/dto/code/swapped_file_event.py b/src/libs/dto/code/swapped_file_event.py deleted file mode 100644 index 33e5551..0000000 --- a/src/libs/dto/code/swapped_file_event.py +++ /dev/null @@ -1,13 +0,0 @@ -# Python imports -from dataclasses import dataclass, field - -# Lib imports - -# Application imports -from .code_event import CodeEvent - - - -@dataclass -class SwappedFileEvent(CodeEvent): - ... diff --git a/src/libs/dto/states/__init__.py b/src/libs/dto/states/__init__.py index 1f05351..58af316 100644 --- a/src/libs/dto/states/__init__.py +++ b/src/libs/dto/states/__init__.py @@ -4,4 +4,3 @@ from .source_view_states import SourceViewStates from .cursor_action import CursorAction -from .move_direction import MoveDirection diff --git a/src/libs/dto/states/move_direction.py b/src/libs/dto/states/move_direction.py deleted file mode 100644 index 9ef314a..0000000 --- a/src/libs/dto/states/move_direction.py +++ /dev/null @@ -1,15 +0,0 @@ -# Python imports -from enum import Enum - -# Lib imports - -# Application imports - - - -class MoveDirection(Enum): - NONE = 0 - UP = 1 - DOWN = 2 - LEFT = 3 - RIGHT = 4 diff --git a/src/libs/singleton.py b/src/libs/singleton.py index 1590162..4fa22e2 100644 --- a/src/libs/singleton.py +++ b/src/libs/singleton.py @@ -15,18 +15,18 @@ class SingletonError(Exception): T = TypeVar('T', bound='Singleton') class Singleton: - _instance = None + __instance = None def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: - if cls._instance is not None: + if cls.__instance is not None: logger.debug(f"'{cls.__name__}' is a Singleton. Returning instance...") - return cls._instance + return cls.__instance - cls._instance = super(Singleton, cls).__new__(cls) - return cls._instance + cls.__instance = super(Singleton, cls).__new__(cls) + return cls.__instance def __init__(self) -> None: - if self._instance is not None: + if self.__instance is not None: return super(Singleton, self).__init__() diff --git a/src/libs/singleton_raised.py b/src/libs/singleton_raised.py index c17b077..bb88d9b 100644 --- a/src/libs/singleton_raised.py +++ b/src/libs/singleton_raised.py @@ -15,15 +15,15 @@ class SingletonError(Exception): T = TypeVar('T', bound='SingletonRaised') class SingletonRaised: - _instance = None + __instance = None def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: - if cls._instance is not None: + if cls.__instance is not None: raise SingletonError(f"'{cls.__name__}' is a Singleton. Cannot create a new instance...") - cls._instance = super(SingletonRaised, cls).__new__(cls) - return cls._instance + cls.__instance = super(SingletonRaised, cls).__new__(cls) + return cls.__instance def __init__(self) -> None: - if self._instance is not None: + if self.__instance is not None: return