gtk_editor.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. #include "lumacs/gtk_editor.hpp"
  2. #include "lumacs/editor_core.hpp"
  3. #include "lumacs/lua_api.hpp"
  4. #include "lumacs/keybinding.hpp"
  5. #include "lumacs/command_system.hpp"
  6. #include "lumacs/minibuffer_manager.hpp" // Include for MinibufferManager and MinibufferMode
  7. #include "lumacs/gtk_renderer.hpp" // Include for GtkRenderer
  8. #include "lumacs/gtk_completion_popup.hpp" // Include for GtkCompletionPopup
  9. #include "lumacs/buffer_manager.hpp" // Include for BufferManager
  10. #include "lumacs/window_manager.hpp" // Include for WindowManager
  11. #include "lumacs/kill_ring_manager.hpp" // Include for KillRingManager
  12. #include "lumacs/register_manager.hpp" // Include for RegisterManager
  13. #include "lumacs/macro_manager.hpp" // Include for MacroManager
  14. #include "lumacs/rectangle_manager.hpp" // Include for RectangleManager
  15. #include "lumacs/logger.hpp"
  16. #include <spdlog/spdlog.h>
  17. #include <filesystem>
  18. #include <vector>
  19. #include <functional> // For std::function
  20. #include <cctype> // For std::isalnum
  21. // Check if GTK is enabled in build
  22. #ifdef LUMACS_WITH_GTK
  23. #include <gtkmm.h>
  24. #include <pangomm.h>
  25. namespace lumacs {
  26. // LumacsWindow method implementations
  27. LumacsWindow::LumacsWindow(const Glib::RefPtr<Gtk::Application>& application)
  28. : Gtk::ApplicationWindow(application) {
  29. set_title("Lumacs - GTK4");
  30. set_default_size(1024, 768);
  31. }
  32. // GtkEditor method implementations
  33. GtkEditor::GtkEditor() : core_(nullptr), drawing_area_(nullptr), content_widget_(nullptr), last_cursor_move_time_(std::chrono::steady_clock::now()) {}
  34. GtkEditor::~GtkEditor() {
  35. // Disconnect cursor timer first to prevent callbacks during destruction
  36. if (cursor_timer_connection_.connected()) {
  37. cursor_timer_connection_.disconnect();
  38. }
  39. // Clear core pointer to prevent any callbacks during GTK cleanup
  40. core_ = nullptr;
  41. // If we still have an app reference, try to quit gracefully
  42. if (app_ && app_->is_registered()) {
  43. try {
  44. app_->quit();
  45. } catch (...) {
  46. // Ignore exceptions during cleanup
  47. }
  48. }
  49. // Clear widget pointers - GTK manages their lifetime
  50. // drawing_area_ is now a raw pointer managed by Gtk::make_managed, so no delete needed
  51. drawing_area_ = nullptr;
  52. window_ = nullptr;
  53. content_widget_ = nullptr; // Also managed by Gtk::make_managed
  54. // Let app_ RefPtr be destroyed naturally
  55. }
  56. void GtkEditor::init() {
  57. // Initialize GTK application
  58. app_ = Gtk::Application::create("org.lumacs.editor");
  59. app_->signal_activate().connect(sigc::mem_fun(*this, &GtkEditor::on_activate));
  60. }
  61. void GtkEditor::run() {
  62. // Run the application's event loop
  63. app_->run();
  64. }
  65. void GtkEditor::handle_editor_event(EditorEvent event) {
  66. // Safety check during destruction
  67. if (!core_ || !app_) return;
  68. // Handle layout changes
  69. if (event == EditorEvent::WindowLayoutChanged) {
  70. rebuild_layout();
  71. } else if (event == EditorEvent::CursorMoved) {
  72. // Cursor moved, reset blink timer and ensure cursor is visible
  73. last_cursor_move_time_ = std::chrono::steady_clock::now();
  74. cursor_visible_ = true;
  75. if (content_widget_) {
  76. // Redraw immediately to ensure cursor is visible at new position
  77. queue_redraw_all_windows(content_widget_);
  78. }
  79. }
  80. // Request redraw on most events - recursively find all drawing areas
  81. // This catches CursorMoved too, but we added explicit redraw for immediate visibility
  82. if (content_widget_ && event != EditorEvent::CursorMoved) { // Avoid double redraw for CursorMoved
  83. queue_redraw_all_windows(content_widget_);
  84. }
  85. if (event == EditorEvent::CommandMode) {
  86. core_->minibuffer_manager().activate_minibuffer(
  87. MinibufferMode::Command, "M-x ",
  88. [this](const std::string& input) {
  89. if (input == "quit" || input == "q") {
  90. app_->quit();
  91. } else {
  92. auto result = core_->command_system().execute(input, {}); // Pass empty args
  93. core_->set_message(result.message);
  94. }
  95. }, nullptr
  96. );
  97. } else if (event == EditorEvent::FindFileMode) {
  98. core_->minibuffer_manager().activate_minibuffer(
  99. MinibufferMode::FilePath, "Find file: ",
  100. [this](const std::string& input) {
  101. if (core_->buffer_manager().load_file(input)) core_->set_message("Loaded");
  102. else core_->set_message("Failed to load");
  103. }, nullptr
  104. );
  105. } else if (event == EditorEvent::BufferSwitchMode) {
  106. core_->minibuffer_manager().activate_minibuffer(
  107. MinibufferMode::BufferName, "Switch to buffer: ",
  108. [this](const std::string& input) {
  109. if (core_->buffer_manager().switch_buffer_in_window(input)) core_->set_message("Switched");
  110. else core_->set_message("Buffer not found");
  111. }, nullptr
  112. );
  113. } else if (event == EditorEvent::KillBufferMode) {
  114. core_->minibuffer_manager().activate_minibuffer(
  115. MinibufferMode::BufferName, "Kill buffer: ",
  116. [this](const std::string& input) {
  117. if (core_->buffer_manager().close_buffer(input)) core_->set_message("Killed buffer");
  118. else core_->set_message("Buffer not found");
  119. }, nullptr
  120. );
  121. } else if (event == EditorEvent::ThemeSelectionMode) {
  122. core_->minibuffer_manager().activate_minibuffer(
  123. MinibufferMode::ThemeName, "Set theme: ",
  124. [this](const std::string& input) {
  125. auto theme_names = core_->theme_manager().theme_names();
  126. auto it = std::find(theme_names.begin(), theme_names.end(), input);
  127. if (it != theme_names.end()) {
  128. core_->theme_manager().set_active_theme(input);
  129. core_->set_message("Switched to theme: " + input);
  130. } else {
  131. core_->set_message("Theme not found: " + input);
  132. }
  133. }, nullptr
  134. );
  135. } else if (event == EditorEvent::ThemeChanged) {
  136. if (gtk_renderer_) {
  137. gtk_renderer_->invalidate_cache();
  138. }
  139. // Redraw everything with new theme
  140. if (content_widget_) {
  141. queue_redraw_all_windows(content_widget_);
  142. }
  143. } else if (event == EditorEvent::ISearchMode) {
  144. core_->minibuffer_manager().activate_minibuffer(
  145. MinibufferMode::ISearch, "I-search: ",
  146. [](const std::string&) { /* Submit handled by return key in isearch */ },
  147. nullptr
  148. );
  149. core_->minibuffer_manager().start_isearch(true);
  150. } else if (event == EditorEvent::ISearchBackwardMode) {
  151. core_->minibuffer_manager().activate_minibuffer(
  152. MinibufferMode::ISearch, "I-search backward: ",
  153. [](const std::string&) { /* Submit handled by return key in isearch */ },
  154. nullptr
  155. );
  156. core_->minibuffer_manager().start_isearch(false);
  157. } else if (event == EditorEvent::TransientMessageCleared) {
  158. // Force redraw to clear the message
  159. if (content_widget_) queue_redraw_all_windows(content_widget_);
  160. } else if (event == EditorEvent::Quit) {
  161. // Disconnect timer before quitting to prevent segfault
  162. if (cursor_timer_connection_.connected()) {
  163. cursor_timer_connection_.disconnect();
  164. }
  165. // Use idle callback to quit safely after current event processing
  166. Glib::signal_idle().connect_once([this]() {
  167. if (app_) {
  168. app_->quit();
  169. }
  170. });
  171. }
  172. // Check if minibuffer is active AFTER processing event to decide popup visibility
  173. if (core_->minibuffer_manager().is_active()) {
  174. show_completion_popup(); // Show/hide completion popup when minibuffer state changes
  175. if (content_widget_) queue_redraw_all_windows(content_widget_);
  176. } else {
  177. hide_completion_popup(); // Minibuffer deactivated, hide popup
  178. }
  179. }
  180. void GtkEditor::set_core(EditorCore* core) {
  181. core_ = core;
  182. }
  183. // Helper to recursively find and redraw all drawing areas
  184. void GtkEditor::queue_redraw_all_windows(Gtk::Widget* widget) {
  185. if (!widget) return;
  186. if (auto drawing_area = dynamic_cast<Gtk::DrawingArea*>(widget)) {
  187. drawing_area->queue_draw();
  188. } else if (auto paned = dynamic_cast<Gtk::Paned*>(widget)) {
  189. if (auto start_child = paned->get_start_child()) {
  190. queue_redraw_all_windows(start_child);
  191. }
  192. if (auto end_child = paned->get_end_child()) {
  193. queue_redraw_all_windows(end_child);
  194. }
  195. }
  196. }
  197. // Helper to convert GDK keyval and modifiers to Lumacs key name
  198. std::string GtkEditor::get_lumacs_key_name(guint keyval, Gdk::ModifierType state) {
  199. std::string key_name;
  200. if ((state & Gdk::ModifierType::CONTROL_MASK) == Gdk::ModifierType::CONTROL_MASK) {
  201. key_name += "C-";
  202. }
  203. // Alt key is often mapped to Meta in Emacs-like editors
  204. if ((state & Gdk::ModifierType::ALT_MASK) == Gdk::ModifierType::ALT_MASK || (state & Gdk::ModifierType::META_MASK) == Gdk::ModifierType::META_MASK) {
  205. key_name += "M-";
  206. }
  207. // Shift is generally handled by the keyval itself for letters (e.g., 'A' vs 'a')
  208. // For special keys, we might want "S-ArrowUp" but for now, rely on keyval.
  209. // GDK_KEY_Tab does not have a unicode value, handle separately
  210. if (keyval == GDK_KEY_Tab) {
  211. key_name += "Tab";
  212. }
  213. else if (keyval == GDK_KEY_ISO_Left_Tab) { // Shift-Tab for cycling backwards
  214. key_name += "S-Tab";
  215. }
  216. else if (keyval >= GDK_KEY_space && keyval <= GDK_KEY_asciitilde) { // Printable ASCII
  217. key_name += static_cast<char>(gdk_keyval_to_unicode(keyval));
  218. } else {
  219. // Handle special keys
  220. switch (keyval) {
  221. case GDK_KEY_Return: key_name += "Return"; break;
  222. case GDK_KEY_Escape: key_name += "Escape"; break;
  223. case GDK_KEY_BackSpace: key_name += "Backspace"; break; // Fixed case sensitivity
  224. case GDK_KEY_Delete: key_name += "Delete"; break;
  225. case GDK_KEY_Up: key_name += "ArrowUp"; break;
  226. case GDK_KEY_Down: key_name += "ArrowDown"; break;
  227. case GDK_KEY_Left: key_name += "ArrowLeft"; break;
  228. case GDK_KEY_Right: key_name += "ArrowRight"; break;
  229. case GDK_KEY_Home: key_name += "Home"; break;
  230. case GDK_KEY_End: key_name += "End"; break;
  231. case GDK_KEY_Page_Up: key_name += "PageUp"; break;
  232. case GDK_KEY_Page_Down: key_name += "PageDown"; break;
  233. case GDK_KEY_F1: key_name += "F1"; break;
  234. case GDK_KEY_F2: key_name += "F2"; break;
  235. case GDK_KEY_F3: key_name += "F3"; break;
  236. case GDK_KEY_F4: key_name += "F4"; break;
  237. case GDK_KEY_F5: key_name += "F5"; break;
  238. case GDK_KEY_F6: key_name += "F6"; break;
  239. case GDK_KEY_F7: key_name += "F7"; break;
  240. case GDK_KEY_F8: key_name += "F8"; break;
  241. case GDK_KEY_F9: key_name += "F9"; break;
  242. case GDK_KEY_F10: key_name += "F10"; break;
  243. case GDK_KEY_F11: key_name += "F11"; break;
  244. case GDK_KEY_F12: key_name += "F12"; break;
  245. // Add more special keys as needed
  246. default:
  247. // Fallback for unhandled keys
  248. // Gdk::keyval_name(keyval) might give "dead_acute", which is not good.
  249. // Best to ignore unhandled keys for now or map explicitly.
  250. return ""; // Consume if we couldn't map to a Lumacs key (to avoid random chars appearing)
  251. }
  252. }
  253. return key_name;
  254. }
  255. bool GtkEditor::on_global_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state) {
  256. (void)keycode;
  257. spdlog::debug("GtkEditor::on_global_key_pressed: keyval={}, state={}", keyval, (int)state);
  258. if (!core_) return false;
  259. // Translate GDK event to Lumacs key name
  260. std::string lumacs_key_name = get_lumacs_key_name(keyval, state);
  261. if (lumacs_key_name.empty()) {
  262. return false; // Not a key we care about or couldn't map
  263. }
  264. if (core_->minibuffer_manager().is_active()) {
  265. // Minibuffer is active, keys should primarily go to it, or completion popup
  266. if (completion_popup_ && completion_popup_->get_visible()) {
  267. // If completion popup is visible, it takes precedence for navigation/selection keys
  268. if (lumacs_key_name == "ArrowUp") {
  269. completion_popup_->select_previous();
  270. return true;
  271. } else if (lumacs_key_name == "ArrowDown") {
  272. completion_popup_->select_next();
  273. return true;
  274. } else if (lumacs_key_name == "Return") {
  275. if (auto selected = completion_popup_->get_selected_candidate()) {
  276. on_completion_selected(selected.value());
  277. } else {
  278. // If no completion selected, but Return pressed, pass to minibuffer
  279. core_->minibuffer_manager().handle_key_event(lumacs_key_name);
  280. }
  281. return true;
  282. } else if (lumacs_key_name == "Escape") {
  283. on_completion_cancelled();
  284. return true;
  285. } else if (lumacs_key_name == "Tab" || lumacs_key_name == "S-Tab") {
  286. // Forward Tab to MinibufferManager for its internal cycle
  287. core_->minibuffer_manager().handle_key_event(lumacs_key_name);
  288. // Redraw popup to show updated selection
  289. show_completion_popup();
  290. return true;
  291. }
  292. }
  293. // If key is not handled by completion popup, or popup is not visible,
  294. // pass to MinibufferManager
  295. bool handled_by_minibuffer = core_->minibuffer_manager().handle_key_event(lumacs_key_name);
  296. // After any minibuffer key event, update and potentially show/hide completion
  297. if (handled_by_minibuffer) {
  298. if (core_->minibuffer_manager().get_completion_candidates().empty()) {
  299. hide_completion_popup();
  300. } else {
  301. show_completion_popup();
  302. }
  303. queue_redraw_all_windows(content_widget_); // Redraw minibuffer content
  304. } else {
  305. // If minibuffer didn't handle it, it could be a keybinding for the editor that
  306. // should still work while the minibuffer is active (e.g., C-g for quit).
  307. // For now, we assume if minibuffer is active, it consumes all relevant keys.
  308. // If the key is not handled, it probably means it's irrelevant to minibuffer input.
  309. // But we should still consume it to avoid propagating to editor keybindings accidentally.
  310. // Returning true consumes the event.
  311. return true;
  312. }
  313. return handled_by_minibuffer; // Return if minibuffer handled it
  314. } else {
  315. // Minibuffer not active, pass to main keybinding manager
  316. KeyProcessingResult result = core_->keybinding_manager().process_key(lumacs_key_name);
  317. if (result.type == KeyResult::Executed) {
  318. queue_redraw_all_windows(content_widget_);
  319. return true;
  320. } else if (result.type == KeyResult::Partial) {
  321. // Multi-key sequence in progress, wait for next key
  322. queue_redraw_all_windows(content_widget_);
  323. return true;
  324. } else if (result.type == KeyResult::Unbound) {
  325. // Check if it's a self-insertable character
  326. // We assume single characters without modifiers (except Shift) are self-insertable
  327. // if they are not bound to something else.
  328. // lumacs_key_name format: "a", "S-a", "C-a"
  329. bool has_ctrl = lumacs_key_name.find("C-") != std::string::npos;
  330. bool has_meta = lumacs_key_name.find("M-") != std::string::npos;
  331. // Allow Shift-key (e.g. "S-a" -> "A" is handled by GDK usually giving "A" directly if we use unicode,
  332. // but our get_lumacs_key_name handles it.
  333. // Actually, for printable chars, get_lumacs_key_name returns the unicode char (e.g. "A").
  334. // So we just check length == 1 and no modifiers.
  335. if (!has_ctrl && !has_meta && lumacs_key_name.length() == 1) {
  336. // Execute self-insert-command
  337. core_->command_system().execute("self-insert-command", {lumacs_key_name});
  338. // --- Macro Recording Logic for Self-Insert ---
  339. if (core_->is_recording_macro()) {
  340. core_->record_key_sequence(lumacs_key_name);
  341. }
  342. // --------------------------------------------
  343. queue_redraw_all_windows(content_widget_);
  344. return true;
  345. }
  346. }
  347. // If not processed by keybinding, let GTK handle it (e.g., for system shortcuts)
  348. return false;
  349. }
  350. }
  351. void GtkEditor::on_activate() {
  352. // Create main window and associate with the application
  353. // Note: The window is owned by the application through GObject reference counting
  354. // We just keep a raw pointer for access, but don't manage its lifetime
  355. window_ = new LumacsWindow(app_);
  356. // Build initial layout (single window)
  357. rebuild_layout();
  358. // Handle window close event
  359. window_->signal_close_request().connect([this]() -> bool {
  360. // Cleanup before closing
  361. if (cursor_timer_connection_.connected()) {
  362. cursor_timer_connection_.disconnect();
  363. }
  364. core_ = nullptr;
  365. drawing_area_ = nullptr;
  366. content_widget_ = nullptr;
  367. // Allow window to close
  368. return false; // false means "allow close"
  369. }, false);
  370. // Show window
  371. window_->present();
  372. if (drawing_area_) {
  373. drawing_area_->grab_focus();
  374. }
  375. // Initialize GtkRenderer after drawing_area_ is set
  376. if (core_ && drawing_area_) {
  377. gtk_renderer_ = std::make_unique<GtkRenderer>(*core_, *drawing_area_);
  378. }
  379. // Set up cursor blinking timer (BLINK_INTERVAL like Emacs)
  380. cursor_timer_connection_ = Glib::signal_timeout().connect(
  381. sigc::mem_fun(*this, &GtkEditor::on_cursor_blink), BLINK_INTERVAL.count()
  382. );
  383. // Initialize completion popup
  384. completion_popup_ = std::make_unique<GtkCompletionPopup>();
  385. // Popovers are automatically transient and handle their own hide_on_close logic.
  386. completion_popup_->signal_candidate_selected().connect(sigc::mem_fun(*this, &GtkEditor::on_completion_selected));
  387. completion_popup_->signal_cancelled().connect(sigc::mem_fun(*this, &GtkEditor::on_completion_cancelled));
  388. // Attach global key controller to the main window
  389. auto global_key_controller = Gtk::EventControllerKey::create();
  390. global_key_controller->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
  391. global_key_controller->signal_key_pressed().connect(
  392. sigc::mem_fun(*this, &GtkEditor::on_global_key_pressed), false
  393. );
  394. window_->add_controller(global_key_controller);
  395. }
  396. // Rendering
  397. void GtkEditor::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
  398. // Safety check - don't draw if core is destroyed or gtk_renderer_ not initialized
  399. if (!core_ || !gtk_renderer_) return;
  400. gtk_renderer_->on_draw(cr, width, height, cached_active_window_, cursor_visible_);
  401. }
  402. // Cursor blinking callback
  403. bool GtkEditor::on_cursor_blink() {
  404. // Safety check - don't blink if core is destroyed or no drawing area
  405. if (!core_ || !drawing_area_ || !app_) {
  406. return false; // Stop the timer
  407. }
  408. // Double check that the app is still running
  409. if (!app_->is_registered()) {
  410. return false; // Stop the timer
  411. }
  412. try {
  413. auto now = std::chrono::steady_clock::now();
  414. if (now - last_cursor_move_time_ > BLINK_STATIONARY_THRESHOLD) {
  415. // Only blink if cursor has been stationary for a while
  416. cursor_visible_ = !cursor_visible_;
  417. core_->check_and_clear_message(); // Check and clear messages
  418. drawing_area_->queue_draw();
  419. } else {
  420. // Cursor is still moving or just stopped, keep it visible
  421. cursor_visible_ = true;
  422. // No need to queue_draw here, it will be done on CursorMoved or next blink.
  423. }
  424. } catch (...) {
  425. return false; // Stop timer on any exception
  426. }
  427. return true; // Continue timer
  428. }
  429. // Rebuild the GTK layout to match the core's window tree
  430. void GtkEditor::rebuild_layout() {
  431. if (!core_ || !window_) return;
  432. auto root_layout = core_->root_layout();
  433. if (!root_layout) return;
  434. // Remove existing content
  435. if (content_widget_) {
  436. window_->unset_child();
  437. }
  438. // Clear the drawing area reference since we're rebuilding
  439. drawing_area_ = nullptr;
  440. // Initialize cached active window to prevent focus jumping
  441. cached_active_window_ = core_->active_window();
  442. // Create new layout based on the tree
  443. content_widget_ = create_widget_for_layout_node(root_layout);
  444. if (content_widget_) {
  445. window_->set_child(*content_widget_);
  446. }
  447. }
  448. // Create GTK widget tree from LayoutNode tree
  449. Gtk::Widget* GtkEditor::create_widget_for_layout_node(std::shared_ptr<LayoutNode> node) {
  450. if (!node) return nullptr;
  451. if (node->type == LayoutNode::Type::Leaf) {
  452. // Create a new DrawingArea for this window
  453. auto drawing_area = Gtk::make_managed<Gtk::DrawingArea>();
  454. // Set up drawing for this specific window
  455. // Use a weak reference to the window to avoid crashes if the layout is rebuilt
  456. std::weak_ptr<Window> weak_window = node->window;
  457. drawing_area->set_draw_func([this, weak_window, drawing_area](const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
  458. if (auto window_ptr = weak_window.lock()) { // Correctly capture shared_ptr
  459. if (gtk_renderer_) {
  460. gtk_renderer_->draw_window(cr, width, height, window_ptr, drawing_area, cached_active_window_, cursor_visible_);
  461. }
  462. }
  463. });
  464. drawing_area->set_focusable(true);
  465. // Add click handling to set active window explicitly and move cursor
  466. // We use GestureClick instead of EventControllerFocus to avoid spurious focus changes
  467. auto click_controller = Gtk::GestureClick::create();
  468. std::weak_ptr<Window> weak_window_click = node->window;
  469. click_controller->signal_pressed().connect([this, weak_window_click, drawing_area](int /*n_press*/, double x, double y) {
  470. if (auto window = weak_window_click.lock()) {
  471. // 1. Activate Window
  472. if (core_ && core_->active_window() != window) {
  473. core_->window_manager().set_active_window(window);
  474. cached_active_window_ = window; // Cache for rendering to prevent focus jumping
  475. }
  476. // IMPORTANT: Grab keyboard focus for this widget
  477. drawing_area->grab_focus();
  478. // 2. Move Cursor
  479. if (gtk_renderer_) {
  480. if (auto pos = gtk_renderer_->resolve_screen_pos(window, x, y)) {
  481. window->set_cursor(*pos);
  482. // Clear mark on simple click
  483. window->buffer().deactivate_mark();
  484. drawing_area->queue_draw();
  485. }
  486. }
  487. }
  488. });
  489. drawing_area->add_controller(click_controller);
  490. // Add Drag Gesture for Selection
  491. auto drag_controller = Gtk::GestureDrag::create();
  492. std::weak_ptr<Window> weak_window_drag = node->window;
  493. drag_controller->signal_drag_begin().connect([this, weak_window_drag, drawing_area](double x, double y) {
  494. if (auto window = weak_window_drag.lock()) {
  495. if (gtk_renderer_) {
  496. if (auto pos = gtk_renderer_->resolve_screen_pos(window, x, y)) {
  497. // Set mark at start of drag
  498. window->buffer().set_mark(*pos);
  499. window->set_cursor(*pos);
  500. drawing_area->queue_draw();
  501. }
  502. }
  503. }
  504. });
  505. drag_controller->signal_drag_update().connect([this, weak_window_drag, drawing_area, drag_controller](double dx, double dy) {
  506. if (auto window = weak_window_drag.lock()) {
  507. double start_x, start_y;
  508. if (drag_controller->get_start_point(start_x, start_y) && gtk_renderer_) {
  509. double current_x = start_x + dx;
  510. double current_y = start_y + dy;
  511. if (auto pos = gtk_renderer_->resolve_screen_pos(window, current_x, current_y)) {
  512. window->set_cursor(*pos);
  513. drawing_area->queue_draw();
  514. }
  515. }
  516. }
  517. });
  518. drawing_area->add_controller(drag_controller);
  519. // Add scroll handling
  520. auto scroll_controller = Gtk::EventControllerScroll::create();
  521. scroll_controller->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
  522. std::weak_ptr<Window> weak_window_scroll = node->window;
  523. scroll_controller->signal_scroll().connect([weak_window_scroll, drawing_area](double /*dx*/, double dy) -> bool {
  524. if (auto window = weak_window_scroll.lock()) {
  525. // dy is usually 1.0 or -1.0 for wheel steps
  526. // Scroll 3 lines per step
  527. int lines = static_cast<int>(dy * 3.0);
  528. if (lines != 0) {
  529. window->scroll_lines(lines);
  530. drawing_area->queue_draw();
  531. }
  532. return true;
  533. }
  534. return false;
  535. }, true);
  536. drawing_area->add_controller(scroll_controller);
  537. // Context menus and tooltips removed per user request (Phase A.1)
  538. // Store reference for single-window compatibility
  539. if (!drawing_area_) {
  540. drawing_area_ = drawing_area;
  541. }
  542. return drawing_area;
  543. } else {
  544. // Create a paned container for splits
  545. Gtk::Paned* paned = nullptr;
  546. if (node->type == LayoutNode::Type::HorizontalSplit) {
  547. paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::VERTICAL);
  548. } else { // VerticalSplit
  549. paned = Gtk::make_managed<Gtk::Paned>(Gtk::Orientation::HORIZONTAL);
  550. }
  551. // Recursively create children
  552. auto child1 = create_widget_for_layout_node(node->child1);
  553. auto child2 = create_widget_for_layout_node(node->child2);
  554. if (child1) paned->set_start_child(*child1);
  555. if (child2) paned->set_end_child(*child2);
  556. // Set initial position based on ratio
  557. // Use signal_map to set position when widget is ready
  558. paned->signal_map().connect([paned, node](){
  559. int width = paned->get_width();
  560. int height = paned->get_height();
  561. int size = (paned->get_orientation() == Gtk::Orientation::HORIZONTAL) ? width : height;
  562. // Fallback if size not yet available
  563. if (size <= 1) size = 1000; // Assume a reasonable default window size
  564. paned->set_position(static_cast<int>(size * node->ratio));
  565. });
  566. return paned;
  567. }
  568. }
  569. // === Completion Popup Helpers ===
  570. void GtkEditor::show_completion_popup() {
  571. if (!completion_popup_ || !window_) return;
  572. auto candidates = core_->minibuffer_manager().get_completion_candidates();
  573. if (candidates.empty()) {
  574. hide_completion_popup();
  575. return;
  576. }
  577. // Minibuffer is rendered at the bottom of the main window/drawing area.
  578. // We need to position the popover relative to the main window, pointing to the minibuffer area.
  579. Gdk::Rectangle rect;
  580. rect.set_x(0); // Minibuffer starts at x=0
  581. rect.set_y(window_->get_height() - 1); // Last line of the window
  582. rect.set_width(window_->get_width());
  583. rect.set_height(1); // Small height to represent the minibuffer line
  584. completion_popup_->show_popup(candidates, 0, *window_, rect.get_x(), rect.get_y());
  585. }
  586. void GtkEditor::hide_completion_popup() {
  587. if (completion_popup_) {
  588. completion_popup_->hide_popup();
  589. }
  590. }
  591. void GtkEditor::on_completion_selected(CompletionCandidate candidate) {
  592. core_->minibuffer_manager().set_input_buffer(candidate.text);
  593. hide_completion_popup();
  594. }
  595. void GtkEditor::on_completion_cancelled() {
  596. hide_completion_popup();
  597. }
  598. std::unique_ptr<IEditorView> create_gtk_editor() {
  599. return std::make_unique<GtkEditor>();
  600. }
  601. } // namespace lumacs
  602. #else // LUMACS_WITH_GTK not defined
  603. namespace lumacs {
  604. std::unique_ptr<IEditorView> create_gtk_editor() {
  605. spdlog::error("Error: Lumacs was built without GTK support.");
  606. return nullptr;
  607. }
  608. } // namespace lumacs
  609. #endif