| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694 |
- #include "lumacs/gtk_editor.hpp"
- #include "lumacs/editor_core.hpp"
- #include "lumacs/lua_api.hpp"
- #include "lumacs/keybinding.hpp"
- #include "lumacs/command_system.hpp"
- #include "lumacs/minibuffer_manager.hpp" // Include for MinibufferManager and MinibufferMode
- #include "lumacs/gtk_renderer.hpp" // Include for GtkRenderer
- #include "lumacs/gtk_completion_popup.hpp" // Include for GtkCompletionPopup
- #include "lumacs/buffer_manager.hpp" // Include for BufferManager
- #include "lumacs/window_manager.hpp" // Include for WindowManager
- #include "lumacs/kill_ring_manager.hpp" // Include for KillRingManager
- #include "lumacs/register_manager.hpp" // Include for RegisterManager
- #include "lumacs/macro_manager.hpp" // Include for MacroManager
- #include "lumacs/rectangle_manager.hpp" // Include for RectangleManager
- #include "lumacs/logger.hpp"
- #include <spdlog/spdlog.h>
- #include <filesystem>
- #include <vector>
- #include <functional> // For std::function
- #include <cctype> // For std::isalnum
- // Check if GTK is enabled in build
- #ifdef LUMACS_WITH_GTK
- #include <gtkmm.h>
- #include <pangomm.h>
- namespace lumacs {
- // LumacsWindow method implementations
- LumacsWindow::LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
- : Gtk::ApplicationWindow(application) {
- set_title("Lumacs - GTK4");
- set_default_size(1024, 768);
- }
- // GtkEditor method implementations
- GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr), last_cursor_move_time_(std::chrono::steady_clock::now()) {}
- GtkEditor::~GtkEditor() {
- // Disconnect cursor timer first to prevent callbacks during destruction
- if (cursor_timer_connection_.connected()) {
- cursor_timer_connection_.disconnect();
- }
-
- // Clear core pointer to prevent any callbacks during GTK cleanup
- core_ = nullptr;
-
- // If we still have an app reference, try to quit gracefully
- if (app_ && app_->is_registered()) {
- try {
- app_->quit();
- } catch (...) {
- // Ignore exceptions during cleanup
- }
- }
-
- // Clear widget pointers - GTK manages their lifetime
- // drawing_area_ is now a raw pointer managed by Gtk::make_managed, so no delete needed
- drawing_area_ = nullptr;
- window_ = nullptr;
- content_widget_ = nullptr; // Also managed by Gtk::make_managed
- // Let app_ RefPtr be destroyed naturally
- }
- void GtkEditor::init() {
- // Initialize GTK application
- app_ = Gtk::Application::create("org.lumacs.editor");
- app_->signal_activate().connect(sigc::mem_fun(*this, &GtkEditor::on_activate));
- }
- void GtkEditor::run() {
- // Run the application's event loop
- app_->run();
- }
- void GtkEditor::handle_editor_event(EditorEvent event) {
- // Safety check during destruction
- if (!core_ || !app_) return;
- // Handle layout changes
- if (event == EditorEvent::WindowLayoutChanged) {
- rebuild_layout();
- } else if (event == EditorEvent::CursorMoved) {
- // Cursor moved, reset blink timer and ensure cursor is visible
- last_cursor_move_time_ = std::chrono::steady_clock::now();
- cursor_visible_ = true;
- if (content_widget_) {
- // Redraw immediately to ensure cursor is visible at new position
- queue_redraw_all_windows(content_widget_);
- }
- }
- // Request redraw on most events - recursively find all drawing areas
- // This catches CursorMoved too, but we added explicit redraw for immediate visibility
- if (content_widget_ && event != EditorEvent::CursorMoved) { // Avoid double redraw for CursorMoved
- queue_redraw_all_windows(content_widget_);
- }
- if (event == EditorEvent::CommandMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::Command, "M-x ",
- [this](const std::string& input) {
- if (input == "quit" || input == "q") {
- app_->quit();
- } else {
- auto result = core_->command_system().execute(input, {}); // Pass empty args
- core_->set_message(result.message);
- }
- }, nullptr
- );
- } else if (event == EditorEvent::FindFileMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::FilePath, "Find file: ",
- [this](const std::string& input) {
- if (core_->buffer_manager().load_file(input)) core_->set_message("Loaded");
- else core_->set_message("Failed to load");
- }, nullptr
- );
- } else if (event == EditorEvent::BufferSwitchMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::BufferName, "Switch to buffer: ",
- [this](const std::string& input) {
- if (core_->buffer_manager().switch_buffer_in_window(input)) core_->set_message("Switched");
- else core_->set_message("Buffer not found");
- }, nullptr
- );
- } else if (event == EditorEvent::KillBufferMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::BufferName, "Kill buffer: ",
- [this](const std::string& input) {
- if (core_->buffer_manager().close_buffer(input)) core_->set_message("Killed buffer");
- else core_->set_message("Buffer not found");
- }, nullptr
- );
- } else if (event == EditorEvent::ThemeSelectionMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::ThemeName, "Set theme: ",
- [this](const std::string& input) {
- auto theme_names = core_->theme_manager().theme_names();
- auto it = std::find(theme_names.begin(), theme_names.end(), input);
- if (it != theme_names.end()) {
- core_->theme_manager().set_active_theme(input);
- core_->set_message("Switched to theme: " + input);
- } else {
- core_->set_message("Theme not found: " + input);
- }
- }, nullptr
- );
- } else if (event == EditorEvent::ThemeChanged) {
- if (gtk_renderer_) {
- gtk_renderer_->invalidate_cache();
- }
- // Redraw everything with new theme
- if (content_widget_) {
- queue_redraw_all_windows(content_widget_);
- }
- } else if (event == EditorEvent::ISearchMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::ISearch, "I-search: ",
- [](const std::string&) { /* Submit handled by return key in isearch */ },
- nullptr
- );
- core_->minibuffer_manager().start_isearch(true);
- } else if (event == EditorEvent::ISearchBackwardMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::ISearch, "I-search backward: ",
- [](const std::string&) { /* Submit handled by return key in isearch */ },
- nullptr
- );
- core_->minibuffer_manager().start_isearch(false);
- } else if (event == EditorEvent::TransientMessageCleared) {
- // Force redraw to clear the message
- if (content_widget_) queue_redraw_all_windows(content_widget_);
- } else if (event == EditorEvent::Quit) {
- // Disconnect timer before quitting to prevent segfault
- if (cursor_timer_connection_.connected()) {
- cursor_timer_connection_.disconnect();
- }
- // Use idle callback to quit safely after current event processing
- Glib::signal_idle().connect_once([this]() {
- if (app_) {
- app_->quit();
- }
- });
- }
-
- // Check if minibuffer is active AFTER processing event to decide popup visibility
- if (core_->minibuffer_manager().is_active()) {
- show_completion_popup(); // Show/hide completion popup when minibuffer state changes
- if (content_widget_) queue_redraw_all_windows(content_widget_);
- } else {
- hide_completion_popup(); // Minibuffer deactivated, hide popup
- }
- }
- void GtkEditor::set_core(EditorCore* core) {
- core_ = core;
- }
- // Helper to recursively find and redraw all drawing areas
- void GtkEditor::queue_redraw_all_windows(Gtk::Widget* widget) {
- if (!widget) return;
-
- if (auto drawing_area = dynamic_cast<Gtk::DrawingArea*>(widget)) {
- drawing_area->queue_draw();
- } else if (auto paned = dynamic_cast<Gtk::Paned*>(widget)) {
- if (auto start_child = paned->get_start_child()) {
- queue_redraw_all_windows(start_child);
- }
- if (auto end_child = paned->get_end_child()) {
- queue_redraw_all_windows(end_child);
- }
- }
- }
- // Helper to convert GDK keyval and modifiers to Lumacs key name
- std::string GtkEditor::get_lumacs_key_name(guint keyval, Gdk::ModifierType state) {
- std::string key_name;
- if ((state & Gdk::ModifierType::CONTROL_MASK) == Gdk::ModifierType::CONTROL_MASK) {
- key_name += "C-";
- }
- // Alt key is often mapped to Meta in Emacs-like editors
- if ((state & Gdk::ModifierType::ALT_MASK) == Gdk::ModifierType::ALT_MASK || (state & Gdk::ModifierType::META_MASK) == Gdk::ModifierType::META_MASK) {
- key_name += "M-";
- }
- // Shift is generally handled by the keyval itself for letters (e.g., 'A' vs 'a')
- // For special keys, we might want "S-ArrowUp" but for now, rely on keyval.
- // GDK_KEY_Tab does not have a unicode value, handle separately
- if (keyval == GDK_KEY_Tab) {
- key_name += "Tab";
- }
- else if (keyval == GDK_KEY_ISO_Left_Tab) { // Shift-Tab for cycling backwards
- key_name += "S-Tab";
- }
- else if (keyval >= GDK_KEY_space && keyval <= GDK_KEY_asciitilde) { // Printable ASCII
- key_name += static_cast<char>(gdk_keyval_to_unicode(keyval));
- } else {
- // Handle special keys
- switch (keyval) {
- case GDK_KEY_Return: key_name += "Return"; break;
- case GDK_KEY_Escape: key_name += "Escape"; break;
- case GDK_KEY_BackSpace: key_name += "Backspace"; break; // Fixed case sensitivity
- case GDK_KEY_Delete: key_name += "Delete"; break;
- case GDK_KEY_Up: key_name += "ArrowUp"; break;
- case GDK_KEY_Down: key_name += "ArrowDown"; break;
- case GDK_KEY_Left: key_name += "ArrowLeft"; break;
- case GDK_KEY_Right: key_name += "ArrowRight"; break;
- case GDK_KEY_Home: key_name += "Home"; break;
- case GDK_KEY_End: key_name += "End"; break;
- case GDK_KEY_Page_Up: key_name += "PageUp"; break;
- case GDK_KEY_Page_Down: key_name += "PageDown"; break;
- case GDK_KEY_F1: key_name += "F1"; break;
- case GDK_KEY_F2: key_name += "F2"; break;
- case GDK_KEY_F3: key_name += "F3"; break;
- case GDK_KEY_F4: key_name += "F4"; break;
- case GDK_KEY_F5: key_name += "F5"; break;
- case GDK_KEY_F6: key_name += "F6"; break;
- case GDK_KEY_F7: key_name += "F7"; break;
- case GDK_KEY_F8: key_name += "F8"; break;
- case GDK_KEY_F9: key_name += "F9"; break;
- case GDK_KEY_F10: key_name += "F10"; break;
- case GDK_KEY_F11: key_name += "F11"; break;
- case GDK_KEY_F12: key_name += "F12"; break;
- // Add more special keys as needed
- default:
- // Fallback for unhandled keys
- // Gdk::keyval_name(keyval) might give "dead_acute", which is not good.
- // Best to ignore unhandled keys for now or map explicitly.
- return ""; // Consume if we couldn't map to a Lumacs key (to avoid random chars appearing)
- }
- }
- return key_name;
- }
- bool GtkEditor::on_global_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state) {
- (void)keycode;
- spdlog::debug("GtkEditor::on_global_key_pressed: keyval={}, state={}", keyval, (int)state);
- if (!core_) return false;
- // Translate GDK event to Lumacs key name
- std::string lumacs_key_name = get_lumacs_key_name(keyval, state);
- if (lumacs_key_name.empty()) {
- return false; // Not a key we care about or couldn't map
- }
- if (core_->minibuffer_manager().is_active()) {
- // Minibuffer is active, keys should primarily go to it, or completion popup
- if (completion_popup_ && completion_popup_->get_visible()) {
- // If completion popup is visible, it takes precedence for navigation/selection keys
- if (lumacs_key_name == "ArrowUp") {
- completion_popup_->select_previous();
- return true;
- } else if (lumacs_key_name == "ArrowDown") {
- completion_popup_->select_next();
- return true;
- } else if (lumacs_key_name == "Return") {
- if (auto selected = completion_popup_->get_selected_candidate()) {
- on_completion_selected(selected.value());
- } else {
- // If no completion selected, but Return pressed, pass to minibuffer
- core_->minibuffer_manager().handle_key_event(lumacs_key_name);
- }
- return true;
- } else if (lumacs_key_name == "Escape") {
- on_completion_cancelled();
- return true;
- } else if (lumacs_key_name == "Tab" || lumacs_key_name == "S-Tab") {
- // Forward Tab to MinibufferManager for its internal cycle
- core_->minibuffer_manager().handle_key_event(lumacs_key_name);
- // Redraw popup to show updated selection
- show_completion_popup();
- return true;
- }
- }
-
- // If key is not handled by completion popup, or popup is not visible,
- // pass to MinibufferManager
- bool handled_by_minibuffer = core_->minibuffer_manager().handle_key_event(lumacs_key_name);
-
- // After any minibuffer key event, update and potentially show/hide completion
- if (handled_by_minibuffer) {
- if (core_->minibuffer_manager().get_completion_candidates().empty()) {
- hide_completion_popup();
- } else {
- show_completion_popup();
- }
- queue_redraw_all_windows(content_widget_); // Redraw minibuffer content
- } else {
- // If minibuffer didn't handle it, it could be a keybinding for the editor that
- // should still work while the minibuffer is active (e.g., C-g for quit).
- // For now, we assume if minibuffer is active, it consumes all relevant keys.
- // If the key is not handled, it probably means it's irrelevant to minibuffer input.
- // But we should still consume it to avoid propagating to editor keybindings accidentally.
- // Returning true consumes the event.
- return true;
- }
- return handled_by_minibuffer; // Return if minibuffer handled it
- } else {
- // Minibuffer not active, pass to main keybinding manager
- KeyProcessingResult result = core_->keybinding_manager().process_key(lumacs_key_name);
- if (result.type == KeyResult::Executed) {
- queue_redraw_all_windows(content_widget_);
- return true;
- } else if (result.type == KeyResult::Partial) {
- // Multi-key sequence in progress, wait for next key
- queue_redraw_all_windows(content_widget_);
- return true;
- } else if (result.type == KeyResult::Unbound) {
- // Check if it's a self-insertable character
- // We assume single characters without modifiers (except Shift) are self-insertable
- // if they are not bound to something else.
- // lumacs_key_name format: "a", "S-a", "C-a"
- bool has_ctrl = lumacs_key_name.find("C-") != std::string::npos;
- bool has_meta = lumacs_key_name.find("M-") != std::string::npos;
-
- // Allow Shift-key (e.g. "S-a" -> "A" is handled by GDK usually giving "A" directly if we use unicode,
- // but our get_lumacs_key_name handles it.
- // Actually, for printable chars, get_lumacs_key_name returns the unicode char (e.g. "A").
- // So we just check length == 1 and no modifiers.
-
- if (!has_ctrl && !has_meta && lumacs_key_name.length() == 1) {
- // Execute self-insert-command
- core_->command_system().execute("self-insert-command", {lumacs_key_name});
-
- // --- Macro Recording Logic for Self-Insert ---
- if (core_->is_recording_macro()) {
- core_->record_key_sequence(lumacs_key_name);
- }
- // --------------------------------------------
- queue_redraw_all_windows(content_widget_);
- return true;
- }
- }
- // If not processed by keybinding, let GTK handle it (e.g., for system shortcuts)
- return false;
- }
- }
- void GtkEditor::on_activate() {
- // Create main window and associate with the application
- // Note: The window is owned by the application through GObject reference counting
- // We just keep a raw pointer for access, but don't manage its lifetime
- window_ = new LumacsWindow(app_);
- // Build initial layout (single window)
- rebuild_layout();
- // Handle window close event
- window_->signal_close_request().connect([this]() -> bool {
- // Cleanup before closing
- if (cursor_timer_connection_.connected()) {
- cursor_timer_connection_.disconnect();
- }
- core_ = nullptr;
- drawing_area_ = nullptr;
- content_widget_ = nullptr;
-
- // Allow window to close
- return false; // false means "allow close"
- }, false);
- // Show window
- window_->present();
- if (drawing_area_) {
- drawing_area_->grab_focus();
- }
-
- // Initialize GtkRenderer after drawing_area_ is set
- if (core_ && drawing_area_) {
- gtk_renderer_ = std::make_unique<GtkRenderer>(*core_, *drawing_area_);
- }
- // Set up cursor blinking timer (BLINK_INTERVAL like Emacs)
- cursor_timer_connection_ = Glib::signal_timeout().connect(
- sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), BLINK_INTERVAL.count()
- );
- // Initialize completion popup
- completion_popup_ = std::make_unique<GtkCompletionPopup>();
- // Popovers are automatically transient and handle their own hide_on_close logic.
- completion_popup_->signal_candidate_selected().connect(sigc::mem_fun(*this, &GtkEditor::on_completion_selected));
- completion_popup_->signal_cancelled().connect(sigc::mem_fun(*this, &GtkEditor::on_completion_cancelled));
- // Attach global key controller to the main window
- auto global_key_controller = Gtk::EventControllerKey::create();
- global_key_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
- global_key_controller->signal_key_pressed().connect(
- sigc::mem_fun(*this, &GtkEditor::on_global_key_pressed), false
- );
- window_->add_controller(global_key_controller);
- }
- // Rendering
- void GtkEditor::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
- // Safety check - don't draw if core is destroyed or gtk_renderer_ not initialized
- if (!core_ || !gtk_renderer_) return;
- gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_visible_);
- }
- // Cursor blinking callback
- bool GtkEditor::on_cursor_blink() {
- // Safety check - don't blink if core is destroyed or no drawing area
- if (!core_ || !drawing_area_ || !app_) {
- return false; // Stop the timer
- }
-
- // Double check that the app is still running
- if (!app_->is_registered()) {
- return false; // Stop the timer
- }
-
- try {
- auto now = std::chrono::steady_clock::now();
- if (now - last_cursor_move_time_ > BLINK_STATIONARY_THRESHOLD) {
- // Only blink if cursor has been stationary for a while
- cursor_visible_ = !cursor_visible_;
- core_->check_and_clear_message(); // Check and clear messages
- drawing_area_->queue_draw();
- } else {
- // Cursor is still moving or just stopped, keep it visible
- cursor_visible_ = true;
- // No need to queue_draw here, it will be done on CursorMoved or next blink.
- }
- } catch (...) {
- return false; // Stop timer on any exception
- }
-
- return true; // Continue timer
- }
- // Rebuild the GTK layout to match the core's window tree
- void GtkEditor::rebuild_layout() {
- if (!core_ || !window_) return;
- auto root_layout = core_->root_layout();
- if (!root_layout) return;
- // Remove existing content
- if (content_widget_) {
- window_->unset_child();
- }
- // Clear the drawing area reference since we're rebuilding
- drawing_area_ = nullptr;
- // Initialize cached active window to prevent focus jumping
- cached_active_window_ = core_->active_window();
- // Create new layout based on the tree
- content_widget_ = create_widget_for_layout_node(root_layout);
- if (content_widget_) {
- window_->set_child(*content_widget_);
- }
- }
- // Create GTK widget tree from LayoutNode tree
- Gtk::Widget* GtkEditor::create_widget_for_layout_node(std::shared_ptr<LayoutNode> node) {
- if (!node) return nullptr;
- if (node->type == LayoutNode::Type::Leaf) {
- // Create a new DrawingArea for this window
- auto drawing_area = Gtk::make_managed<Gtk::DrawingArea>();
-
- // Set up drawing for this specific window
- // Use a weak reference to the window to avoid crashes if the layout is rebuilt
- std::weak_ptr<Window> weak_window = node->window;
- drawing_area->set_draw_func([this, weak_window, drawing_area](const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
- if (auto window_ptr = weak_window.lock()) { // Correctly capture shared_ptr
- if (gtk_renderer_) {
- gtk_renderer_->draw_window(cr, width, height, window_ptr, drawing_area, cached_active_window_, cursor_visible_);
- }
- }
- });
-
- drawing_area->set_focusable(true);
-
- // Add click handling to set active window explicitly and move cursor
- // We use GestureClick instead of EventControllerFocus to avoid spurious focus changes
- auto click_controller = Gtk::GestureClick::create();
- std::weak_ptr<Window> weak_window_click = node->window;
- click_controller->signal_pressed().connect([this, weak_window_click, drawing_area](int /*n_press*/, double x, double y) {
- if (auto window = weak_window_click.lock()) {
- // 1. Activate Window
- if (core_ && core_->active_window() != window) {
- core_->window_manager().set_active_window(window);
- cached_active_window_ = window; // Cache for rendering to prevent focus jumping
- }
- // IMPORTANT: Grab keyboard focus for this widget
- drawing_area->grab_focus();
-
- // 2. Move Cursor
- if (gtk_renderer_) {
- if (auto pos = gtk_renderer_->resolve_screen_pos(window, x, y)) {
- window->set_cursor(*pos);
- // Clear mark on simple click
- window->buffer().deactivate_mark();
- drawing_area->queue_draw();
- }
- }
- }
- });
- drawing_area->add_controller(click_controller);
- // Add Drag Gesture for Selection
- auto drag_controller = Gtk::GestureDrag::create();
- std::weak_ptr<Window> weak_window_drag = node->window;
-
- drag_controller->signal_drag_begin().connect([this, weak_window_drag, drawing_area](double x, double y) {
- if (auto window = weak_window_drag.lock()) {
- if (gtk_renderer_) {
- if (auto pos = gtk_renderer_->resolve_screen_pos(window, x, y)) {
- // Set mark at start of drag
- window->buffer().set_mark(*pos);
- window->set_cursor(*pos);
- drawing_area->queue_draw();
- }
- }
- }
- });
-
- drag_controller->signal_drag_update().connect([this, weak_window_drag, drawing_area, drag_controller](double dx, double dy) {
- if (auto window = weak_window_drag.lock()) {
- double start_x, start_y;
- if (drag_controller->get_start_point(start_x, start_y) && gtk_renderer_) {
- double current_x = start_x + dx;
- double current_y = start_y + dy;
-
- if (auto pos = gtk_renderer_->resolve_screen_pos(window, current_x, current_y)) {
- window->set_cursor(*pos);
- drawing_area->queue_draw();
- }
- }
- }
- });
-
- drawing_area->add_controller(drag_controller);
-
- // Add scroll handling
- auto scroll_controller = Gtk::EventControllerScroll::create();
- scroll_controller->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
- std::weak_ptr<Window> weak_window_scroll = node->window;
- scroll_controller->signal_scroll().connect([weak_window_scroll, drawing_area](double /*dx*/, double dy) -> bool {
- if (auto window = weak_window_scroll.lock()) {
- // dy is usually 1.0 or -1.0 for wheel steps
- // Scroll 3 lines per step
- int lines = static_cast<int>(dy * 3.0);
- if (lines != 0) {
- window->scroll_lines(lines);
- drawing_area->queue_draw();
- }
- return true;
- }
- return false;
- }, true);
- drawing_area->add_controller(scroll_controller);
-
- // Context menus and tooltips removed per user request (Phase A.1)
-
- // Store reference for single-window compatibility
- if (!drawing_area_) {
- drawing_area_ = drawing_area;
- }
-
- return drawing_area;
- } else {
- // Create a paned container for splits
- Gtk::Paned* paned = nullptr;
- if (node->type == LayoutNode::Type::HorizontalSplit) {
- paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
- } else { // VerticalSplit
- paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
- }
-
- // Recursively create children
- auto child1 = create_widget_for_layout_node(node->child1);
- auto child2 = create_widget_for_layout_node(node->child2);
-
- if (child1) paned->set_start_child(*child1);
- if (child2) paned->set_end_child(*child2);
-
- // Set initial position based on ratio
- // Use signal_map to set position when widget is ready
- paned->signal_map().connect([paned, node](){
- int width = paned->get_width();
- int height = paned->get_height();
- int size = (paned->get_orientation() == Gtk::Orientation::HORIZONTAL) ? width : height;
-
- // Fallback if size not yet available
- if (size <= 1) size = 1000; // Assume a reasonable default window size
-
- paned->set_position(static_cast<int>(size * node->ratio));
- });
-
- return paned;
- }
- }
- // === Completion Popup Helpers ===
- void GtkEditor::show_completion_popup() {
- if (!completion_popup_ || !window_) return;
- auto candidates = core_->minibuffer_manager().get_completion_candidates();
- if (candidates.empty()) {
- hide_completion_popup();
- return;
- }
- // Minibuffer is rendered at the bottom of the main window/drawing area.
- // We need to position the popover relative to the main window, pointing to the minibuffer area.
- Gdk::Rectangle rect;
- rect.set_x(0); // Minibuffer starts at x=0
- rect.set_y(window_->get_height() - 1); // Last line of the window
- rect.set_width(window_->get_width());
- rect.set_height(1); // Small height to represent the minibuffer line
- completion_popup_->show_popup(candidates, 0, *window_, rect.get_x(), rect.get_y());
- }
- void GtkEditor::hide_completion_popup() {
- if (completion_popup_) {
- completion_popup_->hide_popup();
- }
- }
- void GtkEditor::on_completion_selected(CompletionCandidate candidate) {
- core_->minibuffer_manager().set_input_buffer(candidate.text);
- hide_completion_popup();
- }
- void GtkEditor::on_completion_cancelled() {
- hide_completion_popup();
- }
- std::unique_ptr<IEditorView> create_gtk_editor() {
- return std::make_unique<GtkEditor>();
- }
- } // namespace lumacs
- #else // LUMACS_WITH_GTK not defined
- namespace lumacs {
- std::unique_ptr<IEditorView> create_gtk_editor() {
- spdlog::error("Error: Lumacs was built without GTK support.");
- return nullptr;
- }
- } // namespace lumacs
- #endif
|