/*
 * ct_main_win_events.cc
 *
 * Copyright 2009-2026
 * Giuseppe Penone <giuspen@gmail.com>
 * Evgenii Gurianov <https://github.com/txe>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include "ct_main_win.h"
#include "ct_actions.h"
#include "ct_list.h"

void CtMainWin::_on_treeview_cursor_changed()
{
    CtTreeIter treeIter = curr_tree_iter();
    if (not treeIter) {
        // just removed the last node on the tree?
        _prevTreeIter = treeIter;
        return;
    }
    const gint64 nodeId = treeIter.get_node_id();
    const gint64 nodeIdDataHolder = treeIter.get_node_id_data_holder();
    if (_prevTreeIter) {
        const gint64 prevNodeId = _prevTreeIter.get_node_id();
        if (prevNodeId == nodeId) {
            return;
        }
        const gint64 prevNodeIdDataHolder = _prevTreeIter.get_node_id_data_holder();
        Glib::RefPtr<Gtk::TextBuffer> pTextBuffer = _prevTreeIter.get_node_text_buffer();
        if (pTextBuffer->get_modified()) {
            _fileSaveNeeded = true;
            pTextBuffer->set_modified(false);
            _ctStateMachine.update_state(_prevTreeIter);
        }
        const int scr = round(_scrolledwindowText.get_vadjustment()->get_value());
        const int cur = pTextBuffer->property_cursor_position();
        _nodesVScrollPos[prevNodeIdDataHolder] = scr;
        _nodesCursorPos[prevNodeIdDataHolder] = cur;
        //spdlog::debug("W[{}] scr={}, cur={}", prevNodeIdDataHolder, scr, cur);
    }

    Glib::RefPtr<Gtk::TextBuffer> pTextBuffer = treeIter.get_node_text_buffer();
    if (not pTextBuffer) {
        CtDialogs::error_dialog(str::format(_("Failed to retrieve the content of the node '%s'"), treeIter.get_node_name().raw()), *this);
        if (_prevTreeIter) {
            _uCtTreeview->set_cursor_safe(_prevTreeIter);
        }
        return;
    }
    _uCtTreestore->text_view_apply_textbuffer(treeIter, &_ctTextview);

    if (user_active()) {
        auto mapScrIter = _nodesVScrollPos.find(nodeIdDataHolder);
        auto mapCurIter = _nodesCursorPos.find(nodeIdDataHolder);
        if (mapScrIter != _nodesVScrollPos.end() and
            mapCurIter != _nodesCursorPos.end() and mapCurIter->second > 0)
        {
            const int scr = mapScrIter->second;
            const int cur = mapCurIter->second;
            text_view_apply_cursor_position(treeIter, cur, scr);
            //spdlog::debug("R[{}] scr={}, cur={}", nodeIdDataHolder, scr, cur);
        }
        else {
            text_view_apply_cursor_position(treeIter, 0, 0);
        }

        const bool is_bookmarked = _uCtTreestore->is_node_bookmarked(nodeId);
        menu_update_bookmark_menu_item(is_bookmarked);
        window_header_update();
        window_header_update_lock_icon(treeIter.get_node_read_only());
        window_header_update_ghost_icon(treeIter.get_node_is_excluded_from_search() or treeIter.get_node_children_are_excluded_from_search());
        window_header_update_bookmark_icon(is_bookmarked);
        update_selected_node_statusbar_info();
    }

    _ctStateMachine.node_selected_changed(nodeIdDataHolder);

    _prevTreeIter = treeIter;
}

bool CtMainWin::_on_treeview_button_release_event(GdkEventButton* event)
{
    if (event->button == 3) {
        _uCtMenu->get_popup_menu(CtMenu::POPUP_MENU_TYPE::Node)->popup(event->button, event->time);
        return true;
    }
    return false;
}

bool CtMainWin::_on_window_key_press_event(GdkEventKey* event)
{
    if (event->state & GDK_CONTROL_MASK) {
        if (GDK_KEY_Tab == event->keyval or GDK_KEY_ISO_Left_Tab == event->keyval) {
            _uCtActions->toggle_focus_tree_text();
            return true;
        }
    }
    return false;
}

void CtMainWin::_on_treeview_event_after(GdkEvent* event)
{
    if (event->type == GDK_BUTTON_PRESS and event->button.button == 1) {
        if (_pCtConfig->treeClickFocusText) {
            _ctTextview.mm().grab_focus();
        }
        if (_pCtConfig->treeClickExpand) {
            _tree_just_auto_expanded = false;
            Gtk::TreePath path_at_click;
            if (get_tree_view().get_path_at_pos((int)event->button.x, (int)event->button.y, path_at_click)) {
                if (!get_tree_view().row_expanded(path_at_click)) {
                    get_tree_view().expand_row(path_at_click, false);
                    _tree_just_auto_expanded = true;
                }
            }
        }
    }
    if (event->type == GDK_BUTTON_PRESS && event->button.button == 2 /* wheel click */) {
        auto path = get_tree_store().get_path(curr_tree_iter());
        if (_uCtTreeview->row_expanded(path))
            _uCtTreeview->collapse_row(path);
        else
            _uCtTreeview->expand_row(path, true);
    }
    else if (event->type == GDK_2BUTTON_PRESS and event->button.button == 1) {
        // _on_treeview_row_activated works better for double-click
        // but it doesn't work with one click
        // in this case use real double click
        if (_pCtConfig->treeClickExpand) {
            Gtk::TreePath path_at_click;
            if (get_tree_view().get_path_at_pos((int)event->button.x, (int)event->button.y, path_at_click)) {
                if (path_at_click == get_tree_store().get_path(curr_tree_iter()))
                {
                    if (_uCtTreeview->row_expanded(path_at_click))
                        _uCtTreeview->collapse_row(path_at_click);
                    else
                        _uCtTreeview->expand_row(path_at_click, false);
                }
            }
        }
    }
}

void CtMainWin::_on_treeview_row_activated(const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn*)
{
    // _on_treeview_row_activated works better for double-click
    // but it doesn't work with one click
    // in this case use real double click
    if (_pCtConfig->treeClickExpand)
        return;
    if (_uCtTreeview->row_expanded(path))
        _uCtTreeview->collapse_row(path);
    else
        _uCtTreeview->expand_row(path, false);
}

void CtMainWin::_on_treeview_row_expanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path&)
{
    if (not iter) {
        return;
    }
    const gint64 node_id = iter->get_value(_uCtTreestore->get_columns().colNodeUniqueId);
    _treeExpandedNodeIds.insert(node_id);
    if (_treeRestoreInProgress) {
        return;
    }
    _treeRestoreInProgress = true;
    _treeview_restore_expanded_descendants(iter);
    _treeRestoreInProgress = false;
}

void CtMainWin::_on_treeview_row_collapsed(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path&)
{
    if (not iter) {
        return;
    }
    const gint64 node_id = iter->get_value(_uCtTreestore->get_columns().colNodeUniqueId);
    _treeExpandedNodeIds.erase(node_id);
}

void CtMainWin::_treeview_restore_expanded_descendants(const Gtk::TreeModel::iterator& iter)
{
    if (not iter) {
        return;
    }
    const CtTreeModelColumns& columns = _uCtTreestore->get_columns();
    const Gtk::TreeNodeChildren children = iter->children();
    for (auto child_iter = children.begin(); child_iter != children.end(); ++child_iter) {
        const gint64 node_id = child_iter->get_value(columns.colNodeUniqueId);
        if (_treeExpandedNodeIds.count(node_id) > 0) {
            Gtk::TreePath path = _uCtTreestore->get_path(child_iter);
            if (not _uCtTreeview->row_expanded(path)) {
                _uCtTreeview->expand_row(path, false);
            }
        }
        _treeview_restore_expanded_descendants(child_iter);
    }
}

bool CtMainWin::_on_treeview_test_collapse_row(const Gtk::TreeModel::iterator&,const Gtk::TreeModel::Path&)
{
    // to fix one click
    if (_pCtConfig->treeClickExpand) {
        if (_tree_just_auto_expanded) {
            _tree_just_auto_expanded = false;
            return true;
        }
    }
    return false;
}

bool CtMainWin::_on_treeview_key_press_event(GdkEventKey* event)
{
    if (not curr_tree_iter()) return false;
    if (event->state & GDK_MOD1_MASK) {
    }
    else if (event->state & GDK_CONTROL_MASK) {
        auto reduce = [](Gtk::TreeModel::iterator first, std::function<Gtk::TreeModel::iterator(Gtk::TreeModel::iterator)> operatr)->Gtk::TreeModel::iterator{
            Gtk::TreeModel::iterator result;
            for (auto next = operatr(first); next; next = operatr(next))
                result = next;
            return result;
        };
        if (GDK_KEY_Up == event->keyval) {
            auto fist_sibling = reduce(curr_tree_iter(), [](Gtk::TreeModel::iterator iter) { return --iter;});
            if (fist_sibling)
                get_tree_view().set_cursor_safe(fist_sibling);
            return true;
        }
        if (GDK_KEY_Down == event->keyval) {
            auto last_sibling = reduce(curr_tree_iter(), [](Gtk::TreeModel::iterator iter) { return ++iter;});
            if (last_sibling)
                get_tree_view().set_cursor_safe(last_sibling);
            return true;
        }
        if (GDK_KEY_Left == event->keyval) {
            auto fist_parent = reduce(curr_tree_iter(), [](Gtk::TreeModel::iterator iter) { return iter->parent();});
            if (fist_parent)
                get_tree_view().set_cursor_safe(fist_parent);
            return true;
        }
        if (GDK_KEY_Right == event->keyval) {
            auto last_child = reduce(curr_tree_iter(), [](Gtk::TreeModel::iterator iter) { return iter->children().begin();});
            if (last_child)
                get_tree_view().set_cursor_safe(last_child);
            return true;
        }
        if (GDK_KEY_plus == event->keyval or GDK_KEY_KP_Add == event->keyval or GDK_KEY_equal == event->keyval) {
            _zoom_tree(true);
            return true;
        }
        if (GDK_KEY_minus == event->keyval or GDK_KEY_KP_Subtract == event->keyval) {
            _zoom_tree(false);
            return true;
        }
        if (GDK_KEY_0 == event->keyval or GDK_KEY_KP_0 == event->keyval) {
            _zoom_tree(std::nullopt);
            return true;
        }
    }
    else {
        if (GDK_KEY_Left == event->keyval) {
            if (_uCtTreeview->row_expanded(_uCtTreestore->get_path(curr_tree_iter())))
                _uCtTreeview->collapse_row(_uCtTreestore->get_path(curr_tree_iter()));
            else if (curr_tree_iter().parent())
                get_tree_view().set_cursor_safe(curr_tree_iter().parent());
            return true;
        }
        if (GDK_KEY_Right == event->keyval) {
            get_tree_view().expand_row(get_tree_store().get_path(curr_tree_iter()), false);
            return true;
        }
        if (GDK_KEY_Return == event->keyval or GDK_KEY_KP_Enter == event->keyval) {
            auto p = get_tree_store().get_path(curr_tree_iter());
            if (_uCtTreeview->row_expanded(p))
                _uCtTreeview->collapse_row(p);
            else
                _uCtTreeview->expand_row(p, false);
            return true;
        }
        if (GDK_KEY_Menu == event->keyval) {
            _uCtMenu->get_popup_menu(CtMenu::POPUP_MENU_TYPE::Node)->popup(0, event->time);
            return true;
        }
        if (GDK_KEY_Tab == event->keyval or GDK_KEY_ISO_Left_Tab == event->keyval) {
            _uCtActions->toggle_focus_tree_text();
            return true;
        }
        if (GDK_KEY_Delete == event->keyval) {
            _uCtActions->node_delete();
            return true;
        }
        {
            const gunichar unicode_char = gdk_keyval_to_unicode(event->keyval);
            if (unicode_char != 0 && g_unichar_isalnum(unicode_char)) {
                Glib::ustring entryStr{1, unicode_char};
                _uCtActions->command_selnode_str(entryStr);
                return true;
            }
        }
    }
    return false;
}

bool CtMainWin::_on_treeview_popup_menu()
{
    _uCtMenu->get_popup_menu(CtMenu::POPUP_MENU_TYPE::Node)->popup(0, 0);
    return true;
}

bool CtMainWin::_on_treeview_scroll_event(GdkEventScroll* event)
{
    if (!(event->state & GDK_CONTROL_MASK))
        return false;
    if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_DOWN)
        _zoom_tree(event->direction == GDK_SCROLL_DOWN);
    if (event->direction == GDK_SCROLL_SMOOTH && event->delta_y != 0)
        _zoom_tree(event->delta_y < 0);
    return true;
}

// Extend the Default Right-Click Menu
void CtMainWin::_on_textview_populate_popup(Gtk::Menu* menu)
{
    if (curr_tree_iter().get_node_syntax_highlighting() == CtConst::RICH_TEXT_ID) {
        for (auto child: menu->get_children()) {
            if (auto menuItem = dynamic_cast<Gtk::MenuItem*>(child)) {
                if (menuItem->get_label() == "_Paste") {
                    menuItem->set_sensitive(true);
                    break;
                }
            }
        }

        if (hovering_link_iter_offset() >= 0) {
            Gtk::TextIter target_iter = curr_buffer()->get_iter_at_offset(hovering_link_iter_offset());
            if (target_iter) {
                bool do_set_cursor = true;
                if (curr_buffer()->get_has_selection()) {
                    Gtk::TextIter iter_sel_start, iter_sel_end;
                    curr_buffer()->get_selection_bounds(iter_sel_start, iter_sel_end);
                    if (hovering_link_iter_offset() >= iter_sel_start.get_offset()
                        and hovering_link_iter_offset() <= iter_sel_end.get_offset())
                    {
                        do_set_cursor = false;
                    }
                }
                if (do_set_cursor) curr_buffer()->place_cursor(target_iter);
            }
            //for (auto iter : menu->get_children()) menu->remove(*iter);
            get_ct_menu().build_popup_menu(menu, CtMenu::POPUP_MENU_TYPE::Link);
        }
        else {
            //for (auto iter : menu->get_children()) menu->remove(*iter);
            get_ct_menu().build_popup_menu(menu, CtMenu::POPUP_MENU_TYPE::Text);
        }
    }
    else {
        //for (auto iter : menu->get_children()) menu->remove(*iter);
        _uCtActions->getCtMainWin()->get_ct_menu().build_popup_menu(menu, CtMenu::POPUP_MENU_TYPE::Code);
    }
}

// Update the cursor image if the pointer moved
bool CtMainWin::_on_textview_motion_notify_event(GdkEventMotion* event)
{
    Gtk::TextView& textView = _ctTextview.mm();
    if (not textView.get_cursor_visible()) {
        textView.set_cursor_visible(true);
    }
    //textView.reset_cursor_blink();
    CtTreeIter ctTreeIter = curr_tree_iter();
    if (ctTreeIter.get_node_is_code()) {
        textView.get_window(Gtk::TEXT_WINDOW_TEXT)->set_cursor(Gdk::Cursor::create(Gdk::XTERM));
        return false;
    }
    int x, y;
    textView.window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT, (int)event->x, (int)event->y, x, y);
    _ctTextview.cursor_and_tooltips_handler(x, y);
    return false;
}

// Update the cursor image if the window becomes visible (e.g. when a window covering it got iconified)
bool CtMainWin::_on_textview_visibility_notify_event(GdkEventVisibility*)
{
    CtTreeIter ct_tree_iter = curr_tree_iter();
    if (not ct_tree_iter) {
        return false;
    }
    const auto syntax_highl = ct_tree_iter.get_node_syntax_highlighting();
    if (CtConst::RICH_TEXT_ID != syntax_highl and CtConst::PLAIN_TEXT_ID != syntax_highl) {
        _ctTextview.mm().get_window(Gtk::TEXT_WINDOW_TEXT)->set_cursor(Gdk::Cursor::create(Gdk::XTERM));
        return false;
    }
    int x, y, bx, by;
    Gdk::ModifierType mask;
    _ctTextview.mm().get_window(Gtk::TEXT_WINDOW_TEXT)->get_pointer(x, y, mask);
    _ctTextview.mm().window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT, x, y, bx, by);
    _ctTextview.cursor_and_tooltips_handler(bx, by);
    return false;
}

bool CtMainWin::_on_window_configure_event(GdkEventConfigure*/*configure_event*/)
{
    auto f_update_configs = [this](){
        _pCtConfig->winIsMaximised = is_maximized();
        if (not _pCtConfig->winIsMaximised) {
            get_position(_pCtConfig->winRect[0], _pCtConfig->winRect[1]);
            get_size(_pCtConfig->winRect[2], _pCtConfig->winRect[3]);
        }
    };
    const bool prevWinIsMaximised = _pCtConfig->winIsMaximised;
    f_update_configs();
    if (prevWinIsMaximised and _pCtConfig->winIsMaximised) {
        // when unmaximising, the maximised flag takes a moment to go down
        Glib::signal_idle().connect_once([f_update_configs](){
            f_update_configs();
        });
    }
    return false;
}

void CtMainWin::_on_textview_size_allocate(Gtk::Allocation& allocation)
{
    // When treeRightSide=true, pack1 is the text area (Gtk::EXPAND) and pack2 is the tree (Gtk::FILL).
    // property_position() is the pack1 (text) width, which grows as the window expands.
    // To avoid hpanedPos growing on each start, always store the tree width (stable FILL child).
    if (_pCtConfig->treeRightSide) {
        _pCtConfig->hpanedPos = _hPaned.get_allocation().get_width() - _hPaned.property_position();
    }
    else {
        _pCtConfig->hpanedPos = _hPaned.property_position();
    }
    _pCtConfig->vpanedPos = _vPaned.property_position();
    if (_prevTextviewWidth == 0) {
        _prevTextviewWidth = allocation.get_width();
    }
    else if (_prevTextviewWidth != allocation.get_width()) {
        _prevTextviewWidth = allocation.get_width();
        CtTreeIter ct_tree_iter = curr_tree_iter();
        if (ct_tree_iter) {
            std::list<CtAnchoredWidget*> widgets = ct_tree_iter.get_anchored_widgets_fast();
            for (CtAnchoredWidget* pWidget : widgets) {
                if (CtCodebox* pCodebox = dynamic_cast<CtCodebox*>(pWidget)) {
                    if (not pCodebox->get_width_in_pixels()) {
                        pCodebox->apply_width_height(allocation.get_width());
                    }
                }
            }
        }
    }
}

bool CtMainWin::_on_textview_event(GdkEvent* event)
{
    if (event->type != GDK_KEY_PRESS)
        return false;

    auto curr_buffer = _ctTextview.get_buffer();
    if (event->key.state & Gdk::SHIFT_MASK) {
        if (event->key.keyval == GDK_KEY_ISO_Left_Tab) {
            if (not curr_buffer->get_has_selection()) {
                auto iter_insert = curr_buffer->get_insert()->get_iter();
                CtListInfo list_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(iter_insert);
                if (list_info and list_info.level) {
                    _ctTextview.list_change_level(iter_insert, list_info, false);
                    return true;
                }
            }
            else {
                // Apply Shift+Tab (decrease level) to all selected list items
                Gtk::TextIter iter_sel_start, iter_sel_end;
                curr_buffer->get_selection_bounds(iter_sel_start, iter_sel_end);
                // Collect list items paragraph by paragraph (bottom to top to preserve offsets)
                std::vector<std::pair<Gtk::TextIter, CtListInfo>> list_items;
                Gtk::TextIter iter_scan = iter_sel_end;
                const int sel_start_line = iter_sel_start.get_line();
                // Walk lines from bottom to top within the selection
                while (true) {
                    Gtk::TextIter line_iter = iter_scan;
                    line_iter.set_line_offset(0);
                    const bool is_first_line = (line_iter.get_line() <= sel_start_line);
                    // For the first selected line use iter_sel_start as anchor so that
                    // get_paragraph_list_info can walk back to the real line start even
                    // when the selection begins after the leading indentation spaces.
                    Gtk::TextIter anchor = is_first_line ? iter_sel_start : line_iter;
                    CtListInfo list_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(anchor);
                    if (list_info and list_info.level) {
                        list_items.emplace_back(anchor, list_info);
                    }
                    if (is_first_line or line_iter.get_line() == 0) break;
                    // move to previous line
                    iter_scan = curr_buffer->get_iter_at_line(line_iter.get_line() - 1);
                }
                if (not list_items.empty()) {
                    for (auto& item : list_items) {
                        auto iter_insert = curr_buffer->get_iter_at_offset(item.second.startoffs);
                        CtListInfo refreshed_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(iter_insert);
                        if (refreshed_info and refreshed_info.level) {
                            _ctTextview.list_change_level(iter_insert, refreshed_info, false);
                        }
                    }
                    return true;
                }
            }
        }
    }
    if (GDK_KEY_Return == event->key.keyval or GDK_KEY_KP_Enter == event->key.keyval) {
        auto iter_insert = curr_buffer->get_insert()->get_iter();
        if (iter_insert)
            cursor_key_press() = iter_insert.get_offset();
        else
            cursor_key_press() = -1;
        // print "self.cursor_key_press", self.cursor_key_press
    }
    else if (GDK_KEY_Menu == event->key.keyval) {
        if (curr_tree_iter().get_node_syntax_highlighting() == CtConst::RICH_TEXT_ID) {
            if (not curr_buffer->get_has_selection()) return false;
            Gtk::TextIter iter_sel_start, iter_sel_end;
            curr_buffer->get_selection_bounds(iter_sel_start, iter_sel_end);
            int num_chars = iter_sel_end.get_offset() - iter_sel_start.get_offset();
            if (num_chars != 1) return false;
            auto widgets = curr_tree_iter().get_anchored_widgets(iter_sel_start.get_offset(), iter_sel_start.get_offset());
            if (widgets.empty()) return false;
            if (CtImageAnchor* anchor = dynamic_cast<CtImageAnchor*>(widgets.front())) {
                _uCtActions->curr_anchor_anchor = anchor;
                _uCtActions->object_set_selection(anchor);
                gtk_menu_popup_at_pointer(_uCtMenu->get_popup_menu(CtMenu::POPUP_MENU_TYPE::Anchor)->gobj(), event);
            }
            else if (CtImagePng* image = dynamic_cast<CtImagePng*>(widgets.front())) {
                _uCtActions->curr_image_anchor = image;
                _uCtActions->object_set_selection(image);
                _uCtMenu->find_action("img_link_dismiss")->signal_set_visible.emit(not image->get_link().empty());
                gtk_menu_popup_at_pointer(_uCtMenu->get_popup_menu(CtMenu::POPUP_MENU_TYPE::Image)->gobj(), event);
            }
            return true;
        }
    }
    else if (GDK_KEY_Tab == event->key.keyval or GDK_KEY_ISO_Left_Tab == event->key.keyval) {
        if (not curr_buffer->get_has_selection()) {
            auto iter_insert = curr_buffer->get_insert()->get_iter();
            CtListInfo list_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(iter_insert);
            if (list_info) {
                _ctTextview.list_change_level(iter_insert, list_info, true);
                return true;
            }
        }
        else {
            // With a selection: first try to apply Tab (increase level) to all selected list items
            Gtk::TextIter iter_sel_start, iter_sel_end;
            curr_buffer->get_selection_bounds(iter_sel_start, iter_sel_end);
            const int num_chars = iter_sel_end.get_offset() - iter_sel_start.get_offset();
            // Collect list items paragraph by paragraph (bottom to top to preserve offsets)
            std::vector<std::pair<Gtk::TextIter, CtListInfo>> list_items;
            Gtk::TextIter iter_scan = iter_sel_end;
            const int sel_start_line = iter_sel_start.get_line();
            while (true) {
                Gtk::TextIter line_iter = iter_scan;
                line_iter.set_line_offset(0);
                const bool is_first_line = (line_iter.get_line() <= sel_start_line);
                // For the first selected line use iter_sel_start as anchor so that
                // get_paragraph_list_info can walk back to the real line start even
                // when the selection begins after the leading indentation spaces.
                Gtk::TextIter anchor = is_first_line ? iter_sel_start : line_iter;
                CtListInfo list_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(anchor);
                if (list_info) {
                    list_items.emplace_back(anchor, list_info);
                }
                if (is_first_line or line_iter.get_line() == 0) break;
                iter_scan = curr_buffer->get_iter_at_line(line_iter.get_line() - 1);
            }
            if (not list_items.empty()) {
                for (auto& item : list_items) {
                    auto iter_insert = curr_buffer->get_iter_at_offset(item.second.startoffs);
                    CtListInfo refreshed_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(iter_insert);
                    if (refreshed_info) {
                        _ctTextview.list_change_level(iter_insert, refreshed_info, true);
                    }
                }
                return true;
            }
            // No list items found: fall back to existing widget/table navigation
            if (curr_tree_iter().get_node_syntax_highlighting() == CtConst::RICH_TEXT_ID) {
                if (num_chars != 1) {
                    return false;
                }
                auto widgets = curr_tree_iter().get_anchored_widgets(iter_sel_start.get_offset(), iter_sel_start.get_offset());
                if (widgets.empty()) {
                    return false;
                }
                if (dynamic_cast<CtTableCommon*>(widgets.front())) {
                    curr_buffer->place_cursor(iter_sel_end);
                    _ctTextview.mm().grab_focus();
                    return true;
                }
                return false;
            }
        }
    }
    else if (event->key.state & Gdk::CONTROL_MASK) {
        if (not (event->key.state & Gdk::MOD1_MASK)) {
            if (GDK_KEY_space == event->key.keyval) {
                if (_try_move_focus_to_anchored_widget_if_on_it()) {
                    return true;
                }
                auto iter_insert = _ctTextview.get_buffer()->get_insert()->get_iter();
                CtListInfo list_info = CtList{_pCtConfig, curr_buffer}.get_paragraph_list_info(iter_insert);
                if (list_info and list_info.type == CtListType::Todo) {
                    if (_uCtActions->_is_curr_node_not_read_only_or_error()) {
                        auto iter_start_list = curr_buffer->get_iter_at_offset(list_info.startoffs + 3*list_info.level);
                        CtList{_pCtConfig, curr_buffer}.todo_list_rotate_status(iter_start_list);
                        return true;
                    }
                }
            }
            if (GDK_KEY_plus == event->key.keyval or GDK_KEY_KP_Add == event->key.keyval or GDK_KEY_equal == event->key.keyval) {
                _ctTextview.zoom_text(true, curr_tree_iter().get_node_syntax_highlighting());
                return true;
            }
            if (GDK_KEY_minus == event->key.keyval or GDK_KEY_KP_Subtract == event->key.keyval) {
                _ctTextview.zoom_text(false, curr_tree_iter().get_node_syntax_highlighting());
                return true;
            }
            if (GDK_KEY_0 == event->key.keyval or GDK_KEY_KP_0 == event->key.keyval) {
                _ctTextview.zoom_text(std::nullopt, curr_tree_iter().get_node_syntax_highlighting());
                return true;
            }
        }
    }
    return false;
}

// Called after every event on the SourceView
void CtMainWin::_on_textview_event_after(GdkEvent* event)
{
    if (event->type == GDK_2BUTTON_PRESS and (1 == event->button.button or 2 == event->button.button)) {
        _ctTextview.for_event_after_double_click_button12(event);
    }
    if (event->type == GDK_3BUTTON_PRESS and (1 == event->button.button or 2 == event->button.button)) {
        _ctTextview.for_event_after_triple_click_button12(event);
    }
    else if (event->type == GDK_BUTTON_PRESS or event->type == GDK_KEY_PRESS) {
        if (event->type == GDK_BUTTON_PRESS) {
            _ctTextview.for_event_after_button_press(event);
        }
        if (event->type == GDK_KEY_PRESS) {
            _ctTextview.for_event_after_key_press(event, curr_tree_iter().get_node_syntax_highlighting());
        }
    }
    else if (event->type == GDK_BUTTON_RELEASE) {
        if (_pCtConfig->wordCountOn) {
            update_selected_node_statusbar_info();
        }
    }
    else if (event->type == GDK_KEY_RELEASE) {
        if (_pCtConfig->wordCountOn) {
            Gtk::TextIter iter_sel_start;
            Gtk::TextIter iter_sel_end;
            const bool has_selection = _ctTextview.get_buffer()->get_selection_bounds(iter_sel_start, iter_sel_end);
            if (has_selection or
                GDK_KEY_Return == event->key.keyval or
                GDK_KEY_KP_Enter == event->key.keyval or
                event->key.keyval == GDK_KEY_space)
            {
                update_selected_node_statusbar_info();
            }
        }
    }
}

bool CtMainWin::_on_textview_scroll_event(GdkEventScroll* event)
{
    if (!(event->state & GDK_CONTROL_MASK))
        return false;
    if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_DOWN)
        _ctTextview.zoom_text(event->direction == GDK_SCROLL_DOWN, curr_tree_iter().get_node_syntax_highlighting());
    if (event->direction == GDK_SCROLL_SMOOTH && event->delta_y != 0)
        _ctTextview.zoom_text(event->delta_y < 0, curr_tree_iter().get_node_syntax_highlighting());
    return true;
}

bool CtMainWin::_on_treeview_drag_motion(const Glib::RefPtr<Gdk::DragContext>& /*context*/,
                                         int x,
                                         int y,
                                         guint /*time*/)
{
    // DND events don't have a 'state' field, so gtk_get_current_event_state fails.
    // Actively poll the pointer/keyboard state from the window instead.
    Gdk::ModifierType rawState = static_cast<Gdk::ModifierType>(0);
    if (auto window = _uCtTreeview->get_window()) {
        int px, py;
        window->get_pointer(px, py, rawState);
    }

    _drag_shift_held = (rawState & Gdk::SHIFT_MASK) != 0;
    //spdlog::debug("drag_motion: rawState=0x{:x} shift={}", (unsigned)rawState, _drag_shift_held);

    if (y < CtConst::TREE_DRAG_EDGE_PROX or y > (_uCtTreeview->get_allocation().get_height() - CtConst::TREE_DRAG_EDGE_PROX)) {
        const int delta = y < CtConst::TREE_DRAG_EDGE_PROX ? -CtConst::TREE_DRAG_EDGE_SCROLL : CtConst::TREE_DRAG_EDGE_SCROLL;
        Gtk::Scrollbar* vscroll_obj = _scrolledwindowTree.get_vscrollbar();
        vscroll_obj->set_value(vscroll_obj->get_value() + delta);
    }
    if (x < CtConst::TREE_DRAG_EDGE_PROX or x > (_uCtTreeview->get_allocation().get_width() - CtConst::TREE_DRAG_EDGE_PROX)) {
        const int delta = x < CtConst::TREE_DRAG_EDGE_PROX ? -CtConst::TREE_DRAG_EDGE_SCROLL : CtConst::TREE_DRAG_EDGE_SCROLL;
        Gtk::Scrollbar* hscroll_obj = _scrolledwindowTree.get_hscrollbar();
        hscroll_obj->set_value(hscroll_obj->get_value() + delta);
    }
    Gtk::TreePath treePath;
    Gtk::TreeViewDropPosition treeDropPos{Gtk::TREE_VIEW_DROP_BEFORE};
    if (_uCtTreeview->get_dest_row_at_pos(x, y, treePath, treeDropPos)) {
        _uCtTreeview->set_drag_dest_row(treePath, treeDropPos);
    }
    return true;
}

void CtMainWin::_on_treeview_drag_data_received(const Glib::RefPtr<Gdk::DragContext>& context,
                                                int x,
                                                int y,
                                                const Gtk::SelectionData& selection_data,
                                                guint /*info*/,
                                                guint time)
{
    auto on_scope_exit = scope_guard([&](void*) { context->drag_finish(false, false, time); });
    Gtk::TreePath treePathDest;
    Gtk::TreeViewDropPosition treeDropPos{Gtk::TREE_VIEW_DROP_BEFORE};
    if (not _uCtTreeview->get_dest_row_at_pos(x, y, treePathDest, treeDropPos)) {
        return;
    }
    const std::string treePathSrcStr = selection_data.get_data_as_string();
    if (treePathSrcStr.empty()) {
        return;
    }
    Gtk::TreePath treePathSrc{treePathSrcStr};
    if (treePathDest == treePathSrc) {
        return;
    }
    CtTreeIter drag_iter = _uCtTreestore->get_iter(treePathSrc);
    if (not drag_iter) {
        return;
    }
    CtTreeIter drop_iter = _uCtTreestore->get_iter(treePathDest);
    if (not drop_iter) {
        return;
    }
    CtTreeIter move_towards_top_iter = drop_iter.parent();
    while (move_towards_top_iter) {
        if (move_towards_top_iter == drag_iter) {
            CtDialogs::error_dialog(_("The new parent can't be one of his children!"), *this);
            return;
        }
        move_towards_top_iter = move_towards_top_iter.parent();
    }
    // Use the Shift state captured during the last drag_motion event
    const bool keep_focus = _drag_shift_held;
    spdlog::debug("drag_data_received: keep_focus={} _drag_shift_held={}", keep_focus, _drag_shift_held);
    if (treeDropPos == Gtk::TREE_VIEW_DROP_BEFORE) {
        auto prev_iter = drop_iter;
        --prev_iter;
        // note: prev_iter could be None, use drop_iter to retrieve the parent
        _uCtActions->node_move_after(drag_iter, drop_iter.parent(), prev_iter, true/*set_first*/, keep_focus);
    }
    else if (treeDropPos == Gtk::TREE_VIEW_DROP_AFTER) {
        _uCtActions->node_move_after(drag_iter, drop_iter.parent(), drop_iter, false/*set_first*/, keep_focus);
    }
    else {
        _uCtActions->node_move_after(drag_iter, drop_iter, Gtk::TreeModel::iterator{}, false/*set_first*/, keep_focus);
    }
}

void CtMainWin::_on_treeview_drag_data_get(const Glib::RefPtr<Gdk::DragContext>& /*context*/,
                                           Gtk::SelectionData& selection_data,
                                           guint /*info*/,
                                           guint /*time*/)
{
    Gtk::TreeModel::iterator sel_iter = _uCtTreeview->get_selection()->get_selected();
    if (sel_iter) {
        const Glib::ustring treePathStr = _uCtTreeview->get_model()->get_path(sel_iter).to_string();
        selection_data.set("UTF8_STRING", 8, (const guint8*)treePathStr.c_str(), (int)treePathStr.size());
    }
}
