// SPDX-FileCopyrightText: 2021 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "headerview.h"
#include "fileview.h"
#include "private/fileview_p.h"
#include "models/fileselectionmodel.h"
#include "models/fileviewmodel.h"
#include "baseitemdelegate.h"
#include "iconitemdelegate.h"
#include "listitemdelegate.h"
#include "listitempaintproxy.h"
#include "treeitempaintproxy.h"
#include "fileviewstatusbar.h"
#include "events/workspaceeventcaller.h"
#include "utils/workspacehelper.h"
#include "utils/fileviewhelper.h"
#include "utils/dragdrophelper.h"
#include "utils/viewdrawhelper.h"
#include "utils/selecthelper.h"
#include "utils/shortcuthelper.h"
#include "utils/fileviewmenuhelper.h"
#include "utils/fileoperatorhelper.h"
#include "utils/viewanimationhelper.h"
#include "utils/viewgeometryhelper.h"
#include "utils/stickygroupheaderhelper.h"
#include "events/workspaceeventsequence.h"

#include <dfm-base/mimedata/dfmmimedata.h>
#include <dfm-base/dfm_event_defines.h>
#include <dfm-base/dfm_global_defines.h>
#include <dfm-base/base/application/application.h>
#include <dfm-base/base/application/settings.h>
#include <dfm-base/utils/windowutils.h>
#include <dfm-base/utils/universalutils.h>
#include <dfm-base/utils/networkutils.h>
#include <dfm-base/utils/fileutils.h>
#include <dfm-base/utils/dialogmanager.h>
#include <dfm-base/widgets/filemanagerwindowsmanager.h>
#include <dfm-base/base/configs/dconfig/dconfigmanager.h>
#include <dfm-base/utils/fileinfohelper.h>
#include <dfm-base/utils/protocolutils.h>
#include <dfm-base/utils/viewdefines.h>

#ifdef DTKWIDGET_CLASS_DSizeMode
#    include <DSizeMode>
#    include <DGuiApplicationHelper>
#endif

#include <QResizeEvent>
#include <QScrollBar>
#include <QScroller>
#include <QTimer>
#include <QDrag>
#include <QApplication>
#include <QUrlQuery>
#include <QMimeData>
#include <QLayout>
#include <QAccessible>

using namespace dfmplugin_workspace;
DFMGLOBAL_USE_NAMESPACE
DFMBASE_USE_NAMESPACE
using namespace GlobalDConfDefines::ConfigPath;
using namespace GlobalDConfDefines::BaseConfig;

FileView::FileView(const QUrl &url, QWidget *parent)
    : DListView(parent), d(new FileViewPrivate(this))
{
    d->url = url;
    setMinimumHeight(10);
    setDragDropMode(QAbstractItemView::DragDrop);
    setDropIndicatorShown(false);
    setSelectionMode(QAbstractItemView::ExtendedSelection);
    setSelectionBehavior(QAbstractItemView::SelectRows);
    setEditTriggers(QListView::EditKeyPressed | QListView::SelectedClicked);
    setTextElideMode(Qt::ElideMiddle);
    setAlternatingRowColors(false);
    setSelectionRectVisible(true);
    setDefaultDropAction(Qt::CopyAction);
    setDragDropOverwriteMode(true);
    setDragEnabled(true);
    setAutoScroll(true);
    setAutoScrollMargin(32);
//  TODO (search): perf
//  setLayoutMode(QListView::Batched);
#ifdef QT_SCROLL_WHEEL_ANI
    QScrollBar *bar = verticalScrollBar();
    bar->setSingleStep(1);
    setVerticalScrollBarPolicy(Qt::ScrollBarSlideAnimationOn);
#endif
    setVerticalScrollMode(ScrollPerPixel);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

    initializeModel();
    initializeDelegate();
    initializeStatusBar();
    initializeConnect();
    initializeScrollBarWatcher();
    initializePreSelectTimer();
    initializeGroupHeaderTimer();

    viewport()->installEventFilter(this);
}

FileView::~FileView()
{
    fmInfo() << "Destroying FileView for URL:" << rootUrl();
#if QT_CONFIG(accessibility)
    // 通知无障碍系统该对象即将销毁，避免退出阶段继续访问此 view
    if (QAccessible::isActive()) {
        QAccessibleEvent hideEvent(this, QAccessible::ObjectHide);
        QAccessible::updateAccessibility(&hideEvent);
    }
#endif

    // 必须在基类(QAbstractItemView)析构前解绑 model:
    // QAbstractItemView / delegate / selection / header 可能持有 QPersistentModelIndex,
    // 若 model 先释放会在索引析构时访问已销毁模型。

    // 1. 先结束编辑器，避免编辑器无障碍对象继续持有 model index
    if (itemDelegate()) {
        itemDelegate()->commitDataAndCloseActiveEditor();
        itemDelegate()->hideAllIIndexWidget();
    }

    // 2. 避免在析构阶段主动操作 headerView 的 selectionModel。
    // 某些退出时序下，headerView 内部旧 selectionModel 可能已悬空，
    // 调用 setSelectionModel(nullptr) 会在 Qt 内部触发崩溃。
    // 此处仅解除 model 关联，selectionModel 交由 QObject 析构流程处理。
    if (d && d->headerView) {
        d->headerView->setModel(nullptr);
    }

    // 3. 断开信号连接
    //    保存 model 指针: setModel(nullptr) 后 model() 返回 nullptr,
    //    需在解绑前获取以便后续显式删除。
    FileViewModel *savedModel = model();
    if (savedModel) {
        disconnect(savedModel, nullptr, this, nullptr);
    }
    if (selectionModel()) {
        disconnect(selectionModel(), nullptr, this, nullptr);
        selectionModel()->clear();
    }
    setCurrentIndex(QModelIndex());

    // SelectHelper caches QItemSelection, which owns QPersistentModelIndex in Qt6.
    // Destroy it before detaching/deleting the model to avoid helper teardown
    // walking stale persistent indexes after the model has gone away.
    if (d && d->selectHelper) {
        d->selectHelper->prepareForModelTeardown();
        delete d->selectHelper;
        d->selectHelper = nullptr;
    }

    // 4. 解绑 model(关键步骤): 会触发 QAbstractItemView 清理持有的 QPersistentModelIndex
    DListView::setModel(nullptr);

    // 5. 显式删除 model: ~QAbstractItemModel() 会调用 invalidatePersistentIndexes(),
    //    将所有残留 QPersistentModelIndex 的 model 指针置 null。
    //    必须在 ~QObject() 子对象清理前执行,否则 headerView / selectionModel 等
    //    子控件在 ~QObject() 中销毁时,其内部 QPersistentModelIndex 会访问已销毁
    //    的 model 私有数据哈希表(removePersistentIndexData)而崩溃。
    if (savedModel && savedModel->QObject::parent() == this) {
        delete savedModel;
    }

    // 6. 清理其他订阅
    dpfSignalDispatcher->unsubscribe("dfmplugin_workspace", "signal_View_HeaderViewSectionChanged", this, &FileView::onHeaderViewSectionChanged);
    dpfSignalDispatcher->unsubscribe("dfmplugin_filepreview", "signal_ThumbnailDisplay_Changed", this, &FileView::onWidgetUpdate);

    fmDebug() << "FileView destruction completed";

    // model 已在步骤 5 显式删除,不会进入 ~QObject() 子对象清理流程
}

QWidget *FileView::widget() const
{
    return const_cast<FileView *>(this);
}

QWidget *FileView::contentWidget() const
{
    return findChild<QWidget *>("qt_scrollarea_viewport");
}

void FileView::setViewMode(Global::ViewMode mode)
{
    // itemDelegate 未设置时为未初始化状态，此时调用setViewMode需要执行设置流程
    // itemDelegate 已设置时，若view不可见，则暂不执行viewMode设置逻辑
    if (!isVisible() && itemDelegate()
        && d->delegates[static_cast<int>(mode)] == itemDelegate()) {
        fmDebug() << "View mode change skipped: view not visible and delegate already set for mode:" << static_cast<int>(mode);
        return;
    }

    if (itemDelegate()) {
        itemDelegate()->hideAllIIndexWidget();
    }

    // 未正确重置编辑状态导致编辑失败（重命名失效）
    if (state() == QAbstractItemView::EditingState) {
        setState(QAbstractItemView::NoState);
    }

    int delegateModeIndex = mode == Global::ViewMode::kTreeMode ? static_cast<int>(Global::ViewMode::kListMode) : static_cast<int>(mode);
    if (d->delegates.keys().contains(delegateModeIndex)) {
        d->currentViewMode = mode;
        fmDebug() << "View mode set successfully to:" << static_cast<int>(mode);
    } else {
        fmWarning() << QString("The view mode %1 is not support in this dir! This view will set default mode.").arg(delegateModeIndex);
        d->currentViewMode = Global::ViewMode::kIconMode;
        delegateModeIndex = static_cast<int>(Global::ViewMode::kIconMode);
    }

    // 切换视图模式前记录当前选中文件，以便切换视图模式后文件数量变化了也能保持存在文件的选中状态。
    recordSelectedUrls();

    setItemDelegate(d->delegates[delegateModeIndex]);
    switch (d->currentViewMode) {
    case Global::ViewMode::kIconMode:
        d->initHorizontalOffset = false;
        setUniformItemSizes(false);
        setResizeMode(QListView::Adjust);
        setOrientation(QListView::LeftToRight, true);

        d->adjustIconModeSpacing(model()->groupingStrategy());
        d->initIconModeView();
        setMinimumWidth(0);
        model()->setTreeView(false);
        verticalScrollBar()->setFixedHeight(rect().height() - d->statusBar->height());
        break;
    case Global::ViewMode::kListMode:
        d->delegates[static_cast<int>(Global::ViewMode::kListMode)]->setPaintProxy(new ListItemPaintProxy(this));
        setIconSize(QSize(kListViewIconSize, kListViewIconSize));
        viewport()->setContentsMargins(0, 0, 0, 0);
        d->delegates[static_cast<int>(Global::ViewMode::kListMode)]->setPaintProxy(new ListItemPaintProxy(this));
        model()->setTreeView(false);
        setListViewMode();
        break;
    case Global::ViewMode::kExtendMode:
        break;
    case Global::ViewMode::kTreeMode:
        viewport()->setContentsMargins(0, 0, 0, 0);
        if (d->itemsExpandable) {
            auto proxy = new TreeItemPaintProxy(this);
            proxy->setStyleProxy(style());
            d->delegates[static_cast<int>(Global::ViewMode::kListMode)]->setPaintProxy(proxy);
            model()->setTreeView(true);
        } else {
            d->delegates[static_cast<int>(Global::ViewMode::kListMode)]->setPaintProxy(new ListItemPaintProxy(this));
            model()->setTreeView(false);
        }
        setIconSize(QSize(kListViewIconSize, kListViewIconSize));
        setListViewMode();
        break;
    case Global::ViewMode::kAllViewMode:
        break;
    default:
        break;
    }
    fmDebug() << "View mode change completed for URL:" << rootUrl().toString();
}

Global::ViewMode FileView::currentViewMode() const
{
    return d->currentViewMode;
}

void FileView::setDelegate(Global::ViewMode mode, BaseItemDelegate *view)
{
    if (!view)
        return;

    auto delegate = d->delegates[static_cast<int>(mode)];
    if (delegate) {
        if (delegate->parent())
            delegate->setParent(nullptr);
        delete delegate;
    }

    d->delegates[static_cast<int>(mode)] = view;
    if (model())
        view->setHighlightKeywords(model()->getKeyWords());
}

bool FileView::setRootUrl(const QUrl &url)
{
    d->url = url;

    // Respect focus policy set by plugins (e.g., search scheme should keep focus on SearchEditWidget)
    if (!WorkspaceHelper::instance()->isFocusFileViewDisabled(url.scheme())) {
        setFocus();
    }

    clearSelection();
    selectionModel()->clear();
    d->statusBar->itemCounted(0);

    const QUrl &fileUrl = parseSelectedUrl(url);

    const QModelIndex &index = model()->setRootUrl(fileUrl);
    d->itemsExpandable = DConfigManager::instance()->value(kViewDConfName, kTreeViewEnable, true).toBool()
            && WorkspaceHelper::instance()->isViewModeSupported(rootUrl().scheme(), DFMGLOBAL_NAMESPACE::ViewMode::kTreeMode);
    if (d->selectHelper)
        d->selectHelper->setItemsExpandable(d->itemsExpandable);

    setRootIndex(index);

    loadViewState(fileUrl);
    delayUpdateStatusBar();
    updateContentLabel();
    setDefaultViewMode();

    resetSelectionModes();
    updateListHeaderView();

    // 初始化分组状态追踪
    d->previousGroupStrategy = model()->groupingStrategy();

    // dir already traversal
    if (model()->currentState() == ModelState::kIdle)
        updateSelectedUrl();

    fmDebug() << "FileView: root URL set successfully to" << url.toString();
    return true;
}

QUrl FileView::rootUrl() const
{
    return model()->rootUrl();
}

AbstractBaseView::ViewState FileView::viewState() const
{
    switch (model()->currentState()) {
    case ModelState::kIdle:
        return AbstractBaseView::ViewState::kViewIdle;
    case ModelState::kBusy:
        return AbstractBaseView::ViewState::kViewBusy;
    }

    return AbstractBaseView::viewState();
}

QList<QAction *> FileView::toolBarActionList() const
{
    // TODO(liuyangming): impl me
    return QList<QAction *>();
}

QList<QUrl> FileView::selectedUrlList() const
{
    QModelIndex rootIndex = this->rootIndex();
    QList<QUrl> list;

    for (const QModelIndex &index : selectedIndexes()) {
        if (index.parent() != rootIndex)
            continue;
        list << model()->data(index, ItemRoles::kItemUrlRole).toUrl();
    }

    return list;
}

void FileView::refresh()
{
    fmDebug() << "FileView: refresh requested for URL" << rootUrl().toString();
    if (NetworkUtils::instance()->checkFtpOrSmbBusy(rootUrl())) {
        fmWarning() << "FileView: cannot refresh - FTP or SMB is busy for URL" << rootUrl().toString();
        DialogManager::instance()->showUnableToVistDir(rootUrl().path());
        return;
    }
    model()->refresh();
}

void FileView::doItemsLayout()
{
    if (rootIndex().isValid())
        DListView::doItemsLayout();
}

FileViewModel *FileView::model() const
{
    return qobject_cast<FileViewModel *>(QAbstractItemView::model());
}

void FileView::setModel(QAbstractItemModel *model)
{
    // 1. 获取旧 model
    auto oldModel = FileView::model();

    // 2. 先解绑旧 model (关键!)
    // 必须先调用 DListView::setModel(nullptr) 来清理 QAbstractItemView 中的 QPersistentModelIndex
    if (oldModel) {
        DListView::setModel(nullptr);
        // 只删除我们拥有的 model (通过 QObject 父子关系判断)
        if (oldModel->QObject::parent() == this) {
            delete oldModel;
        }
    }

    // 3. 设置新 model
    if (model) {
        // 如果没有 parent,设置为 this,这样 Qt 会自动管理生命周期
        if (!model->QObject::parent()) {
            model->setParent(this);
        }
        DListView::setModel(model);
    }
}

void FileView::stopWork(const QUrl &newUrl)
{
    model()->stopTraversWork(newUrl);
}

void FileView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
    DListView::dataChanged(topLeft, bottomRight, roles);

    d->selectHelper->resortSelectFiles();
}

int FileView::getColumnWidth(const int &column) const
{
    if (d->headerView)
        return d->headerView->sectionSize(column);

    return GlobalPrivate::kListViewDefaultWidth;
}

int FileView::getHeaderViewWidth() const
{
    if (d->headerView)
        return d->headerView->length();

    return 0;
}

void FileView::setAlwaysOpenInCurrentWindow(bool openInCurrentWindow)
{
    // for dialog
    d->isAlwaysOpenInCurrentWindow = openInCurrentWindow;
}

void FileView::onHeaderViewMouseReleased()
{
    if (d->headerView->width() != width()) {
        d->adjustFileNameColumn = false;
    }

    if (d->headerView->length() != d->oldHeaderViewLenght) {
        QList<ItemRoles> roleList = d->columnRoles;
        QVariantMap state;
        for (const ItemRoles role : roleList) {
            int colWidth = getColumnWidth(model()->getColumnByRole(role));

            if (colWidth > 0)
                state[QString::number(role)] = colWidth;
        }
        Application::appObtuselySetting()->setValue("WindowManager", "ViewColumnState", state);
    }
}

void FileView::onHeaderSectionResized(int logicalIndex, int oldSize, int newSize)
{
    Q_UNUSED(logicalIndex)
    Q_UNUSED(oldSize)
    Q_UNUSED(newSize)

    d->headerView->adjustSize();
    updateGeometries();
    update();
}

void FileView::onSectionHandleDoubleClicked(int logicalIndex)
{
    if (model()->currentState() != ModelState::kIdle)
        return;

    int rowCount = model()->rowCount(rootIndex());

    if (rowCount < 1)
        return;

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QStyleOptionViewItem option = viewOptions();
#else
    QStyleOptionViewItem option;
    initViewItemOption(&option);
#endif

    option.rect.setWidth(QWIDGETSIZE_MAX);
    option.rect.setHeight(itemSizeHint().height());

    int columnMaxWidth = 0;

    for (int i = 0; i < rowCount; ++i) {
        const QModelIndex &index = model()->index(i, 0, rootIndex());
        const QList<QRect> &list = itemDelegate()->paintGeomertys(option, index, true);

        // 第0列为文件名列，此列比较特殊，因为前面还有文件图标占用了一部分空间
        int width = 0;

        if (logicalIndex == 0) {
            // 树形视图多绘制一个扩展标识，名称是第三列
            width = list.at(currentViewMode() == Global::ViewMode::kTreeMode ? 2 : 1).right() + kColumnPadding / 2;
        } else {
            width = list.at(logicalIndex + 1).width() + kColumnPadding * 2;
        }

        if (width > columnMaxWidth) {
            columnMaxWidth = width;
        }
    }

    for (int i = d->headerView->count() - 1; i >= 0; --i) {
        if (d->headerView->isSectionHidden(i))
            continue;

        // 最后一列要多加上视图的右margin
        if (i == logicalIndex)
            columnMaxWidth += kListModeRightMargin;

        break;
    }

    d->headerView->resizeSection(logicalIndex, columnMaxWidth);
}

void FileView::onHeaderSectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex)
{
    Q_UNUSED(logicalIndex)
    Q_UNUSED(oldVisualIndex)
    Q_UNUSED(newVisualIndex)

    QVariantList logicalIndexList;
    for (int i = 0; i < d->headerView->count(); ++i) {
        int logicalIndex = d->headerView->logicalIndex(i);
        logicalIndexList << model()->getRoleByColumn(logicalIndex);
    }

    QUrl rootUrl = this->rootUrl();

    setFileViewStateValue(rootUrl, "headerList", logicalIndexList);
    // sync data to config file.
    Application::appObtuselySetting()->sync();

    // each views should refresh
    dpfSignalDispatcher->publish("dfmplugin_workspace", "signal_View_HeaderViewSectionChanged", rootUrl);
    fmDebug() << "Header section move completed and published for URL:" << rootUrl.toString();
}

void FileView::onHeaderHiddenChanged(const QString &roleName, const bool isHidden)
{
    fmDebug() << "Header hidden changed - role:" << roleName << "hidden:" << isHidden << "for URL:" << rootUrl().toString();

    d->columnForRoleHiddenMap[roleName] = isHidden;

    const QUrl &url = rootUrl();
    if (d->shouldPersistState(url)) {
        for (int i = 0; i < model()->columnCount(); ++i) {
            if (model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString() == roleName) {
                ItemRoles role = model()->getRoleByColumn(i);
                QVariantMap hiddenState = d->fileViewStateValue(url, "columnHiddenState", {}).toMap();
                hiddenState[QString::number(role)] = isHidden;
                setFileViewStateValue(url, "columnHiddenState", hiddenState);
                Application::appObtuselySetting()->sync();
                break;
            }
        }
    }

    if (d->allowedAdjustColumnSize) {
        updateListHeaderView();
    } else {
        d->headerView->updateColumnWidth();
    }
}

void FileView::onSortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
{
    if (model()->currentState() == ModelState::kBusy) {
        fmDebug() << "Sort indicator change ignored: model is busy for URL:" << rootUrl().toString();
        return;
    }

    fmDebug() << "Sort indicator changed - column:" << logicalIndex << "order:" << (order == Qt::AscendingOrder ? "Ascending" : "Descending") << "for URL:" << rootUrl().toString();

    recordSelectedUrls();

    model()->sort(logicalIndex, order);

    const QUrl &url = rootUrl();
    if (d->shouldPersistState(url)) {
        const ItemRoles &role = model()->getRoleByColumn(logicalIndex);
        setFileViewStateValue(url, "sortRole", role);
        setFileViewStateValue(url, "sortOrder", static_cast<int>(order));
    }
}

void FileView::onClicked(const QModelIndex &index)
{
    d->lastClickedIndex = index;
    if (isGroupHeader(index)) {
        d->groupHeaderTimer->start();
        return;
    }
    openIndexByClicked(ClickedAction::kClicked, index);

    QUrl url { "" };
    const FileInfoPointer &info = model()->fileInfo(index);
    if (info)
        url = info->urlOf(UrlInfoType::kUrl);
    QVariantMap data;
    data.insert("displayName", model()->data(index));
    data.insert("url", url);
    WorkspaceEventCaller::sendViewItemClicked(data);

    fmDebug() << "Item clicked:" << url.toString() << "for URL:" << rootUrl().toString();
}

void FileView::onDoubleClicked(const QModelIndex &index)
{
    fmDebug() << "Item double clicked for URL:" << rootUrl().toString();
    if (isGroupHeader(index)) {
        d->groupHeaderTimer->stop();
        groupExpandOrCollapseItem(index, QPoint(), false);
        return;
    }
    openIndexByClicked(ClickedAction::kDoubleClicked, index);
}

void FileView::wheelEvent(QWheelEvent *event)
{
    // Handle size control for all view modes with Ctrl + wheel
    if (WindowUtils::keyCtrlIsPressed()) {
        if (event->angleDelta().y() > 0) {
            increaseIcon();
        } else {
            decreaseIcon();
        }

        event->accept();
        return;
    }

    // Handle horizontal scrolling with Alt modifier or horizontal wheel
    if (event->modifiers() == Qt::AltModifier || event->angleDelta().x() != 0) {
        horizontalScrollBar()->setSliderPosition(horizontalScrollBar()->sliderPosition() - event->angleDelta().x());
        return;
    }

    // Default vertical scrolling
#ifdef QT_SCROLL_WHEEL_ANI
    DListView::wheelEvent(event);
#else
    verticalScrollBar()->setSliderPosition(verticalScrollBar()->sliderPosition() - event->angleDelta().y());
#endif
}

void FileView::keyPressEvent(QKeyEvent *event)
{
    if (!d->shortcutHelper->processKeyPressEvent(event)) {
        switch (event->modifiers()) {
        case Qt::AltModifier:
        case Qt::AltModifier | Qt::KeypadModifier:
            switch (event->key()) {
            case Qt::Key_Left:
            case Qt::Key_Right:
                return QWidget::keyPressEvent(event);
            }
            break;
        // Ctrl+Left/Right 用于前进/后退（由 FileManagerWindowPrivate::processKeyPressEvent
        // 发射 reqBack/reqForward 信号处理）。此处必须显式转发给 QWidget::keyPressEvent，
        // 否则会落入 DListView::keyPressEvent（即 QAbstractItemView）的默认实现：
        // QAbstractItemView 会用 Ctrl 修饰键移动 currentIndex 并以 NoUpdate 标志调用
        // setSelection，进而触发 SelectHelper::selection 打印
        // "Selection with NoUpdate flag - skipping" 并 accept() 事件，导致按键事件被
        // 父类消费、不再向上冒泡到 FileManagerWindow，前进/后退失效（BUG370967）。
        // 交给 QWidget::keyPressEvent 后，事件会被 ignore()，从而冒泡到窗口层处理。
        case Qt::ControlModifier:
            switch (event->key()) {
            case Qt::Key_Left:
            case Qt::Key_Right:
                return QWidget::keyPressEvent(event);
            }
            break;
        }
        return DListView::keyPressEvent(event);
    }
}

void FileView::onScalingValueChanged(const int value)
{
    const QUrl &url = rootUrl();
    setFileViewStateValue(url, "iconSizeLevel", value);
}

void FileView::delayUpdateStatusBar()
{
    if (d->updateStatusBarTimer)
        d->updateStatusBarTimer->start();
}

void FileView::viewModeChanged(quint64 windowId, int viewMode)
{
    Global::ViewMode mode = static_cast<Global::ViewMode>(viewMode);
    if (currentViewMode() == mode) {
        qWarning() << "Current view mode equal to the new view mode that switched by global event. Don't need to do anything.";
        return;
    }

    if (mode == Global::ViewMode::kIconMode || mode == Global::ViewMode::kListMode || mode == Global::ViewMode::kTreeMode) {
        setViewMode(mode);
    }

    saveViewModeState();
}

FileView::RangeIndexList FileView::visibleIndexes(const QRect &rect) const
{
    return d->geometryHelper->visibleIndexes(rect);
}

FileView::RangeIndexList FileView::rectContainsIndexes(const QRect &rect) const
{
    return d->geometryHelper->rectContainsIndexes(rect);
}

FileView::RangeIndexList FileView::calcRectContiansIndexes(int columnCount, const QRect &rect) const
{
    return d->geometryHelper->calcRectContiansIndexes(columnCount, rect);
}

BaseItemDelegate *FileView::itemDelegate() const
{
    return qobject_cast<BaseItemDelegate *>(DListView::itemDelegate());
}

int FileView::rowCount() const
{
    int count = this->count();
    int itemCountForRow = this->itemCountForRow();

    return count / itemCountForRow + int(count % itemCountForRow > 0);
}

bool FileView::isSelected(const QModelIndex &index) const
{
    return static_cast<FileSelectionModel *>(selectionModel())->isSelected(index);
}

int FileView::selectedIndexCount() const
{
    return static_cast<FileSelectionModel *>(selectionModel())->selectedCount();
}

bool FileView::selectFiles(const QList<QUrl> &files) const
{
    if (isTreeViewMode())
        return d->selectHelper->select(files);

    if (files.isEmpty())
        return false;

    // do not check parent if root is virtual path.
    if (rootUrl().scheme() == Global::Scheme::kFile) {
        if (UniversalUtils::urlEquals(files.first().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash), rootUrl()))
            return d->selectHelper->select(files);
        return false;
    }

    return d->selectHelper->select(files);
}

void FileView::setSelectionMode(const QAbstractItemView::SelectionMode mode)
{
    if (d->enabledSelectionModes.contains(mode))
        QAbstractItemView::setSelectionMode(mode);
}

void FileView::reverseSelect() const
{
    d->shortcutHelper->reverseSelect();
}

void FileView::setEnabledSelectionModes(const QList<QAbstractItemView::SelectionMode> &modes)
{
    d->enabledSelectionModes = modes;
    if (!modes.contains(selectionMode()))
        resetSelectionModes();
}

void FileView::setSort(const ItemRoles role, const Qt::SortOrder order)
{
    if (model()->currentState() == ModelState::kBusy)
        return;

    if (role == model()->sortRole() && order == model()->sortOrder())
        return;

    int column = model()->getColumnByRole(role);

    onSortIndicatorChanged(column, order);
    if (d->headerView) {
        QSignalBlocker blocker(d->headerView);
        Q_UNUSED(blocker)
        d->headerView->setSortIndicator(column, order);
    }
}

void FileView::setGroup(const QString &strategyName, const Qt::SortOrder order)
{
    // NOTE: do NOT short-circuit on ModelState::kBusy here. When a grouping
    // request arrives mid-traversal (e.g. SearchManager::search pushes a
    // queued slot_Model_SetGroup that lands while search results are still
    // streaming in), the model's grouping() will stash it as a pending
    // request and replay it from onWorkFinish() once traversal completes.
    // Dropping the request here would silently defeat that deferral.
    if (strategyName == model()->groupingStrategy() && order == model()->groupingOrder())
        return;

    fmInfo() << "Setting group strategy:" << strategyName << "order:" << (order == Qt::AscendingOrder ? "Ascending" : "Descending")
             << "for URL:" << rootUrl().toString();

    // 在分组开始前保存当前的分组策略
    d->previousGroupStrategy = model()->groupingStrategy();
    clearStickyHeaderState();

    model()->grouping(strategyName, order);

    const QUrl &url = rootUrl();

    if (d->shouldPersistState(url)) {
        setFileViewStateValue(url, "groupStrategy", strategyName);
        setFileViewStateValue(url, "groupingOrder", static_cast<int>(order));
    }

    // Dynamically adjust icon mode spacing based on grouped view state
    d->adjustIconModeSpacing(strategyName);
}

GroupingState FileView::groupingState()
{
    return model()->groupingState();
}

void FileView::setViewSelectState(bool isSelect)
{
    // 拖拽时一直在设置d->isShowViewSelectBox值，每次都在全屏绘制，绘制效率低下
    if (d->isShowViewSelectBox == isSelect)
        return;
    d->isShowViewSelectBox = isSelect;
    viewport()->update();
}

QModelIndex FileView::currentPressIndex() const
{
    return d->selectHelper->getCurrentPressedIndex();
}

bool FileView::isDragTarget(const QModelIndex &index) const
{
    return d->dragDropHelper->isDragTarget(index);
}

QRectF FileView::itemRect(const QUrl &url, const ItemRoles role) const
{
    QModelIndex index = model()->getIndexByUrl(url);

    switch (role) {
    case kItemIconRole: {
        QRectF rect = visualRect(index);
        return itemDelegate()->itemIconRect(rect);
    }
    case kItemBackgroundRole: {
        QRectF rect = visualRect(index);
        return rect;
    }
    default:
        return QRectF();
    }
}

bool FileView::isVerticalScrollBarSliderDragging() const
{
    if (d->scrollBarValueChangedTimer)
        return d->scrollBarValueChangedTimer->isActive();

    return false;
}

void FileView::updateViewportContentsMargins(const QSize &itemSize)
{
    if (isListViewMode() || isTreeViewMode() || itemSize.width() <= spacing())
        return;
    int itemWidth = itemSize.width() + 2 * spacing();
    int iconHorizontalMargin = kIconHorizontalMargin;
#ifdef DTKWIDGET_CLASS_DSizeMode
    iconHorizontalMargin = DSizeModeHelper::element(kCompactIconHorizontalMargin, kIconHorizontalMargin);
#endif

    if (itemWidth < 2 * kIconHorizontalMargin)
        return;

    int contentsWidth = contentsSize().width();
    if (contentsWidth < itemWidth) {
        viewport()->setContentsMargins(0, 0, 0, 0);
        return;
    }
    int widthModel = (contentsWidth - 1) % itemWidth;
    if (widthModel >= iconHorizontalMargin && widthModel <= itemWidth - kIconHorizontalMargin) {
        viewport()->setContentsMargins(0, 0, 0, 0);
        return;
    }
    int margin = kIconHorizontalMargin - (widthModel >= iconHorizontalMargin ? itemWidth - widthModel : widthModel);
    if (margin <= 0 || margin > kIconHorizontalMargin) {
        viewport()->setContentsMargins(0, 0, 0, 0);
        return;
    }
    viewport()->setContentsMargins(margin, 0, margin, 0);
}

bool FileView::indexInRect(const QRect &actualRect, const QModelIndex &index)
{
    return d->geometryHelper->indexInRect(actualRect, index);
}

QList<QUrl> FileView::selectedTreeViewUrlList() const
{
    return d->selectHelper->selectedTreeViewUrlList();
}

void FileView::selectedTreeViewUrlList(QList<QUrl> &selectedUrls, QList<QUrl> &treeSelectedUrls) const
{
    d->selectHelper->selectedTreeViewUrlList(selectedUrls, treeSelectedUrls);
}

QRect FileView::calcVisualRect(int widgetWidth, int index) const
{
    return d->geometryHelper->calcVisualRect(widgetWidth, index);
}

void FileView::aboutToChangeWidth(int deltaWidth)
{
    if (!isIconViewMode())
        return;

    d->animationHelper->initAnimationHelper();
    d->animationHelper->aboutToPlay();
    d->animationHelper->playAnimationWithWidthChange(deltaWidth);
}

void FileView::initDefaultHeaderView()
{
    if (!model())
        return;

    auto roleName = model()->roleDisplayString(kItemFileCreatedRole);
    d->columnForRoleHiddenMap[roleName] = true;
}

void FileView::onHeaderViewMousePressed()
{
    d->oldHeaderViewLenght = d->headerView->length();
}

void FileView::onSelectAndEdit(const QUrl &url)
{
    if (!url.isValid())
        return;

    quint64 winId = WorkspaceHelper::instance()->windowId(this);
    if (!WorkspaceHelper::kSelectionAndRenameFile.contains(winId))
        return;

    QPair<QUrl, QUrl> urlPair = WorkspaceHelper::kSelectionAndRenameFile[winId];
    if (!UniversalUtils::urlEquals(urlPair.first, rootUrl()) || !UniversalUtils::urlEquals(urlPair.second, url))
        return;

    WorkspaceHelper::kSelectionAndRenameFile.take(winId);

    const QModelIndex &index = model()->getIndexByUrl(url);
    if (!index.isValid())
        return;

    selectionModel()->clear();
    selectFiles({ url });
    edit(index, QAbstractItemView::AllEditTriggers, nullptr);
}

int FileView::itemCountForRow() const
{
    if (!isIconViewMode())
        return 1;

    return d->iconModeColumnCount();
}

int FileView::iconModeColumnCount(int itemWidth) const
{
    return d->iconModeColumnCount(itemWidth);
}

QSize FileView::itemSizeHint() const
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    if (itemDelegate())
        return itemDelegate()->sizeHint(viewOptions(), rootIndex());
#else
    if (itemDelegate()) {
        QStyleOptionViewItem option;
        initViewItemOption(&option);
        return itemDelegate()->sizeHint(option, rootIndex());
    }
#endif

    return QSize();
}

void FileView::increaseIcon()
{
    if (!itemDelegate())
        return;

    int newLevel = itemDelegate()->increaseIcon();
    if (newLevel < 0)
        return;

    const QUrl &url = rootUrl();
    if (isIconViewMode()) {
        setFileViewStateValue(url, "iconSizeLevel", newLevel);
        fmDebug() << "Icon size level increased to:" << newLevel;
    } else if (isListViewMode() || isTreeViewMode()) {
        setFileViewStateValue(url, "listHeightLevel", newLevel);
        fmDebug() << "List height level increased to:" << newLevel;
    }
}

void FileView::decreaseIcon()
{
    if (!itemDelegate())
        return;

    int newLevel = itemDelegate()->decreaseIcon();
    if (newLevel < 0)
        return;

    const QUrl &url = rootUrl();
    if (isIconViewMode()) {
        setFileViewStateValue(url, "iconSizeLevel", newLevel);
        fmDebug() << "Icon size level decreased to:" << newLevel;
    } else if (isListViewMode() || isTreeViewMode()) {
        setFileViewStateValue(url, "listHeightLevel", newLevel);
        fmDebug() << "List height level decreased to:" << newLevel;
    }
}

void FileView::setIconSizeBySizeIndex(const int sizeIndex)
{
    QSignalBlocker blocker(d->statusBar->scalingSlider());
    Q_UNUSED(blocker)

    d->currentIconSizeLevel = sizeIndex;
    d->statusBar->scalingSlider()->setValue(sizeIndex);
}

void FileView::onShowFileSuffixChanged(bool isShow)
{
    Q_UNUSED(isShow);
    update();
}

void FileView::updateHorizontalOffset()
{
    d->updateHorizontalOffset();

    // Update editor positions after horizontal offset changes
    updateEditorGeometries();
}

void FileView::updateOneView(const QModelIndex &index)
{
    update(index);
}

void FileView::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
    delayUpdateStatusBar();

    emit selectUrlChanged(selectedUrlList());

    quint64 winId = WorkspaceHelper::instance()->windowId(this);
    WorkspaceEventCaller::sendViewSelectionChanged(winId, selected, deselected);
}

void FileView::onDefaultViewModeChanged(int mode)
{
    Global::ViewMode newMode = static_cast<Global::ViewMode>(mode);

    if (!WorkspaceHelper::instance()->isViewModeSupported(rootUrl().scheme(), newMode)) {
        fmWarning() << "View mode not supported for scheme:" << rootUrl().scheme() << "mode:" << mode;
        return;
    }

    if (newMode == d->currentViewMode) {
        fmDebug() << "View mode unchanged, skipping update";
        return;
    }

    Global::ViewMode oldMode = d->currentViewMode;
    d->loadViewMode(rootUrl());

    if (oldMode == d->currentViewMode) {
        fmDebug() << "View mode unchanged after loading, skipping update";
        return;
    }

    fmDebug() << "Switching view mode from" << static_cast<int>(oldMode) << "to" << static_cast<int>(d->currentViewMode) << "for URL:" << rootUrl().toString();
    setViewMode(d->currentViewMode);
}

void FileView::onIconSizeChanged(int sizeIndex)
{
    if (!itemDelegate())
        return;

    if (itemDelegate()->iconSizeLevel() == sizeIndex && d->currentIconSizeLevel == sizeIndex)
        return;

    itemDelegate()->setIconSizeByIconSizeLevel(sizeIndex);
    setIconSizeBySizeIndex(sizeIndex);
}

void FileView::onItemWidthLevelChanged(int level)
{
    if (!itemDelegate())
        return;

    // Check if this is an icon delegate that supports width level control
    auto iconDelegate = dynamic_cast<IconItemDelegate *>(itemDelegate());
    if (!iconDelegate)
        return;

    if (iconDelegate->minimumWidthLevel() == level && d->currentGridDensityLevel == level)
        return;

    d->currentGridDensityLevel = level;
    iconDelegate->setItemMinimumWidthByWidthLevel(level);
    doItemsLayout();
    updateHorizontalOffset();
}

void FileView::onItemHeightLevelChanged(int level)
{
    if (!itemDelegate())
        return;

    if (!d->fileViewHelper->canChangeListItemHeight())
        return;

    if (itemDelegate()->iconSizeLevel() == level && d->currentListHeightLevel == level)
        return;

    d->currentListHeightLevel = level;
    itemDelegate()->setIconSizeByIconSizeLevel(level);
    doItemsLayout();
}

bool FileView::isIconViewMode() const
{
    return d->currentViewMode == Global::ViewMode::kIconMode;
}

bool FileView::isListViewMode() const
{
    return d->currentViewMode == Global::ViewMode::kListMode;
}

bool FileView::isTreeViewMode() const
{
    return d->currentViewMode == Global::ViewMode::kTreeMode;
}

bool FileView::isGroupedView() const
{
    const auto strategyName = model()->groupingStrategy();

    if (strategyName.isEmpty() || strategyName == GroupStrategy::kNoGroup)
        return false;

    return true;
}

void FileView::resetSelectionModes()
{
    const QList<SelectionMode> &supportSelectionModes = fetchSupportSelectionModes();

    for (SelectionMode mode : supportSelectionModes) {
        if (d->enabledSelectionModes.contains(mode)) {
            setSelectionMode(mode);
            break;
        }
    }
}

QList<QAbstractItemView::SelectionMode> FileView::fetchSupportSelectionModes()
{
    QList<SelectionMode> modes {};
    WorkspaceEventSequence::instance()->doFetchSelectionModes(rootUrl(), &modes);

    if (modes.isEmpty())
        modes << ExtendedSelection << SingleSelection << MultiSelection
              << ContiguousSelection << NoSelection;

    return modes;
}

bool FileView::cdUp()
{
    const QUrl &oldCurrentUrl = rootUrl();
    fmInfo() << "Navigating up from URL:" << oldCurrentUrl;

    QUrl parentUrl = UrlRoute::urlParent(oldCurrentUrl);

    if (parentUrl.isValid()) {
        fmDebug() << "Parent URL found:" << parentUrl;
        FileOperatorHelperIns->openFilesByMode(this, { parentUrl }, DirOpenMode::kOpenInCurrentWindow);
        return true;
    } else {
        fmDebug() << "No valid parent URL, navigating to computer root";
        auto windowId = WorkspaceHelper::instance()->windowId(this);
        QUrl computerRoot;
        computerRoot.setScheme(Global::Scheme::kComputer);
        computerRoot.setPath("/");
        WorkspaceEventCaller::sendChangeCurrentUrl(windowId, computerRoot);
    }
    return false;
}

QModelIndex FileView::iconIndexAt(const QPoint &pos, const QSize &itemSize) const
{
    if (isListViewMode() || isTreeViewMode())
        return QModelIndex();

    if (itemDelegate() && itemDelegate()->itemExpanded() && itemDelegate()->expandItemRect().contains(pos)) {
        return itemDelegate()->expandedIndex();
    }

    auto currentIndex = DListView::indexAt(pos);
    if (isGroupHeader(currentIndex))
        return currentIndex;

    auto paintRect = visualRect(currentIndex);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    auto opt = viewOptions();
#else
    QStyleOptionViewItem opt;
    initViewItemOption(&opt);
#endif
    opt.rect = paintRect;
    if (!itemDelegate())
        return QModelIndex();

    auto rectList = itemDelegate()->itemGeomertys(opt, currentIndex);
    for (const auto &rect : rectList) {
        if (rect.contains(pos))
            return currentIndex;
    }
    return QModelIndex();
}

bool FileView::expandOrCollapseItem(const QModelIndex &index, const QPoint &pos)
{
    QRect arrowRect = itemDelegate()->getRectOfItem(RectOfItemType::kItemTreeArrowRect, index);

    if (!arrowRect.contains(pos))
        return false;

    // get expand state
    bool expanded = model()->data(index, kItemTreeViewExpandedRole).toBool();
    if (expanded) {
        // do collapse
        fmInfo() << "do collapse item, index = " << index << index.row() << model()->data(index, kItemUrlRole).toUrl();
        model()->toggleTreeItemCollapse(index);
    } else {
        // do expanded
        fmInfo() << "do expanded item, index = " << index << index.row() << model()->data(index, kItemUrlRole).toUrl();
        model()->toggleTreeItemExpansion(index);
    }

    return true;
}

bool FileView::groupExpandOrCollapseItem(const QModelIndex &index, const QPoint &pos, const bool isArr)
{
    if (!isArr) {
        onGroupExpansionToggled(index.data(kItemGroupHeaderKey).toString());
        return true;
    }
    QStyleOptionViewItem op;
    const bool isStickyHeader = (index == d->stickyHelper->currentStickyIndex());
    QRect rect = isStickyHeader ? d->stickyHelper->currentStickyRect() : visualRect(index);
    op.rect = rect;

    if (!isStickyHeader && index.data(Global::kItemGroupDisplayIndex).toInt() > 0) {
        // Non-first group headers reserve a 16px transparent gap above the content.
        // Align the hit area with the actual painted arrow inside the content rect.
        op.rect.setTop(op.rect.top() + kGroupHeaderInterval);
    }

    QRect arrowRect = itemDelegate()->getExpandButtonHitRect(op);

    if (!arrowRect.contains(pos))
        return false;

    onGroupExpansionToggled(index.data(kItemGroupHeaderKey).toString());

    return true;
}

QString FileView::truncateButtonGroupKeyAt(const QPoint &pos) const
{
    if (!itemDelegate()) {
        return {};
    }

    auto hitGroupKey = [this, &pos](const QModelIndex &index, const QRect &rect) -> QString {
        if (!index.isValid() || !isGroupHeader(index) || !itemDelegate()->shouldShowTruncateButton(index)) {
            return {};
        }

        QStyleOptionViewItem option;
        option.rect = rect;
        if (!itemDelegate()->getTruncateButtonRect(option).contains(pos)) {
            return {};
        }

        return index.data(kItemGroupHeaderKey).toString();
    };

    if (isPosInStickyHeader(pos)) {
        return hitGroupKey(d->stickyHelper->currentStickyIndex(), d->stickyHelper->currentStickyRect());
    }

    const QModelIndex index = DListView::indexAt(pos);
    return hitGroupKey(index, visualRect(index));
}

void FileView::updateTruncateButtonHover(const QPoint &pos)
{
    if (!itemDelegate()) {
        return;
    }

    const QString groupKey = truncateButtonGroupKeyAt(pos);
    if (groupKey == itemDelegate()->hoveredTruncateGroupKey()) {
        return;
    }

    itemDelegate()->setHoveredTruncateGroupKey(groupKey);
    viewport()->update();
}

void FileView::clearTruncateButtonHover()
{
    if (!itemDelegate() || itemDelegate()->hoveredTruncateGroupKey().isEmpty()) {
        return;
    }

    itemDelegate()->setHoveredTruncateGroupKey({});
    viewport()->update();
}

void FileView::recordSelectedUrls()
{
    auto selectedUrls = selectedUrlList();
    if (!selectedUrls.isEmpty()) {
        const QUrl curFile = model()->data(currentIndex(), ItemRoles::kItemUrlRole).toUrl();
        d->selectHelper->saveSelectedFilesList(curFile, selectedUrls);
    }
}

DirOpenMode FileView::currentDirOpenMode() const
{
    DirOpenMode mode;

    if (d->isAlwaysOpenInCurrentWindow) {
        mode = DirOpenMode::kAwaysInCurrentWindow;
    } else {
        if (Application::instance()->appAttribute(Application::kAllwayOpenOnNewWindow).toBool()) {
            mode = DirOpenMode::kOpenNewWindow;
        } else {
            mode = DirOpenMode::kOpenInCurrentWindow;
        }
    }
    return mode;
}

void FileView::onWidgetUpdate()
{
    this->update();
}

void FileView::onRenameProcessStarted()
{
    if (d->shortcutHelper) {
        d->shortcutHelper->renameProcessing();
    }
}

void FileView::onAboutToSwitchListView(const QList<QUrl> &allShowList)
{
    d->selectHelper->filterSelectedFiles(allShowList);
}

void FileView::onRowCountChanged()
{
    // clean selected indexes
    // the selectList will be reseted while call the selectedIndexes() at next time
    static_cast<FileSelectionModel *>(selectionModel())->clearSelectList();

    d->selectHelper->resortSelectFiles();

    delayUpdateStatusBar();
    updateContentLabel();

    if (isIconViewMode()) {
        updateHorizontalOffset();
    }
}

void FileView::setFilterData(const QUrl &url, const QVariant &data)
{
    fmDebug() << "Setting filter data for URL:" << url << "current URL:" << rootUrl();

    if (url == rootUrl() && isVisible()) {
        fmInfo() << "Applying filter data to current view";
        clearSelection();
        model()->setFilterData(data);
    } else {
        fmDebug() << "Filter data not applied - URL mismatch or view not visible";
    }
}

void FileView::setFilterCallback(const QUrl &url, const FileViewFilterCallback callback)
{
    fmDebug() << "Setting filter callback for URL:" << url << "current URL:" << rootUrl();

    if (url == rootUrl() && isVisible()) {
        fmInfo() << "Applying filter callback to current view";
        clearSelection();
        model()->setFilterCallback(callback);
    } else {
        fmDebug() << "Filter callback not applied - URL mismatch or view not visible";
    }
}

void FileView::trashStateChanged()
{
    if (Q_LIKELY(model()))
        model()->updateFile(FileUtils::trashRootUrl());
}

void FileView::onHeaderViewSectionChanged(const QUrl &url)
{
    if (UniversalUtils::urlEquals(url, rootUrl()) && viewMode() == ViewMode::ListMode) {
        // refresh
        updateListHeaderView();
        update();
    }
}

void FileView::onAppAttributeChanged(const QString &group, const QString &key, const QVariant &value)
{
    if (group == "FileViewState") {
        if (isListViewMode() || isTreeViewMode()) {
            int configListHeightLevel = d->fileViewStateValue(rootUrl(), "listHeightLevel", d->currentListHeightLevel).toInt();
            onItemHeightLevelChanged(configListHeightLevel);
        }

        if (isIconViewMode()) {
            int configGridDensityLevel = d->fileViewStateValue(rootUrl(), "gridDensityLevel", d->currentGridDensityLevel).toInt();
            onItemWidthLevelChanged(configGridDensityLevel);

            int configIconSizeLevel = d->fileViewStateValue(rootUrl(), "iconSizeLevel", d->currentIconSizeLevel).toInt();
            onIconSizeChanged(configIconSizeLevel);
        }
    }
}

bool FileView::edit(const QModelIndex &index, QAbstractItemView::EditTrigger trigger, QEvent *event)
{
    if (selectedIndexCount() > 1)
        return false;

    return DListView::edit(index, trigger, event);
}

void FileView::resizeEvent(QResizeEvent *event)
{
    d->initHorizontalOffset = false;

    d->isResizeEvent = true;
    DListView::resizeEvent(event);
    d->isResizeEvent = false;

    updateHorizontalOffset();

    // TODO(liuyangming) crash when launch via command with params.
    if (itemDelegate() && itemDelegate()->editingIndex().isValid())
        doItemsLayout();

    if (isIconViewMode()) {
        updateViewportContentsMargins(itemSizeHint());
        if (model()->currentState() == ModelState::kIdle && event->size().width() != event->oldSize().width()) {
            d->animationHelper->playViewAnimation(event->oldSize(), event->size());
        }
    }

    verticalScrollBar()->setFixedHeight(rect().height() - d->statusBar->height() - (d->headerView ? d->headerView->height() : 0));
}

void FileView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags)
{
    d->selectHelper->selection(rect, flags);
}

void FileView::mousePressEvent(QMouseEvent *event)
{
    if (isPosInStickyHeader(event->pos())) {
        QModelIndex stickyIdx = d->stickyHelper->currentStickyIndex();
        if (event->button() == Qt::LeftButton) {
            d->lastClickedIndex = stickyIdx;
            if (groupExpandOrCollapseItem(stickyIdx, event->pos())) {
                if (stickyIdx.data(Global::kItemGroupExpandedRole).toBool())
                    scrollStickyHeaderToTop(stickyIdx);
                return;
            }
            const QString truncateGroupKey = truncateButtonGroupKeyAt(event->pos());
            if (!truncateGroupKey.isEmpty() && itemDelegate()) {
                itemDelegate()->setPressedTruncateGroupKey(truncateGroupKey);
                viewport()->update();
                return;
            }
            d->groupHeaderTimer->start();
        } else if (event->button() == Qt::RightButton) {
            onGroupHeaderClicked(stickyIdx);
        }
        return;
    }

    if (event->buttons().testFlag(Qt::LeftButton)) {
        d->mouseLeftPressed = true;
        d->mouseLastPos = event->globalPos();
    } else {
        d->mouseLeftPressed = false;
    }

    switch (event->button()) {
    case Qt::LeftButton: {
        if (dragDropMode() != NoDragDrop) {
            setDragDropMode(DragDrop);
        }

        // 按下shift多选文件时，如果isEmptyArea为true,由于 SelectHelper::click
        // 后续会调用到SelectHelper::caculateListViewSelection性能下滑
        // 而输出kFullItemRow后isEmptyArea一直为true，影响文件框选
        auto mode = qApp->keyboardModifiers() == Qt::NoModifier
                ? FileViewHelper::EmptyAreaMode::kDefault
                : FileViewHelper::EmptyAreaMode::kFullItemRow;
        bool isEmptyArea = d->fileViewHelper->isEmptyArea(event->pos(), mode);
        if (isEmptyArea && (qApp->keyboardModifiers() == Qt::NoModifier))
            setCurrentIndex(QModelIndex());

        QModelIndex index = indexAt(event->pos());

        if (itemDelegate())
            itemDelegate()->commitDataAndCloseActiveEditor();

        if (d->currentViewMode == Global::ViewMode::kTreeMode && d->itemsExpandable) {
            if (index.data(kItemTreeViewCanExpandRole).toBool() && expandOrCollapseItem(index, event->pos())) {
                d->lastMousePressedIndex = QModelIndex();
                d->pressedStartWithExpand = true;
                return;
            }
        }

        // 分组视图展开逻辑处理
        if (isGroupHeader(index)) {
            if (groupExpandOrCollapseItem(index, event->pos())) {
                return;
            }

            const QString truncateGroupKey = truncateButtonGroupKeyAt(event->pos());
            if (!truncateGroupKey.isEmpty() && itemDelegate()) {
                itemDelegate()->setPressedTruncateGroupKey(truncateGroupKey);
                viewport()->update();
                return;
            }
        }

        d->selectHelper->click(isEmptyArea ? QModelIndex() : index);

        if (isEmptyArea) {
            if (selectionMode() != QAbstractItemView::SingleSelection)
                d->selectHelper->setSelection(selectionModel()->selection());
            if (!WindowUtils::keyCtrlIsPressed()) {
                if (itemDelegate())
                    itemDelegate()->hideNotEditingIndexWidget();
                if (qApp->keyboardModifiers() == Qt::NoModifier)
                    setCurrentIndex(QModelIndex());
                if (dragDropMode() != NoDragDrop) {
                    setDragDropMode(DropOnly);
                }
            }
        } else if (WindowUtils::keyCtrlIsPressed() && selectionModel()->isSelected(index)) {
            d->selectHelper->setSelection(selectionModel()->selection());
            d->lastMousePressedIndex = index;
            DListView::mousePressEvent(event);

            return;
        } else if (WindowUtils::keyShiftIsPressed()) {
            if (!selectionModel()->isSelected(index)) {   // 如果该项没有被选择
                DListView::mousePressEvent(event);   // 选择该项
                return;
            }
        } else {
            if (selectionMode() != QAbstractItemView::SingleSelection)
                d->selectHelper->setSelection(selectionModel()->selection());
        }

        d->lastMousePressedIndex = QModelIndex();

        DListView::mousePressEvent(event);
        break;
    }
    case Qt::RightButton: {
        bool isEmptyArea = d->fileViewHelper->isEmptyArea(event->pos());

        if (!isEmptyArea) {
            const QModelIndex &index = indexAt(event->pos());
            if (selectedIndexes().isEmpty() || !selectedIndexes().contains(index)) {
                setCurrentIndex(index);
            }
        }

        break;
    }
    default:
        break;
    }
}

void FileView::mouseMoveEvent(QMouseEvent *event)
{
    if (isPosInStickyHeader(event->pos())) {
        return;
    }

    if (d->pressedStartWithExpand)
        return;

    if (itemDelegate() && !itemDelegate()->pressedTruncateGroupKey().isEmpty())
        return;

    if (event->buttons() & Qt::LeftButton)
        d->mouseMoveRect = QRect(event->globalPos(), d->mouseLastPos);

    DListView::mouseMoveEvent(event);
}

void FileView::mouseReleaseEvent(QMouseEvent *event)
{
    d->pressedStartWithExpand = false;

    if (event->buttons() & Qt::LeftButton) {
        d->mouseMoveRect = QRect(-1, -1, 1, 1);
        d->mouseLastPos = QPoint(0, 0);
    }

    d->selectHelper->release();

    if (itemDelegate() && !itemDelegate()->pressedTruncateGroupKey().isEmpty()) {
        const QString pressedGroupKey = itemDelegate()->pressedTruncateGroupKey();
        itemDelegate()->setPressedTruncateGroupKey({});
        viewport()->update();
        if (event->button() == Qt::LeftButton && pressedGroupKey == truncateButtonGroupKeyAt(event->pos())) {
            onGroupTruncationToggled(pressedGroupKey);
        }
        return;
    }

    if (!QScroller::hasScroller(this))
        return DListView::mouseReleaseEvent(event);

    if (WindowUtils::keyCtrlIsPressed()
        && d->lastMousePressedIndex.isValid()
        && d->lastMousePressedIndex == indexAt(event->pos())) {
        selectionModel()->select(d->lastMousePressedIndex, QItemSelectionModel::Deselect);
    }
}

void FileView::dragEnterEvent(QDragEnterEvent *event)
{
    d->dragDropHelper->resetDragState();
    if (d->dragDropHelper->dragEnter(event))
        return;

    DListView::dragEnterEvent(event);
}

void FileView::dragMoveEvent(QDragMoveEvent *event)
{
    // 不调用 QAbstractItemView::dragMoveEvent：它在 canDrop 分支里执行
    // d->viewport->update() 做全 viewport 重绘，仅为绘制 Qt 内置 drop indicator。
    // 而 FileView 已 setDropIndicatorShown(false)，该重绘纯浪费，是拖拽卡顿根因。
    // auto-scroll 由 DragDropHelper 自管（见其 timerEvent），保留滚动能力的同时彻底避免全屏重绘。
    if (d->dragDropHelper->dragMove(event))
        d->dragDropHelper->updateDragHighlight(indexAt(event->position().toPoint()));

    d->dragDropHelper->updateDragAutoScroll(event->position().toPoint());
}

void FileView::dragLeaveEvent(QDragLeaveEvent *event)
{
    setViewSelectState(false);
    d->dragDropHelper->resetDragState();
    if (d->dragDropHelper->dragLeave(event))
        return;

    DListView::dragLeaveEvent(event);
}

void FileView::dropEvent(QDropEvent *event)
{
    setViewSelectState(false);
    d->dragDropHelper->drop(event);
    setState(QAbstractItemView::NoState);
    d->dragDropHelper->resetDragState();
}

QModelIndex FileView::indexAt(const QPoint &pos) const
{
    if (d->stickyHelper->currentStickyIndex().isValid() && d->stickyHelper->currentStickyRect().contains(pos))
        return QModelIndex();

    // For icon mode, use custom calculation
    if (isIconViewMode()) {
        QSize itemSize = itemSizeHint();
        QModelIndex index = iconIndexAt(pos, itemSize);

        // Return invalid index if click is in group header spacing area
        if (isGroupedView() && isClickInGroupHeaderSpacing(pos, index))
            return QModelIndex();

        return index;
    }

    // For list/tree mode (both grouped and non-grouped) items may have variable
    // heights: grouped views add kGroupHeaderInterval to non-first group headers,
    // and non-grouped views add a kDefaultHeaderBottomMargin top padding to the
    // first row. Use Qt's native indexAt which handles variable heights correctly,
    // and treat the transparent spacing/padding areas as empty for selection.
    if (isListViewMode() || isTreeViewMode()) {
        QModelIndex index = DListView::indexAt(pos);

        // Group-header inter-group spacing is an empty area for selection.
        if (isGroupedView() && isClickInGroupHeaderSpacing(pos, index))
            return QModelIndex();

        // Non-grouped first-row top padding (the 10px gap above the list) is an
        // empty area for rubber-band selection, just like the inter-group gap.
        if (!isGroupedView() && isClickInTopPadding(pos, index))
            return QModelIndex();

        return index;
    }

    return QModelIndex();
}

QRect FileView::visualRect(const QModelIndex &index) const
{
    QRect rect;
    if (index.column() != 0)
        return rect;

    if (isListViewMode() || isTreeViewMode()) {
        rect = DListView::visualRect(index);
        rect.moveLeft(rect.left() - horizontalScrollBar()->value());
    } else {
        rect = DListView::visualRect(index);
        if (!d->initHorizontalOffset) {
            d->initHorizontalOffset = true;
            d->updateHorizontalOffset();
            rect = DListView::visualRect(index);
        }
    }

    return rect;
}

void FileView::scrollTo(const QModelIndex &index, ScrollHint hint)
{
    DListView::scrollTo(index, hint);

    if (!isGroupedView() || !index.isValid() || isGroupHeader(index))
        return;

    int headerHeight = stickyHeaderHeight();
    if (headerHeight <= 0)
        return;

    QRect rect = visualRect(index);
    if (rect.isValid() && rect.top() < headerHeight)
        verticalScrollBar()->setValue(verticalScrollBar()->value() - (headerHeight - rect.top()));
}

void FileView::setIconSize(const QSize &size)
{
    DListView::setIconSize(size);

    updateHorizontalOffset();
    updateGeometries();
}

int FileView::horizontalOffset() const
{
    return d->horizontalOffset;
}

int FileView::verticalOffset() const
{
    return DListView::verticalOffset();
}

QList<ItemRoles> FileView::getColumnRoles() const
{
    return d->columnRoles;
}

void FileView::updateGeometries()
{
    int totalHeight = 0;
    if (isIconViewMode()) {
        int iconVerticalTopMargin = 0;
#ifdef DTKWIDGET_CLASS_DSizeMode
        iconVerticalTopMargin = DSizeModeHelper::element(kCompactIconVerticalTopMargin, spacing());
#endif
        if (!d->isResizeEvent
            || (d->isResizeEvent && d->lastContentHeight > 0 && d->lastContentHeight != contentsSize().height()))
            resizeContents(contentsSize().width(), contentsSize().height() + iconVerticalTopMargin);

        totalHeight = contentsSize().height();
        d->lastContentHeight = totalHeight;
    } else {
        // For list/tree mode
        int rowCount = model()->rowCount(rootIndex());
        int listHeight = rowCount * itemSizeHint().height() + kListModeBottomMargin;

        if (isGroupedView()) {
            // In grouped view, non-first group headers have additional 16px spacing
            int groupsCount = model()->getGroupOnlyCount();
            if (groupsCount > 1) {
                // Add spacing for non-first group headers: (groupsCount - 1) * kGroupHeaderInterval
                listHeight += (groupsCount - 1) * kGroupHeaderInterval;
            }
        } else {
            // In non-grouped list/tree mode the first row carries a transparent
            // top padding (kDefaultHeaderBottomMargin) as a content-level gap above
            // the file list, so the scrollable content height must include it.
            if (rowCount > 0)
                listHeight += kDefaultHeaderBottomMargin;
        }

        int contentHeight = contentsSize().height();
        totalHeight = listHeight > contentHeight ? listHeight : contentHeight;
    }

    if (!d->headerView || !d->allowedAdjustColumnSize) {
        return DListView::updateGeometries();
    }
    resizeContents(d->headerView->length(), totalHeight);
    DListView::updateGeometries();
}

void FileView::startDrag(Qt::DropActions supportedActions)
{
    if (NetworkUtils::instance()->checkFtpOrSmbBusy(rootUrl())) {
        DialogManager::instance()->showUnableToVistDir(rootUrl().path());
        return;
    }
    QModelIndexList indexes = d->selectedDraggableIndexes();
    if (!indexes.isEmpty()) {
        QMimeData *data = model()->mimeData(indexes);
        if (!data)
            return;
        Qt::DropAction defaultDropAction = QAbstractItemView::defaultDropAction();
        if (WorkspaceEventSequence::instance()->doCheckDragTarget(data->urls(), QUrl(), &defaultDropAction)) {
            fmDebug() << "Change supported actions: " << defaultDropAction;
            supportedActions = defaultDropAction;
        }

        QList<QUrl> transformedUrls;
        UniversalUtils::urlsTransformToLocal(data->urls(), &transformedUrls);
        fmDebug() << "Drag source urls: " << data->urls();
        fmDebug() << "Drag transformed urls: " << transformedUrls;
        DFMMimeData dfmmimeData;
        dfmmimeData.setUrls(data->urls());
        data->setData(DFMGLOBAL_NAMESPACE::Mime::kDFMMimeDataKey, dfmmimeData.toByteArray());
        data->setUrls(transformedUrls);
        // treeview set treeview select url
        if (isTreeViewMode() && d->itemsExpandable) {
            auto treeSelectedUrl = selectedTreeViewUrlList();
            transformedUrls.clear();
            UniversalUtils::urlsTransformToLocal(treeSelectedUrl, &transformedUrls);
            QByteArray ba;
            for (const auto &url : transformedUrls) {
                ba.append(QString(url.toString() + "\n").toUtf8());
            }
            data->setData(DFMGLOBAL_NAMESPACE::Mime::kDFMTreeUrlsKey, ba);
        }

        QPixmap pixmap = d->viewDrawHelper->renderDragPixmap(currentViewMode(), indexes);
        QDrag *drag = new QDrag(this);
        drag->setPixmap(pixmap);
        drag->setMimeData(data);
        drag->setHotSpot(QPoint(static_cast<int>(pixmap.size().width() / (2 * pixmap.devicePixelRatio())),
                                static_cast<int>(pixmap.size().height() / (2 * pixmap.devicePixelRatio()))));

        Qt::DropAction dropAction = Qt::IgnoreAction;
        if (defaultDropAction != Qt::IgnoreAction && (supportedActions & defaultDropAction))
            dropAction = defaultDropAction;
        else if (supportedActions & Qt::CopyAction && dragDropMode() != QAbstractItemView::InternalMove)
            dropAction = Qt::CopyAction;

        drag->exec(supportedActions, dropAction);
    }
}

QModelIndexList FileView::selectedIndexes() const
{
    FileSelectionModel *fileSelectionModel = dynamic_cast<FileSelectionModel *>(selectionModel());
    if (fileSelectionModel) {
        QModelIndexList indexes = fileSelectionModel->selectedIndexes();

        auto isInvalid = [=](const QModelIndex &index) {
            return !(index.isValid());
        };

        indexes.erase(std::remove_if(indexes.begin(), indexes.end(), isInvalid),
                      indexes.end());

        return indexes;
    }

    return QModelIndexList();
}

void FileView::keyboardSearch(const QString &search)
{
    d->fileViewHelper->keyboardSearch(search);
}

void FileView::contextMenuEvent(QContextMenuEvent *event)
{
    // if the left btn is pressed and the rect large enough, means the view state is draggingSelectState.
    // and don't show menu in the draggingSelectState.
    if (d->mouseLeftPressed && (abs(d->mouseMoveRect.width()) > kMinMoveLenght || abs(d->mouseMoveRect.height()) > kMinMoveLenght)) {
        fmDebug() << "Context menu blocked due to drag selection state";
        return;
    }

    // Reset any stale drag-select state left by touch long-press before showing the menu.
    // On touch screens a long-press synthesizes a left-button press/move that sets
    // DragSelectingState; the subsequent release is consumed by the modal menu exec()
    // and never reaches mouseReleaseEvent, so the state is never reset. A stale
    // DragSelectingState later makes QListView::resizeEvent skip doDelayedItemsLayout,
    // leaving visualRect() with stale item positions and breaking icon layout on
    // maximize. Resetting here mirrors what mouseReleaseEvent would do.
    setState(QAbstractItemView::NoState);

    if (NetworkUtils::instance()->checkFtpOrSmbBusy(rootUrl())) {
        fmWarning() << "Cannot show context menu: FTP or SMB is busy for URL:" << rootUrl().toString();
        DialogManager::instance()->showUnableToVistDir(rootUrl().path());
        return;
    }

    if (FileViewMenuHelper::disableMenu()) {
        fmDebug() << "Context menu disabled by helper";
        return;
    }

    d->viewMenuHelper->setWaitCursor();
    const QModelIndex &index = indexAt(event->pos());
    if (itemDelegate()->editingIndex().isValid() && itemDelegate()->editingIndex() == index) {
        fmDebug() << "Setting focus due to editing index";
        setFocus(Qt::FocusReason::OtherFocusReason);
    }

    if (d->fileViewHelper->isEmptyArea(event->pos())) {
        fmDebug() << "Showing context menu for empty area";
        BaseItemDelegate *de = itemDelegate();
        if (de)
            de->hideNotEditingIndexWidget();
        clearSelection();

        d->viewMenuHelper->showEmptyAreaMenu();
    } else {
        if (!isSelected(index)) {
            fmDebug() << "Item not selected, clearing selection and selecting clicked item";
            itemDelegate()->hideNotEditingIndexWidget();
            clearSelection();

            if (!index.isValid()) {
                fmDebug() << "Invalid index, showing empty area menu";
                d->viewMenuHelper->showEmptyAreaMenu();
                d->viewMenuHelper->reloadCursor();
                return;
            }

            selectionModel()->select(index, QItemSelectionModel::Select);
        }

        d->viewMenuHelper->showNormalMenu(index, model()->flags(index));
    }
}

QModelIndex FileView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
    if (!rootIndex().isValid())
        return QModelIndex();

    QModelIndex current = currentIndex();

    if (!current.isValid()) {
        d->lastCursorIndex = DListView::moveCursor(cursorAction, modifiers);

        return d->lastCursorIndex;
    }

    if (rectForIndex(current).isEmpty()) {
        d->lastCursorIndex = model()->index(0, 0, rootIndex());

        return d->lastCursorIndex;
    }

    QModelIndex index;

    switch (cursorAction) {
    case MoveLeft:
        if (WindowUtils::keyShiftIsPressed()) {
            index = DListView::moveCursor(cursorAction, modifiers);

            if (index == d->lastCursorIndex) {
                index = index.sibling(index.row() - 1, index.column());
            }
        } else {
            index = current.sibling(current.row() - 1, current.column());
        }

        // rekols: Loop to find the next file item that can be selected.
        while (index.model() && !(index.flags() & Qt::ItemIsSelectable) && index.isValid()) {
            index = index.sibling(index.row() - 1, index.column());
        }

        break;

    case MoveRight:
        if (WindowUtils::keyShiftIsPressed()) {
            index = DListView::moveCursor(cursorAction, modifiers);

            if (index == d->lastCursorIndex) {
                index = index.sibling(index.row() + 1, index.column());
            }
        } else {
            index = current.sibling(current.row() + 1, current.column());
        }

        while (index.model() && !(index.flags() & Qt::ItemIsSelectable) && index.isValid()) {
            index = index.sibling(index.row() + 1, index.column());
        }

        break;

    default:
        index = DListView::moveCursor(cursorAction, modifiers);
        break;
    }

    if (index.isValid()) {
        if (d->currentViewMode == DFMGLOBAL_NAMESPACE::ViewMode::kIconMode) {
            bool lastRow = indexOfRow(index) == rowCount() - 1;

            if (!lastRow
                && current == index
                && (cursorAction == MoveDown
                    || cursorAction == MovePageDown
                    || cursorAction == MoveNext)) {
                // When there is no element at the next position, QListView will not automatically switch columns to select, you should directly select the last one
                index = model()->index(count() - 1, 0, rootIndex());
                lastRow = true;
            }

            if (lastRow) {
                // call later
                // QTimer::singleShot(0, this, [this, index, d] {//this index unused,改成如下
                QTimer::singleShot(0, this, [this] {
                    // scroll to end
                    verticalScrollBar()->setValue(verticalScrollBar()->maximum());
                });
            }
        }

        d->lastCursorIndex = index;

        return index;
    }

    d->lastCursorIndex = current;

    return current;
}

bool FileView::event(QEvent *e)
{
    switch (e->type()) {
    case QEvent::KeyPress: {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent && (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)) {
            if (keyEvent->modifiers() == Qt::ControlModifier || keyEvent->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier))
                return DListView::event(e);
            e->accept();

            if (keyEvent->modifiers() == Qt::ShiftModifier) {
                QKeyEvent nkeyEvent(keyEvent->type(), Qt::Key_Left, Qt::NoModifier);
                keyPressEvent(&nkeyEvent);
            } else {
                QKeyEvent nkeyEvent(keyEvent->type(), Qt::Key_Right, Qt::NoModifier);
                keyPressEvent(&nkeyEvent);
            }

            return true;
        }
    } break;
    case QEvent::Resize:
        d->pureResizeEvent(static_cast<QResizeEvent *>(e));
        break;
    case QEvent::ParentChange:
        window()->installEventFilter(this);
        break;
    case QEvent::FontChange:
        // blumia: to trigger DIconItemDelegate::updateItemSizeHint() to update its `d->itemSizeHint` ...
        emit iconSizeChanged(iconSize());
        break;
    default:
        break;
    }

    return DListView::event(e);
}

bool FileView::eventFilter(QObject *obj, QEvent *event)
{
    switch (event->type()) {
    case QEvent::MouseButtonRelease: {
        QWidget *viewport = qobject_cast<QWidget *>(obj);
        quint64 winId = dfmbase::FileManagerWindowsManager::instance().findWindowId(viewport);
        auto e = static_cast<QMouseEvent *>(event);
        if (!e)
            break;
        if (e->button() == Qt::BackButton) {
            dpfSlotChannel->push("dfmplugin_titlebar", "slot_Navigator_Backward", winId);
            return true;
        } else if (e->button() == Qt::ForwardButton) {
            dpfSlotChannel->push("dfmplugin_titlebar", "slot_Navigator_Forward", winId);
            return true;
        }
    } break;
    case QEvent::Move:
        if (obj != horizontalScrollBar()->parentWidget())
            return DListView::eventFilter(obj, event);
        d->updateHorizontalScrollBarPosition();
        break;
    case QEvent::WindowStateChange:
        d->animationHelper->suppressNextResizeAnimation();
        if (d->headerView) {
            d->adjustFileNameColumn = true;
            d->headerView->doFileNameColumnResize(width());
        }
        break;
    case QEvent::HoverMove: {
        auto *he = static_cast<QHoverEvent *>(event);
        QPoint hoverPos;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        hoverPos = he->pos();
#else
        hoverPos = he->position().toPoint();
#endif
        if (QWidget *w = qobject_cast<QWidget *>(obj)) {
            if (w != viewport())
                hoverPos = viewport()->mapFromGlobal(w->mapToGlobal(hoverPos));
        }

        if (d->stickyHelper->currentStickyIndex().isValid() && !d->stickyHelper->currentStickyRect().isNull()) {
            QRect stickyRect = d->stickyHelper->currentStickyRect();
            if (stickyRect.y() < 0)
                stickyRect.setY(0);

            bool nowHovered = stickyRect.contains(hoverPos);
            if (nowHovered != d->stickyHelper->isStickyHeaderHovered()) {
                d->stickyHelper->setStickyHeaderHovered(nowHovered);
                viewport()->update(d->stickyHelper->currentStickyRect());
            }
        }
        updateTruncateButtonHover(hoverPos);
        break;
    }
    case QEvent::HoverLeave: {
        if (d->stickyHelper->isStickyHeaderHovered() && d->stickyHelper->currentStickyIndex().isValid()) {
            d->stickyHelper->setStickyHeaderHovered(false);
            viewport()->update(d->stickyHelper->currentStickyRect());
        }
        clearTruncateButtonHover();
        break;
    }
    // blumia: 这里通过给横向滚动条加事件过滤器并监听其显示隐藏时间来判断是否应当进入吸附状态。
    //         不过其实可以通过 Resize 事件的 size 和 oldSize 判断是否由于窗口调整大小而进入了吸附状态。
    //         鉴于已经实现完了，如果当前的实现方式实际发现了较多问题，则应当调整为使用 Resize 事件来标记吸附状态的策略。
    case QEvent::ShowToParent:
    case QEvent::HideToParent:
        if (d->headerView && d->cachedViewWidth != this->width()) {
            d->cachedViewWidth = this->width();
            d->adjustFileNameColumn = true;
        }
        break;
    default:
        break;
    }

    return DListView::eventFilter(obj, event);
}

void FileView::paintEvent(QPaintEvent *event)
{
    // 仅在从无分组切换到有分组时才跳过绘制,避免UI异常
    // 从一种分组策略切换到另一种分组策略时允许绘制,保持流畅性
    if (model()->groupingState() == GroupingState::kGrouping) {
        QString currentStrategy = model()->groupingStrategy();
        bool isFromNoGroupToGrouped = (d->previousGroupStrategy == GroupStrategy::kNoGroup
                                       && currentStrategy != GroupStrategy::kNoGroup);
        if (isFromNoGroupToGrouped) {
            fmDebug() << "Skipping paint during initial grouping from none to:" << currentStrategy;
            return;
        }
        // 允许分组间切换时的绘制
        fmDebug() << "Allow paint during group-to-group switch from:"
                  << d->previousGroupStrategy << "to:" << currentStrategy;
    }

    if (d->animationHelper->isWaitingToPlaying() || d->animationHelper->isAnimationPlaying()) {
        d->animationHelper->paintItems();
        itemDelegate()->hideAllIIndexWidget();
        return;
    }

    if (d->horizontalOffset == 0)
        d->updateHorizontalOffset();

    // 设置标志是否记录当前绘制的itemindex
    DListView::paintEvent(event);

    if (d->isShowViewSelectBox) {
        QPainter painter(viewport());
        QColor color = palette().color(QPalette::Active, QPalette::Highlight);
        color.setAlphaF(0.4);   // 40% transparency
        QPen pen(color, kSelectBoxLineWidth);
        painter.setPen(pen);
        painter.drawRect(QRectF(kSelectBoxLineWidth / 2, kSelectBoxLineWidth / 2, viewport()->size().width() - kSelectBoxLineWidth, viewport()->size().height() - kSelectBoxLineWidth));
    }

    if (isGroupedView() && model()->groupingState() == GroupingState::kIdle) {
        const int headerHeight = stickyHeaderHeight();
        QModelIndex stickyIdx = findStickyGroupIndex(headerHeight);
        if (stickyIdx.isValid()) {
            int stickyY = computeStickyY(headerHeight);
            paintStickyHeaderOverlay(stickyIdx, stickyY, headerHeight);
        } else if (d->stickyHelper->currentStickyIndex().isValid()) {
            clearStickyHeaderState();
        }
    } else if (d->stickyHelper->currentStickyIndex().isValid()) {
        clearStickyHeaderState();
    }
}

void FileView::focusInEvent(QFocusEvent *event)
{
    DListView::focusInEvent(event);
    if (itemDelegate())
        itemDelegate()->commitDataAndCloseActiveEditor();

    setAttribute(Qt::WA_InputMethodEnabled, false);
}

void FileView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
{
    DListView::currentChanged(current, previous);
    setAttribute(Qt::WA_InputMethodEnabled, false);
}

void FileView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
    QModelIndex currentIdx = currentIndex();
    QItemSelection selectionToDeselect;
    bool currentInRemovedRange = false;

    // 遍历所有选中的索引，找出需要取消选择的项
    for (const QModelIndex &index : selectedIndexes()) {
        if (index.parent() != parent)
            continue;

        if (index.row() >= start && index.row() <= end) {
            // 收集需要取消选择的 index（避免在迭代中修改选择模型）
            selectionToDeselect.select(index, index);

            // 检查 currentIndex 是否在被删除范围内
            if (index == currentIdx) {
                currentInRemovedRange = true;
            }
        }
    }

    // 批量取消被删除项的选择（不影响其他选择）
    if (!selectionToDeselect.isEmpty()) {
        selectionModel()->select(selectionToDeselect, QItemSelectionModel::Deselect);
    }

    // 如果 currentIndex 被删除，提前清空，避免 Qt 自动跳转到下一个 item
    if (currentInRemovedRange) {
        setCurrentIndex(QModelIndex());
    }

    if (itemDelegate())
        itemDelegate()->hideNotEditingIndexWidget();

    DListView::rowsAboutToBeRemoved(parent, start, end);
}

void FileView::showEvent(QShowEvent *event)
{
    DListView::showEvent(event);
    focusOnView();

    d->animationHelper->suppressNextResizeAnimation();
}

void FileView::initializeModel()
{
    fmDebug() << "Initializing FileView model and selection model";

    FileViewModel *viewModel = new FileViewModel(this);
    setModel(viewModel);

    FileSelectionModel *selectionModel = new FileSelectionModel(viewModel, this);
    setSelectionModel(selectionModel);

    fmDebug() << "Model and selection model initialized successfully";
}

void FileView::initializeDelegate()
{
    fmDebug() << "Initializing FileView delegates";

    d->fileViewHelper = new FileViewHelper(this);
    d->geometryHelper = new ViewGeometryHelper(this);
    d->stickyHelper = new StickyGroupHeaderHelper(this);

    // Create delegates
    auto iconDelegate = new IconItemDelegate(d->fileViewHelper);
    auto listDelegate = new ListItemDelegate(d->fileViewHelper);

    setDelegate(Global::ViewMode::kIconMode, iconDelegate);
    setDelegate(Global::ViewMode::kListMode, listDelegate);

    d->itemsExpandable = DConfigManager::instance()->value(kViewDConfName, kTreeViewEnable, true).toBool()
            && WorkspaceHelper::instance()->isViewModeSupported(rootUrl().scheme(), DFMGLOBAL_NAMESPACE::ViewMode::kTreeMode);
    if (d->selectHelper)
        d->selectHelper->setItemsExpandable(d->itemsExpandable);
    updateDelegateHighlightKeywords(model()->getKeyWords());

    fmDebug() << "Delegates initialized with grouping signal connections, items expandable:" << d->itemsExpandable;
}

void FileView::initializeStatusBar()
{
    fmDebug() << "Initializing FileView status bar";

    ViewDefines viewDefines;
    d->statusBar = new FileViewStatusBar(this);
    d->statusBar->resetScalingSlider(viewDefines.iconSizeCount() - 1);

    d->updateStatusBarTimer = new QTimer(this);
    d->updateStatusBarTimer->setInterval(100);
    d->updateStatusBarTimer->setSingleShot(true);

    addFooterWidget(d->statusBar);

    fmDebug() << "Status bar initialized with" << viewDefines.iconSizeCount() << "icon size levels";
}

void FileView::initializeConnect()
{
    connect(d->updateStatusBarTimer, &QTimer::timeout, this, &FileView::updateStatusBar);

    connect(d->statusBar->scalingSlider(), &DSlider::valueChanged, this, &FileView::onScalingValueChanged);

    connect(model(), &FileViewModel::stateChanged, this, &FileView::onModelStateChanged);
    connect(
            model(), &FileViewModel::groupingStateChanged, this, [this](GroupingState) {
                if (isIconViewMode())
                    d->adjustIconModeSpacing(model()->groupingStrategy());
            },
            Qt::QueuedConnection);
    connect(model(), &FileViewModel::highlightKeywordsChanged, this, [this](const QStringList &keywords) {
        updateDelegateHighlightKeywords(keywords);
        update();
    });
    connect(model(), &FileViewModel::selectAndEditFile, this, &FileView::onSelectAndEdit);
    connect(model(), &FileViewModel::dataChanged, this, &FileView::updateOneView);
    connect(model(), &FileViewModel::renameFileProcessStarted, this, &FileView::onRenameProcessStarted);
    connect(model(), &FileViewModel::aboutToSwitchToListView, this, &FileView::onAboutToSwitchListView);
    connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileView::onSelectionChanged);

    connect(this, &DListView::rowCountChanged, this, &FileView::onRowCountChanged, Qt::QueuedConnection);
    connect(this, &DListView::clicked, this, &FileView::onClicked, Qt::UniqueConnection);
    connect(this, &DListView::doubleClicked, this, &FileView::onDoubleClicked);
    connect(this, &DListView::iconSizeChanged, this, &FileView::updateHorizontalOffset, Qt::QueuedConnection);

    connect(Application::instance(), &Application::iconSizeLevelChanged, this, &FileView::onIconSizeChanged);
    connect(Application::instance(), &Application::gridDensityLevelChanged, this, &FileView::onItemWidthLevelChanged);
    connect(Application::instance(), &Application::listHeightLevelChanged, this, &FileView::onItemHeightLevelChanged);
    connect(Application::instance(), &Application::showedFileSuffixChanged, this, &FileView::onShowFileSuffixChanged);
    connect(Application::instance(), &Application::previewAttributeChanged, this, &FileView::onWidgetUpdate);
    connect(Application::instance(), &Application::viewModeChanged, this, &FileView::onDefaultViewModeChanged);
    connect(Application::appObtuselySetting(), &Settings::valueChanged, this, &FileView::onAppAttributeChanged);
    connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::sizeModeChanged, this, [this]() {
        if (d->currentViewMode == Global::ViewMode::kIconMode)
            d->adjustIconModeSpacing(model()->groupingStrategy());
    });

    dpfSignalDispatcher->subscribe("dfmplugin_workspace", "signal_View_HeaderViewSectionChanged", this, &FileView::onHeaderViewSectionChanged);

    connect(&FileInfoHelper::instance(), &FileInfoHelper::smbSeverMayModifyPassword, this, [this](const QUrl &url) {
        if (ProtocolUtils::isSMBFile(rootUrl()) && url.path().startsWith(rootUrl().path())) {
            fmInfo() << rootUrl() << url << "smb server may modify password";
            if (d->isShowSmbMountError)
                return;
            d->isShowSmbMountError = true;
            DialogManager::instance()->showErrorDialog(tr("Mount error"),
                                                       tr("Server login credentials are invalid. Please uninstall and remount"));
            d->isShowSmbMountError = false;
        }
    });
}

void FileView::updateDelegateHighlightKeywords(const QStringList &keywords)
{
    for (auto delegate : d->delegates) {
        if (delegate)
            delegate->setHighlightKeywords(keywords);
    }
}

void FileView::initializeScrollBarWatcher()
{
    d->scrollBarValueChangedTimer = new QTimer(this);
    d->scrollBarValueChangedTimer->setInterval(50);
    d->scrollBarValueChangedTimer->setSingleShot(true);

    connect(d->scrollBarValueChangedTimer, &QTimer::timeout, this, [this] { this->update(); });

    connect(verticalScrollBar(), &QScrollBar::sliderPressed, this, [this] { d->scrollBarSliderPressed = true; });
    connect(verticalScrollBar(), &QScrollBar::sliderReleased, this, [this] { d->scrollBarSliderPressed = false; });
    connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
        if (d->scrollBarSliderPressed)
            d->scrollBarValueChangedTimer->start();

        if (isGroupedView()) {
            viewport()->update();
        }
    });
}

void FileView::initializePreSelectTimer()
{
    d->preSelectTimer = new QTimer(this);

    d->preSelectTimer->setInterval(100);
    d->preSelectTimer->setSingleShot(true);
    connect(d->preSelectTimer, &QTimer::timeout, this, [=] {
        if (selectFiles(d->preSelectionUrls))
            d->preSelectionUrls.clear();
    });
}

void FileView::initializeGroupHeaderTimer()
{
    d->groupHeaderTimer = new QTimer(this);

    d->groupHeaderTimer->setInterval(qApp->doubleClickInterval());
    d->groupHeaderTimer->setSingleShot(true);
    connect(d->groupHeaderTimer, &QTimer::timeout, this, [this] {
        onGroupHeaderClicked(d->lastClickedIndex);
    });
}

void FileView::updateStatusBar()
{
    if (model()->currentState() != ModelState::kIdle) {
        fmDebug() << "Status bar update skipped: model is busy for URL:" << rootUrl().toString();
        return;
    }

    int count = selectedIndexCount();

    if (count == 0) {
        // Use file-only count for status bar (excludes group headers in grouped mode)
        int totalCount = model()->getFileOnlyCount();
        d->statusBar->itemCounted(totalCount);
        fmDebug() << "Status bar updated: no selection, total items:" << totalCount << "for URL:" << rootUrl().toString();
        return;
    }

    QList<QUrl> list;
    int selectFiles = 0;
    int selectFolders = 0;
    qint64 filesizes = 0;
    for (const auto &index : selectedIndexes()) {
        if (index.data(Global::ItemRoles::kItemFileIsDirRole).toBool()) {
            selectFolders++;
            list << index.data(Global::ItemRoles::kItemUrlRole).value<QUrl>();
        } else {
            selectFiles++;
            filesizes += index.data(Global::ItemRoles::kItemFileSizeIntRole).toLongLong();
        }
    }

    d->statusBar->itemSelected(selectFiles, selectFolders, filesizes, list);
    fmDebug() << "Status bar updated: selected files:" << selectFiles << "folders:" << selectFolders << "size:" << filesizes << "for URL:" << rootUrl().toString();
}

void FileView::updateLoadingIndicator()
{
    auto state = model()->currentState();
    if (state == ModelState::kBusy) {
        QString tip;

        FileInfoPointer fileInfo = InfoFactory::create<FileInfo>(rootUrl());
        if (fileInfo)
            tip = fileInfo->viewOfTip(ViewInfoType::kLoading);

        d->statusBar->showLoadingIncator(tip);
        fmDebug() << "Loading indicator shown for URL:" << rootUrl().toString();
    }

    if (state == ModelState::kIdle) {
        d->statusBar->hideLoadingIncator();
        updateStatusBar();
        fmDebug() << "Loading indicator hidden for URL:" << rootUrl().toString();
    }
}

void FileView::updateContentLabel()
{
    d->initContentLabel();
    if (model()->currentState() == ModelState::kBusy
        || model()->canFetchMore(rootIndex())) {
        d->contentLabel->setText(QString());
        return;
    }

    bool isEmpty = (count() <= 0);

    if (isEmpty) {
        // set custom empty tips
        const FileInfoPointer &fileInfo = model()->fileInfo(rootIndex());
        if (fileInfo) {
            d->contentLabel->setText(fileInfo->viewOfTip(ViewInfoType::kEmptyDir));
            d->contentLabel->adjustSize();
        } else {
            d->contentLabel->setText(QString());
        }
    } else {
        d->contentLabel->setText(QString());
    }
}

void FileView::updateSelectedUrl()
{
    if (d->preSelectionUrls.isEmpty() || model()->currentState() != ModelState::kIdle)
        return;

    d->preSelectTimer->start();
}

void FileView::updateListHeaderView()
{
    if (!d->headerView) {
        return;
    }

    d->headerView->setModel(Q_NULLPTR);
    d->headerView->setModel(model());
    d->headerView->setRootIndex(rootIndex());

    d->headerView->setDefaultSectionSize(kDefaultHeaderSectionWidth);
    if (d->allowedAdjustColumnSize) {
        d->headerView->setSectionResizeMode(QHeaderView::Interactive);
        d->headerView->setMinimumSectionSize(kMinimumHeaderSectionWidth);
    } else {
        d->headerView->setSectionResizeMode(QHeaderView::Fixed);
        d->headerView->setMinimumSectionSize(kDefaultHeaderSectionWidth);
    }

    d->headerView->setSortIndicator(model()->getColumnByRole(model()->sortRole()), model()->sortOrder());
    d->columnRoles.clear();

    const QVariantMap &state = Application::appObtuselySetting()->value("WindowManager", "ViewColumnState").toMap();
    const QVariantMap &hiddenState = d->fileViewStateValue(rootUrl(), "columnHiddenState", {}).toMap();

    for (int i = 0; i < d->headerView->count(); ++i) {
        int logicalIndex = d->headerView->logicalIndex(i);
        d->columnRoles << model()->getRoleByColumn(i);
        ItemRoles curRole = d->columnRoles.last();

        if (d->allowedAdjustColumnSize) {
            int colWidth = state.value(QString::number(curRole), -1).toInt();
            if (colWidth > 0) {
                d->headerView->resizeSection(model()->getColumnByRole(curRole), colWidth);
            } else {
                if (curRole == kItemFileLastModifiedRole || curRole == kItemFileCreatedRole) {
                    d->headerView->resizeSection(model()->getColumnByRole(curRole), kDefaultItemFileTimeWidth);
                }
            }
        } else {
            int columnWidth = model()->getColumnWidth(i);
            if (columnWidth >= 0) {
                d->headerView->resizeSection(logicalIndex, columnWidth + kColumnPadding * 2);
            } else {
                d->headerView->setSectionResizeMode(logicalIndex, QHeaderView::Stretch);
            }
        }

        const QString &columnName = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
        bool hidden = hiddenState.value(QString::number(curRole), curRole == kItemFileCreatedRole).toBool();
        d->columnForRoleHiddenMap[columnName] = hidden;
        d->headerView->setSectionHidden(logicalIndex, hidden);
    }

    if (d->adjustFileNameColumn) {
        d->headerView->doFileNameColumnResize(width());
    }
}

void FileView::setDefaultViewMode()
{
    setViewMode(d->currentViewMode);
}

void FileView::setListViewMode()
{
    setUniformItemSizes(false);
    setResizeMode(QListView::Fixed);
    setOrientation(QListView::TopToBottom, false);
    setSpacing(kListViewSpacing);

    d->initListModeView();
    if (d->allowedAdjustColumnSize) {
        horizontalScrollBar()->parentWidget()->installEventFilter(this);

        d->cachedViewWidth = this->width();
        d->adjustFileNameColumn = true;
        updateListHeaderView();
    }

    verticalScrollBar()->setFixedHeight(rect().height() - d->statusBar->height() - d->headerView->height());
}

QUrl FileView::parseSelectedUrl(const QUrl &url)
{
    auto fileUrl = url;
    QUrlQuery urlQuery;
    QByteArray encode = QUrl::toPercentEncoding(fileUrl.query(QUrl::FullyEncoded), "=");
    urlQuery.setQuery(encode);
    const auto &selectFile = urlQuery.queryItemValue("selectUrl", QUrl::FullyDecoded);

    const QUrl &defaultSelectUrl = QUrl::fromUserInput(selectFile);
    if (defaultSelectUrl.isValid()) {
        d->preSelectionUrls.clear();
        d->preSelectionUrls << defaultSelectUrl;
        urlQuery.removeQueryItem("selectUrl");
        fileUrl.setQuery(urlQuery);
    } else {
        // todo: liuzhangjian
        // checkGvfsMountfileBusy
        QList<QUrl> ancestors;
        if (const FileInfoPointer &currentFileInfo = InfoFactory::create<FileInfo>(rootUrl())) {
            if (UrlRoute::isAncestorsUrl(rootUrl(), fileUrl, &ancestors)) {
                d->preSelectionUrls.clear();
                d->preSelectionUrls << (ancestors.count() > 1 ? ancestors.at(ancestors.count() - 2) : rootUrl());
            }
        }
    }

    return fileUrl;
}

void FileView::loadViewState(const QUrl &url)
{
    d->loadViewMode(url);

    QVariant defaultIconSize = Application::instance()->appAttribute(Application::kIconSizeLevel).toInt();
    QVariant defaultGridDensity = Application::instance()->appAttribute(Application::kGridDensityLevel).toInt();

    d->currentIconSizeLevel = d->fileViewStateValue(url, "iconSizeLevel", defaultIconSize).toInt();
    d->currentGridDensityLevel = d->fileViewStateValue(url, "gridDensityLevel", defaultGridDensity).toInt();

    int customListHeightLevel = d->fileViewHelper->customDefaultListItemHeightLevel();
    QVariant defaultListHeight = customListHeightLevel >= 0 ? customListHeightLevel : Application::instance()->appAttribute(Application::kListHeightLevel).toInt();
    if (d->fileViewHelper->canChangeListItemHeight()) {
        d->currentListHeightLevel = d->fileViewStateValue(url, "listHeightLevel", defaultListHeight).toInt();
    } else {
        d->currentListHeightLevel = customListHeightLevel;
    }
}

void FileView::onModelStateChanged()
{
    fmDebug() << "Model state changed to:" << static_cast<int>(model()->currentState()) << "for URL:" << rootUrl().toString();

    clearStickyHeaderState();
    updateContentLabel();
    updateLoadingIndicator();
    updateSelectedUrl();

    if (model()->currentState() == ModelState::kBusy) {
        fmDebug() << "Model is busy - disabling header interactions for URL:" << rootUrl().toString();
        if (d->headerView) {
            d->headerView->setSortIndicatorShown(false);
            d->headerView->setSectionsClickable(false);
        }
        d->animationHelper->reset();
    } else {
        fmDebug() << "Model is idle - enabling header interactions for URL:" << rootUrl().toString();
        if (d->headerView) {
            d->headerView->setSortIndicatorShown(true);
            d->headerView->setSectionsClickable(true);
        }
        d->animationHelper->initAnimationHelper();
    }

    if (d->headerView)
        d->headerView->setAttribute(Qt::WA_TransparentForMouseEvents, model()->currentState() == ModelState::kBusy);

    notifyStateChanged();
}

void FileView::openIndexByClicked(const ClickedAction action, const QModelIndex &index)
{
    ClickedAction configAction = static_cast<ClickedAction>(Application::instance()->appAttribute(Application::kOpenFileMode).toInt());
    if (action == configAction) {
        Qt::ItemFlags flags = model()->flags(index);
        if (!flags.testFlag(Qt::ItemIsEnabled)) {
            fmDebug() << "Item not enabled, skipping open for URL:" << rootUrl().toString();
            return;
        }

        if (!WindowUtils::keyCtrlIsPressed() && !WindowUtils::keyShiftIsPressed())
            openIndex(index);
    }
}

void FileView::openIndex(const QModelIndex &index)
{
    const FileInfoPointer &info = model()->fileInfo(index);

    if (!info) {
        fmWarning() << "Cannot open index: file info is null";
        return;
    }

    QUrl fileUrl = info->urlOf(UrlInfoType::kUrl);
    fmInfo() << "Opening file:" << fileUrl;
    if (NetworkUtils::instance()->checkFtpOrSmbBusy(fileUrl)) {
        fmWarning() << "Cannot open file: FTP or SMB is busy for URL:" << fileUrl;
        DialogManager::instance()->showUnableToVistDir(fileUrl.path());
        return;
    }

    FileOperatorHelperIns->openFiles(this, { info->urlOf(UrlInfoType::kUrl) });
}

void FileView::setFileViewStateValue(const QUrl &url, const QString &key, const QVariant &value)
{
    WorkspaceHelper::instance()->setFileViewStateValue(url, key, value);
}

FileView::RangeIndexList FileView::calcGroupRectContiansIndexes(const QRect &rect) const
{
    return d->geometryHelper->calcGroupRectContiansIndexes(rect);
}

void FileView::saveViewModeState()
{
    const QUrl &url = rootUrl();

    setFileViewStateValue(url, "viewMode", static_cast<int>(d->currentViewMode));
}

void FileView::focusOnView()
{
    if (WorkspaceHelper::instance()->isFocusFileViewDisabled(d->url.scheme()))
        return;

    if (isVisible())
        setFocus();
}

bool FileView::isGroupHeader(const QModelIndex &index) const
{
    return !index.data(kItemGroupHeaderKey).toString().isEmpty();
}

int FileView::groupHeaderContentTop(const QModelIndex &index) const
{
    return d->stickyHelper->groupHeaderContentTop(index);
}

bool FileView::isClickInGroupHeaderSpacing(const QPoint &pos, const QModelIndex &index) const
{
    if (!index.isValid() || !isGroupHeader(index))
        return false;

    // First group header (displayIndex == 0) has no spacing above it
    int displayIndex = index.data(Global::kItemGroupDisplayIndex).toInt();
    if (displayIndex == 0)
        return false;

    // Get the visual rect in viewport coordinates
    QRect itemRect = visualRect(index);

    // Check if click is in the top 16px spacing area
    int relativeY = pos.y() - itemRect.top();
    return (relativeY >= 0 && relativeY < kGroupHeaderInterval);
}

bool FileView::isClickInTopPadding(const QPoint &pos, const QModelIndex &index) const
{
    // Only the root's first row in non-grouped list/tree carries the top padding.
    // Requiring parent == rootIndex avoids matching tree-mode expanded sub-items
    // whose row is also 0 under a different parent.
    if (!index.isValid() || isGroupedView() || isIconViewMode())
        return false;
    if (index.row() != 0 || index.parent() != rootIndex())
        return false;

    // The transparent top padding (kDefaultHeaderBottomMargin) sits at the top of
    // the first row's visual rect; a click there is an empty area for selection.
    QRect itemRect = visualRect(index);
    int relativeY = pos.y() - itemRect.top();
    return (relativeY >= 0 && relativeY < kDefaultHeaderBottomMargin);
}

QModelIndex FileView::indexAtForSelection(const QPoint &pos) const
{
    // Similar to indexAt(), but doesn't skip 16px spacing areas for box selection
    if (isIconViewMode()) {
        return iconIndexAt(pos, itemSizeHint());
    }

    // For list/tree mode (grouped or not) use Qt's native indexAt to support
    // variable row heights (group-header spacing / first-row top padding).
    if (isListViewMode() || isTreeViewMode()) {
        // For list mode (single column), clamp X coordinate to viewport range
        // Only Y coordinate matters for determining which row is selected
        QPoint clampedPos = pos;
        clampedPos.setX(qBound(0, pos.x(), viewport()->width() - 1));
        return DListView::indexAt(clampedPos);
    }

    return QModelIndex();
}

// Grouping-related slot implementations
void FileView::onGroupExpansionToggled(const QString &groupKey)
{
    fmDebug() << "Group expansion toggled for key:" << groupKey << "for URL:" << rootUrl().toString();

    if (groupKey.isEmpty()) {
        fmWarning() << "Cannot toggle expansion: empty group key";
        return;
    }

    // Forward to model for handling
    clearStickyHeaderState();
    if (model()) {
        model()->toggleGroupExpansion(groupKey);
    }
}

void FileView::onGroupTruncationToggled(const QString &groupKey)
{
    if (groupKey.isEmpty() || !model()) {
        return;
    }

    model()->toggleGroupTruncation(groupKey);
}

void FileView::onGroupHeaderClicked(const QModelIndex &index)
{
    fmDebug() << "Group header clicked for:" << index << "for URL:" << rootUrl().toString();

    if (!index.isValid()) {
        fmWarning() << "Cannot handle header click: empty group key";
        return;
    }

    // Use SelectHelper to handle group selection
    if (d->selectHelper) {
        d->selectHelper->handleGroupHeaderClick(index, QApplication::keyboardModifiers());
    }
}

int FileView::stickyHeaderHeight() const
{
    return d->stickyHelper->stickyHeaderHeight();
}

QModelIndex FileView::findStickyGroupIndex(int headerHeight)
{
    return d->stickyHelper->findStickyGroupIndex(headerHeight);
}

int FileView::computeStickyY(int headerHeight) const
{
    return d->stickyHelper->computeStickyY(headerHeight);
}

void FileView::paintStickyHeaderOverlay(const QModelIndex &index, int y, int headerHeight)
{
    d->stickyHelper->paintStickyHeaderOverlay(index, y, headerHeight);
}

bool FileView::isPosInStickyHeader(const QPoint &pos) const
{
    return d->stickyHelper->isPosInStickyHeader(pos);
}

void FileView::clearStickyHeaderState()
{
    d->stickyHelper->clearStickyHeaderState();
}

void FileView::scrollStickyHeaderToTop(const QModelIndex &headerIndex)
{
    d->stickyHelper->scrollStickyHeaderToTop(headerIndex);
}

void FileView::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (!truncateButtonGroupKeyAt(event->pos()).isEmpty()) {
        d->groupHeaderTimer->stop();
        return;
    }

    if (isPosInStickyHeader(event->pos())) {
        if (event->button() == Qt::LeftButton) {
            d->groupHeaderTimer->stop();
            QModelIndex stickyIdx = d->stickyHelper->currentStickyIndex();
            bool wasExpanded = stickyIdx.data(Global::kItemGroupExpandedRole).toBool();
            groupExpandOrCollapseItem(stickyIdx, QPoint(), false);
            if (wasExpanded)
                scrollStickyHeaderToTop(stickyIdx);
        }
        return;
    }
    DListView::mouseDoubleClickEvent(event);
}

void FileView::leaveEvent(QEvent *event)
{
    if (d->stickyHelper->isStickyHeaderHovered() && d->stickyHelper->currentStickyIndex().isValid()) {
        d->stickyHelper->setStickyHeaderHovered(false);
        viewport()->update(d->stickyHelper->currentStickyRect());
    }
    clearTruncateButtonHover();
    DListView::leaveEvent(event);
}
