|
|
@@ -5,6 +5,7 @@
|
|
|
#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 <iostream>
|
|
|
#include <filesystem>
|
|
|
#include <vector>
|
|
|
@@ -18,443 +19,563 @@
|
|
|
|
|
|
namespace lumacs {
|
|
|
|
|
|
-// Custom Gtk::ApplicationWindow to make constructor public
|
|
|
-class LumacsWindow : public Gtk::ApplicationWindow {
|
|
|
-public:
|
|
|
- explicit LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
|
|
|
- : Gtk::ApplicationWindow(application) {
|
|
|
- set_title("Lumacs - GTK4");
|
|
|
- set_default_size(1024, 768);
|
|
|
- }
|
|
|
-};
|
|
|
+// LumacsWindow method implementations
|
|
|
+LumacsWindow::LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
|
|
|
+ : Gtk::ApplicationWindow(application) {
|
|
|
+ set_title("Lumacs - GTK4");
|
|
|
+ set_default_size(1024, 768);
|
|
|
+}
|
|
|
|
|
|
-class GtkEditor : public IEditorView {
|
|
|
-public:
|
|
|
- GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr) {}
|
|
|
- ~GtkEditor() override {
|
|
|
- // 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
|
|
|
- }
|
|
|
+// GtkEditor method implementations
|
|
|
+GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr) {}
|
|
|
+
|
|
|
+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
|
|
|
}
|
|
|
+
|
|
|
+ // 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 init() override {
|
|
|
- // Initialize GTK application
|
|
|
- app_ = Gtk::Application::create("org.lumacs.editor");
|
|
|
- app_->signal_activate().connect(sigc::mem_fun(*this, &GtkEditor::on_activate));
|
|
|
- }
|
|
|
+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 run() override {
|
|
|
- // Run the application's event loop
|
|
|
- app_->run();
|
|
|
- }
|
|
|
+void GtkEditor::run() {
|
|
|
+ // Run the application's event loop
|
|
|
+ app_->run();
|
|
|
+}
|
|
|
|
|
|
- void handle_editor_event(EditorEvent event) override {
|
|
|
- // Safety check during destruction
|
|
|
- if (!core_ || !app_) return;
|
|
|
+void GtkEditor::handle_editor_event(EditorEvent event) {
|
|
|
+ // Safety check during destruction
|
|
|
+ if (!core_ || !app_) return;
|
|
|
|
|
|
- // Handle layout changes
|
|
|
- if (event == EditorEvent::WindowLayoutChanged) {
|
|
|
- rebuild_layout();
|
|
|
- }
|
|
|
+ // Handle layout changes
|
|
|
+ if (event == EditorEvent::WindowLayoutChanged) {
|
|
|
+ rebuild_layout();
|
|
|
+ }
|
|
|
|
|
|
- // Request redraw on most events - recursively find all drawing areas
|
|
|
- if (content_widget_) {
|
|
|
- queue_redraw_all_windows(content_widget_);
|
|
|
- }
|
|
|
+ // Request redraw on most events - recursively find all drawing areas
|
|
|
+ if (content_widget_) {
|
|
|
+ queue_redraw_all_windows(content_widget_);
|
|
|
+ }
|
|
|
|
|
|
- bool minibuffer_activated = false;
|
|
|
- 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);
|
|
|
- core_->set_message(result.message);
|
|
|
- }
|
|
|
- }, nullptr
|
|
|
- );
|
|
|
- minibuffer_activated = true;
|
|
|
- } else if (event == EditorEvent::FindFileMode) {
|
|
|
- core_->minibuffer_manager().activate_minibuffer(
|
|
|
- MinibufferMode::FilePath, "Find file: ",
|
|
|
- [this](const std::string& input) {
|
|
|
- if (core_->load_file(input)) core_->set_message("Loaded");
|
|
|
- else core_->set_message("Failed to load");
|
|
|
- }, nullptr
|
|
|
- );
|
|
|
- minibuffer_activated = true;
|
|
|
- } else if (event == EditorEvent::BufferSwitchMode) {
|
|
|
- core_->minibuffer_manager().activate_minibuffer(
|
|
|
- MinibufferMode::BufferName, "Switch to buffer: ",
|
|
|
- [this](const std::string& input) {
|
|
|
- if (core_->switch_buffer_in_window(input)) core_->set_message("Switched");
|
|
|
- else core_->set_message("Buffer not found");
|
|
|
- }, nullptr
|
|
|
- );
|
|
|
- minibuffer_activated = true;
|
|
|
- } else if (event == EditorEvent::KillBufferMode) {
|
|
|
- core_->minibuffer_manager().activate_minibuffer(
|
|
|
- MinibufferMode::BufferName, "Kill buffer: ",
|
|
|
- [this](const std::string& input) {
|
|
|
- if (core_->close_buffer(input)) core_->set_message("Killed buffer");
|
|
|
- else core_->set_message("Buffer not found");
|
|
|
- }, nullptr
|
|
|
- );
|
|
|
- minibuffer_activated = true;
|
|
|
- } 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_->set_theme(input);
|
|
|
- core_->set_message("Switched to theme: " + input);
|
|
|
- } else {
|
|
|
- core_->set_message("Theme not found: " + input);
|
|
|
- }
|
|
|
- }, nullptr
|
|
|
- );
|
|
|
- minibuffer_activated = true;
|
|
|
- } else if (event == EditorEvent::ISearchMode) {
|
|
|
- core_->minibuffer_manager().activate_minibuffer(
|
|
|
- MinibufferMode::ISearch, "I-search: ",
|
|
|
- [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search: " + input); }, nullptr
|
|
|
- );
|
|
|
- minibuffer_activated = true;
|
|
|
- } 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_) {
|
|
|
+ bool minibuffer_activated = false;
|
|
|
+ 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);
|
|
|
}
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (minibuffer_activated) {
|
|
|
- if (content_widget_) queue_redraw_all_windows(content_widget_);
|
|
|
+ }, nullptr
|
|
|
+ );
|
|
|
+ minibuffer_activated = true;
|
|
|
+ } else if (event == EditorEvent::FindFileMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::FilePath, "Find file: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ if (core_->load_file(input)) core_->set_message("Loaded");
|
|
|
+ else core_->set_message("Failed to load");
|
|
|
+ }, nullptr
|
|
|
+ );
|
|
|
+ minibuffer_activated = true;
|
|
|
+ } else if (event == EditorEvent::BufferSwitchMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::BufferName, "Switch to buffer: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ if (core_->switch_buffer_in_window(input)) core_->set_message("Switched");
|
|
|
+ else core_->set_message("Buffer not found");
|
|
|
+ }, nullptr
|
|
|
+ );
|
|
|
+ minibuffer_activated = true;
|
|
|
+ } else if (event == EditorEvent::KillBufferMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::BufferName, "Kill buffer: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ if (core_->close_buffer(input)) core_->set_message("Killed buffer");
|
|
|
+ else core_->set_message("Buffer not found");
|
|
|
+ }, nullptr
|
|
|
+ );
|
|
|
+ minibuffer_activated = true;
|
|
|
+ } 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_->set_theme(input);
|
|
|
+ core_->set_message("Switched to theme: " + input);
|
|
|
+ } else {
|
|
|
+ core_->set_message("Theme not found: " + input);
|
|
|
+ }
|
|
|
+ }, nullptr
|
|
|
+ );
|
|
|
+ minibuffer_activated = true;
|
|
|
+ } else if (event == EditorEvent::ISearchMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::ISearch, "I-search: ",
|
|
|
+ [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search: " + input); }, nullptr
|
|
|
+ );
|
|
|
+ minibuffer_activated = true;
|
|
|
+ } 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();
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
-
|
|
|
- void set_core(EditorCore* core) override {
|
|
|
- core_ = core;
|
|
|
+
|
|
|
+ // 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
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- // Helper to recursively find and redraw all drawing areas
|
|
|
- void 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);
|
|
|
- }
|
|
|
+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);
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
-private:
|
|
|
- EditorCore* core_;
|
|
|
- std::shared_ptr<Window> cached_active_window_; // Cached to prevent focus jumping during redraws
|
|
|
+// 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) {
|
|
|
+ key_name += "C-";
|
|
|
+ }
|
|
|
+ // Alt key is often mapped to Meta in Emacs-like editors
|
|
|
+ if (state & Gdk::ModifierType::ALT_MASK || state & 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.
|
|
|
|
|
|
- Glib::RefPtr<Gtk::Application> app_;
|
|
|
- Gtk::Window* window_ = nullptr; // Store window pointer for widget access only (not lifetime management)
|
|
|
- Gtk::DrawingArea* drawing_area_ = nullptr; // For single-window compatibility
|
|
|
- Gtk::Widget* content_widget_ = nullptr; // Will be either drawing_area_ or a split container
|
|
|
-
|
|
|
- // Cursor blinking
|
|
|
- bool cursor_visible_ = true;
|
|
|
- sigc::connection cursor_timer_connection_;
|
|
|
+ // 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;
|
|
|
+ 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;
|
|
|
+}
|
|
|
|
|
|
- // GtkRenderer instance
|
|
|
- std::unique_ptr<GtkRenderer> gtk_renderer_;
|
|
|
|
|
|
-protected:
|
|
|
- void 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_);
|
|
|
+bool GtkEditor::on_global_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state) {
|
|
|
+ if (!core_) return false;
|
|
|
|
|
|
- // Build initial layout (single window)
|
|
|
- rebuild_layout();
|
|
|
+ // 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
|
|
|
+ }
|
|
|
|
|
|
- // Handle window close event
|
|
|
- window_->signal_close_request().connect([this]() -> bool {
|
|
|
- // Cleanup before closing
|
|
|
- if (cursor_timer_connection_.connected()) {
|
|
|
- cursor_timer_connection_.disconnect();
|
|
|
+ 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;
|
|
|
}
|
|
|
- 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_);
|
|
|
+ // 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;
|
|
|
}
|
|
|
-
|
|
|
- // Set up cursor blinking timer (500ms intervals like Emacs)
|
|
|
- cursor_timer_connection_ = Glib::signal_timeout().connect(
|
|
|
- sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), 500
|
|
|
- );
|
|
|
+ return handled_by_minibuffer; // Return if minibuffer handled it
|
|
|
+ } else {
|
|
|
+ // Minibuffer not active, pass to main keybinding manager
|
|
|
+ KeyResult result = core_->keybinding_manager().process_key(lumacs_key_name);
|
|
|
+ if (result.status == KeyResult::Status::Processed) {
|
|
|
+ queue_redraw_all_windows(content_widget_);
|
|
|
+ return true;
|
|
|
+ } else if (result.status == KeyResult::Status::Pending) {
|
|
|
+ // Multi-key sequence in progress, wait for next key
|
|
|
+ queue_redraw_all_windows(content_widget_);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // If not processed by keybinding, let GTK handle it (e.g., for system shortcuts)
|
|
|
+ return false;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- // Rendering
|
|
|
- void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
|
|
- // Safety check - don't draw if core is null (during destruction)
|
|
|
- if (!core_ || !gtk_renderer_) return;
|
|
|
+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_);
|
|
|
|
|
|
- gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_visible_);
|
|
|
- }
|
|
|
+ // Build initial layout (single window)
|
|
|
+ rebuild_layout();
|
|
|
|
|
|
- // Cursor blinking callback
|
|
|
- bool 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 {
|
|
|
- cursor_visible_ = !cursor_visible_;
|
|
|
- core_->check_and_clear_message(); // Check and clear messages
|
|
|
- drawing_area_->queue_draw();
|
|
|
- } catch (...) {
|
|
|
- return false; // Stop timer on any exception
|
|
|
+ // 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;
|
|
|
|
|
|
- return true; // Continue timer
|
|
|
+ // 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_);
|
|
|
}
|
|
|
|
|
|
- // Rebuild the GTK layout to match the core's window tree
|
|
|
- void rebuild_layout() {
|
|
|
- if (!core_ || !window_) return;
|
|
|
+ // Set up cursor blinking timer (500ms intervals like Emacs)
|
|
|
+ cursor_timer_connection_ = Glib::signal_timeout().connect(
|
|
|
+ sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), 500
|
|
|
+ );
|
|
|
+
|
|
|
+ // Initialize completion popup
|
|
|
+ completion_popup_ = std::make_unique<GtkCompletionPopup>();
|
|
|
+ completion_popup_->set_transient_for(*window_); // Make it transient for the main window
|
|
|
+ completion_popup_->set_hide_on_close(true); // Allow Gtk to hide it cleanly
|
|
|
+ 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->signal_key_pressed().connect(
|
|
|
+ sigc::mem_fun(*this, &GtkEditor::on_global_key_pressed), true // Consume events handled by us
|
|
|
+ );
|
|
|
+ window_->add_controller(global_key_controller);
|
|
|
+}
|
|
|
|
|
|
- auto root_layout = core_->root_layout();
|
|
|
- if (!root_layout) return;
|
|
|
+// 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;
|
|
|
|
|
|
- // Remove existing content
|
|
|
- if (content_widget_) {
|
|
|
- window_->unset_child();
|
|
|
- }
|
|
|
+ gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_visible_);
|
|
|
+}
|
|
|
|
|
|
- // Clear the drawing area reference since we're rebuilding
|
|
|
- drawing_area_ = nullptr;
|
|
|
+// 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 {
|
|
|
+ cursor_visible_ = !cursor_visible_;
|
|
|
+ core_->check_and_clear_message(); // Check and clear messages
|
|
|
+ drawing_area_->queue_draw();
|
|
|
+ } catch (...) {
|
|
|
+ return false; // Stop timer on any exception
|
|
|
+ }
|
|
|
+
|
|
|
+ return true; // Continue timer
|
|
|
+}
|
|
|
|
|
|
- // Initialize cached active window to prevent focus jumping
|
|
|
- cached_active_window_ = core_->active_window();
|
|
|
+// Rebuild the GTK layout to match the core's window tree
|
|
|
+void GtkEditor::rebuild_layout() {
|
|
|
+ if (!core_ || !window_) return;
|
|
|
|
|
|
- // Create new layout based on the tree
|
|
|
- content_widget_ = create_widget_for_layout_node(root_layout);
|
|
|
- if (content_widget_) {
|
|
|
- window_->set_child(*content_widget_);
|
|
|
- }
|
|
|
+ auto root_layout = core_->root_layout();
|
|
|
+ if (!root_layout) return;
|
|
|
+
|
|
|
+ // Remove existing content
|
|
|
+ if (content_widget_) {
|
|
|
+ window_->unset_child();
|
|
|
}
|
|
|
|
|
|
- // Create GTK widget tree from LayoutNode tree
|
|
|
- Gtk::Widget* 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 = weak_window.lock() && gtk_renderer_) {
|
|
|
- gtk_renderer_->draw_window(cr, width, height, window, drawing_area, cached_active_window_, cursor_visible_);
|
|
|
+ // 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 input handling
|
|
|
- auto controller = Gtk::EventControllerKey::create();
|
|
|
- // Use weak reference to window for key handling
|
|
|
- std::weak_ptr<Window> weak_window_key = node->window;
|
|
|
- controller->signal_key_pressed().connect([this, weak_window_key](guint keyval, guint keycode, Gdk::ModifierType state) -> bool {
|
|
|
- // Ensure this window is active when it receives key input
|
|
|
- if (auto window = weak_window_key.lock()) {
|
|
|
- if (core_) {
|
|
|
- core_->set_active_window(window);
|
|
|
- cached_active_window_ = window; // Cache for rendering to prevent focus jumping
|
|
|
- }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ 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_->set_active_window(window);
|
|
|
+ cached_active_window_ = window; // Cache for rendering to prevent focus jumping
|
|
|
}
|
|
|
- // Now handled by GtkWindowController, so we need to instantiate it
|
|
|
- return false;
|
|
|
- }, false);
|
|
|
- drawing_area->add_controller(controller);
|
|
|
-
|
|
|
- // 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_->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();
|
|
|
- }
|
|
|
+ // 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();
|
|
|
- }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ 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();
|
|
|
}
|
|
|
- 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;
|
|
|
+ });
|
|
|
+
|
|
|
+ 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;
|
|
|
}
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
std::unique_ptr<IEditorView> create_gtk_editor() {
|
|
|
return std::make_unique<GtkEditor>();
|