// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "file_grid.h"
#include <wx/dc.h>
#include <wx/settings.h>
#include <wx/timer.h>
#include <zen/i18n.h>
#include <wx+/tooltip.h>
#include <wx+/rtl.h>
#include <wx+/color_tools.h>
#include <wx+/dc.h>
#include <wx+/image_tools.h>
#include <wx+/image_resources.h>
#include <wx+/std_button_layout.h>
#include "../base/file_hierarchy.h"

using namespace zen;
using namespace fff;


namespace fff
{
wxDEFINE_EVENT(EVENT_GRID_CHECK_ROWS,     CheckRowsEvent);
wxDEFINE_EVENT(EVENT_GRID_SYNC_DIRECTION, SyncDirectionEvent);
}


namespace
{
//let's NOT create wxWidgets objects statically:
wxColor getColorSyncBlue(bool faint)
{
    if (faint) return {0xed, 0xee, 0xff}; //faint blue

    return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x80, 0x94, 0xfe} /*medium blue*/ :
           wxColor{0xb9, 0xbc, 0xff} /*light blue*/;
}

wxColor getColorSyncGreen(bool faint)
{
    if (faint)
        return {0xf1, 0xff, 0xed}; //faint green

    return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x6c, 0xfb, 0x53} /*medium green*/ :
           wxColor{0xc4, 0xff, 0xb9} /*light green*/;
}

wxColor getColorConflictBackground (bool faint) { if (faint) return {0xfe, 0xfe, 0xda}; return {247, 252,  62}; } //yellow
wxColor getColorDifferentBackground(bool faint) { if (faint) return {0xff, 0xed, 0xee}; return {255, 185, 187}; } //red

wxColor getColorSymlinkBackground() { return {238, 201, 0}; } //orange

wxColor getColorInactiveBack() { return wxSystemSettings::GetAppearance().IsDark() ? 0x6c6c6c /*medium grey*/ : 0xe4e4e4 /*light grey*/; }
wxColor getColorInactiveText() { return wxSystemSettings::GetAppearance().IsDark() ? 0xffffff /*white*/       : 0x404040 /*dark grey*/; }

wxColor getColorGridLine() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW); }

const int FILE_GRID_GAP_SIZE_DIP = 2;
const int FILE_GRID_GAP_SIZE_WIDE_DIP = 6;

/* class hierarchy:       GridDataBase
                              /|\
                    ___________|____________
                   |                        |
              GridDataRim                   |
                  /|\                       |
             ______|_______                 |
            |              |                |
      GridDataLeft   GridDataRight    GridDataCenter               */


//accessibility, support high-contrast schemes => work with user-defined background color!
wxColor getGridAlternateBackgroundColor()
{
    const wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
    /*  CAVEAT: macOS uses partially-transparent colors! but probably not for this one:
                wxSYS_COLOUR_WINDOW     RGBA = #171717FF      */

    const bool isColorLight = relativeLuminance(backCol) > 0.5;

    //darken or brighten: only a faint gradient to avoid visual distraction
    auto liftChannel = [diff = isColorLight ? -15 : 15](unsigned char c) { return static_cast<unsigned char>(std::clamp(c + diff, 0, 255)); };

    return wxColor(liftChannel(backCol.Red  ()),
                   liftChannel(backCol.Green()),
                   liftChannel(backCol.Blue ()));
}


std::pair<CudAction, SelectSide> getCudAction(SyncOperation so)
{
    switch (so)
    {
        case SO_CREATE_LEFT:
        case SO_MOVE_LEFT_TO: return {CudAction::create, SelectSide::left};

        case SO_CREATE_RIGHT:
        case SO_MOVE_RIGHT_TO: return {CudAction::create, SelectSide::right};

        case SO_DELETE_LEFT:
        case SO_MOVE_LEFT_FROM: return {CudAction::delete_, SelectSide::left};

        case SO_DELETE_RIGHT:
        case SO_MOVE_RIGHT_FROM: return {CudAction::delete_, SelectSide::right};

        case SO_OVERWRITE_LEFT:
        case SO_RENAME_LEFT: return {CudAction::update, SelectSide::left};

        case SO_OVERWRITE_RIGHT:
        case SO_RENAME_RIGHT: return {CudAction::update, SelectSide::right};

        case SO_DO_NOTHING:
        case SO_EQUAL:
        case SO_UNRESOLVED_CONFLICT: return {CudAction::noChange, SelectSide::left};
    }
    assert(false);
    return {CudAction::noChange, SelectSide::left};
}


wxColor getBackGroundColorSyncAction(SyncOperation so)
{
    switch (so)
    {
        case SO_CREATE_LEFT:
        case SO_OVERWRITE_LEFT:
        case SO_DELETE_LEFT:
        case SO_MOVE_LEFT_FROM:
        case SO_MOVE_LEFT_TO:
        case SO_RENAME_LEFT:
            return getColorSyncBlue(false /*faint*/);

        case SO_CREATE_RIGHT:
        case SO_OVERWRITE_RIGHT:
        case SO_DELETE_RIGHT:
        case SO_MOVE_RIGHT_FROM:
        case SO_MOVE_RIGHT_TO:
        case SO_RENAME_RIGHT:
            return getColorSyncGreen(false /*faint*/);

        case SO_DO_NOTHING:
            return getColorInactiveBack();
        case SO_EQUAL:
            break; //usually white
        case SO_UNRESOLVED_CONFLICT:
            return getColorConflictBackground(false /*faint*/);
    }
    return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
}


wxColor getBackGroundColorCmpDifference(CompareFileResult cmpResult)
{
    switch (cmpResult)
    {
        case FILE_EQUAL:
            break; //usually white
        case FILE_LEFT_ONLY:  return getColorSyncBlue(false /*faint*/);
        case FILE_LEFT_NEWER: return getColorSyncBlue(true  /*faint*/);

        case FILE_RIGHT_ONLY:  return getColorSyncGreen(false /*faint*/);
        case FILE_RIGHT_NEWER: return getColorSyncGreen(true  /*faint*/);

        case FILE_DIFFERENT_CONTENT:
            return getColorDifferentBackground(false /*faint*/);
        case FILE_RENAMED: //similar to both "equal" and "conflict": give hint via background color
        case FILE_TIME_INVALID:
        case FILE_CONFLICT:
            return getColorConflictBackground(false /*faint*/);
    }
    return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
}


class GridEventManager;
class GridDataLeft;
class GridDataRight;

class IconUpdater : private wxEvtHandler //update file icons periodically: use SINGLE instance to coordinate left and right grids in parallel
{
public:
    IconUpdater(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer& iconBuffer) : provLeft_(provLeft), provRight_(provRight), iconBuffer_(iconBuffer)
    {
        timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { loadIconsAsynchronously(event); });
    }

    void start() { if (!timer_.IsRunning()) timer_.Start(100); } //timer interval in [ms]
    //don't check too often! give worker thread some time to fetch data

private:
    void stop() { if (timer_.IsRunning()) timer_.Stop(); }

    void loadIconsAsynchronously(wxEvent& event); //loads all (not yet) drawn icons

    GridDataLeft&  provLeft_;
    GridDataRight& provRight_;
    IconBuffer& iconBuffer_;
    wxTimer timer_;
};


struct IconManager
{
    IconManager() {}

    IconManager(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer::IconSize sz, bool showFileIcons) :
        fileIcon_        (IconBuffer::genericFileIcon (showFileIcons ? sz : IconBuffer::IconSize::small)),
        dirIcon_         (IconBuffer::genericDirIcon  (showFileIcons ? sz : IconBuffer::IconSize::small)),
        linkOverlayIcon_ (IconBuffer::linkOverlayIcon (showFileIcons ? sz : IconBuffer::IconSize::small)),
        plusOverlayIcon_ (IconBuffer::plusOverlayIcon (showFileIcons ? sz : IconBuffer::IconSize::small)),
        minusOverlayIcon_(IconBuffer::minusOverlayIcon(showFileIcons ? sz : IconBuffer::IconSize::small))
    {
        if (showFileIcons)
        {
            iconBuffer_ .emplace(sz);
            iconUpdater_.emplace(provLeft, provRight, *iconBuffer_);
        }
    }

    int getIconWxsize() const { return screenToWxsize(iconBuffer_ ? iconBuffer_->getPixSize() : IconBuffer::getPixSize(IconBuffer::IconSize::small)); }

    IconBuffer* getIconBuffer() { return get(iconBuffer_); }
    void startIconUpdater() { assert(iconUpdater_); if (iconUpdater_) iconUpdater_->start(); }

    const wxImage& getGenericFileIcon () const { return fileIcon_;         }
    const wxImage& getGenericDirIcon  () const { return dirIcon_;          }
    const wxImage& getLinkOverlayIcon () const { return linkOverlayIcon_;  }
    const wxImage& getPlusOverlayIcon () const { return plusOverlayIcon_;  }
    const wxImage& getMinusOverlayIcon() const { return minusOverlayIcon_; }

private:
    const wxImage fileIcon_;
    const wxImage dirIcon_;
    const wxImage linkOverlayIcon_;
    const wxImage plusOverlayIcon_;
    const wxImage minusOverlayIcon_;

    std::optional<IconBuffer> iconBuffer_;
    std::optional<IconUpdater> iconUpdater_; //bind ownership to GridDataRim<>!
};


//mark rows selected on overview panel
class NavigationMarker
{
public:
    NavigationMarker() {}

    void set(std::unordered_set<const FileSystemObject*>&& markedFilesAndLinks,
             std::unordered_set<const ContainerObject*>&& markedContainer)
    {
        markedFilesAndLinks_.swap(markedFilesAndLinks);
        markedContainer_    .swap(markedContainer);
    }

    bool isMarked(const FileSystemObject& fsObj) const
    {
        if (markedFilesAndLinks_.contains(&fsObj)) //mark files/links directly
            return true;

        if (auto folder = dynamic_cast<const FolderPair*>(&fsObj))
            if (markedContainer_.contains(folder)) //mark folders which *are* the given ContainerObject*
                return true;

        //also mark all items with any matching ancestors
        for (const FileSystemObject* fsObj2 = &fsObj;;)
        {
            const ContainerObject& parent = fsObj2->parent();
            if (markedContainer_.contains(&parent))
                return true;

            fsObj2 = dynamic_cast<const FolderPair*>(&parent);
            if (!fsObj2)
                return false;
        }
    }

private:
    std::unordered_set<const FileSystemObject*> markedFilesAndLinks_; //mark files/symlinks directly within a container
    std::unordered_set<const ContainerObject*> markedContainer_;      //mark full container including all child-objects
    //DO NOT DEREFERENCE!!!! NOT GUARANTEED TO BE VALID!!!
};


struct SharedComponents //...between left, center, and right grids
{
    SharedRef<FileView> gridDataView = makeSharedRef<FileView>();
    SharedRef<IconManager> iconMgr = makeSharedRef<IconManager>();
    NavigationMarker navMarker;
    std::unique_ptr<GridEventManager> evtMgr;
    GridViewType gridViewType = GridViewType::action;
    std::unordered_map<std::wstring, wxSize, StringHash, StringEqual> compExtentsBuf_; //buffer expensive wxDC::GetTextExtent() calls!
    //StringHash, StringEqual => heterogenous lookup by std::wstring_view
};

//########################################################################################################

class GridDataBase : public GridData
{
public:
    GridDataBase(Grid& grid, const SharedRef<SharedComponents>& sharedComp) :
        grid_(grid), sharedComp_(sharedComp) {}

    void setData(FolderComparison& folderCmp)
    {
        sharedComp_.ref().gridDataView = makeSharedRef<FileView>(); //clear old data view first! avoid memory peaks!
        sharedComp_.ref().gridDataView = makeSharedRef<FileView>(folderCmp);
        sharedComp_.ref().compExtentsBuf_.clear(); //doesn't become stale! but still: re-calculate and save some memory...
    }

    GridEventManager* getEventManager() { return sharedComp_.ref().evtMgr.get(); }

    /**/  FileView& getDataView()       { return sharedComp_.ref().gridDataView.ref(); }
    const FileView& getDataView() const { return sharedComp_.ref().gridDataView.ref(); }

    void setIconManager(const SharedRef<IconManager>& iconMgr) { sharedComp_.ref().iconMgr = iconMgr; }

    IconManager& getIconManager() { return sharedComp_.ref().iconMgr.ref(); }

    GridViewType getViewType() const { return sharedComp_.ref().gridViewType; }
    void setViewType(GridViewType vt) { sharedComp_.ref().gridViewType = vt; }

    bool isNavMarked(const FileSystemObject& fsObj) const { return sharedComp_.ref().navMarker.isMarked(fsObj); }

    void setNavigationMarker(std::unordered_set<const FileSystemObject*>&& markedFilesAndLinks,
                             std::unordered_set<const ContainerObject*>&& markedContainer)
    {
        sharedComp_.ref().navMarker.set(std::move(markedFilesAndLinks), std::move(markedContainer));
    }

    Grid& refGrid() { return grid_; }
    const Grid& refGrid() const { return grid_; }

    const FileSystemObject* getFsObject(size_t row) const { return getDataView().getFsObject(row); }

    const wxSize& getTextExtentBuffered(const wxReadOnlyDC& dc, const std::wstring_view& text)
    {
        auto& compExtentsBuf = sharedComp_.ref().compExtentsBuf_;
        //- only used for parent path names and file names on view => should not grow "too big"
        //- cleaned up during GridDataBase::setData()
        assert(!contains(text, L'\n'));

        auto it = compExtentsBuf.find(text);
        if (it == compExtentsBuf.end())
            it = compExtentsBuf.emplace(text, dc.GetTextExtent(copyStringTo<wxString>(text))).first;
        //GetTextExtent() returns (0, 0) for empty string!
        return it->second;
    }

    //- trim while leaving path components intact
    //- *always* returns at least one component, even if > maxWidth
    size_t getPathTrimmedSize(const wxReadOnlyDC& dc, const std::wstring_view& itemPath, int maxWidth)
    {
        if (itemPath.size() <= 1)
            return itemPath.size();

        std::vector<std::wstring_view> subComp;

        //split path by components, but skip slash at beginning or end
        for (auto it = itemPath.begin() + 1; it != itemPath.end() - 1; ++it)
            if (*it == L'/' ||
                *it == L'\\')
                subComp.push_back(makeStringView(itemPath.begin(), it));

        subComp.push_back(itemPath);

        if (maxWidth <= 0)
            return subComp[0].size();

        size_t low  = 0;
        size_t high = subComp.size();

        for (;;)
        {
            if (high - low == 1)
                return subComp[low].size();

            const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1"

            if (getTextExtentBuffered(dc, subComp[middle]).GetWidth() <= maxWidth)
                low = middle;
            else
                high = middle;
        }
    }

    //improve readability (while lacking cell borders)
    const wxColor& getDefaultBackgroundColorAlternating(bool wantStandardColor)
    {
        return wantStandardColor ? gridBackgroundColor_ : gridBackgroundColorAlt_;
    }

private:
    size_t getRowCount() const override { return getDataView().rowsOnView(); }

    const wxColor gridBackgroundColor_    = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
    const wxColor gridBackgroundColorAlt_ = getGridAlternateBackgroundColor();

    Grid& grid_;
    SharedRef<SharedComponents> sharedComp_;
};

//########################################################################################################

template <SelectSide side>
class GridDataRim : public GridDataBase
{
public:
    GridDataRim(Grid& grid, const SharedRef<SharedComponents>& sharedComp) : GridDataBase(grid, sharedComp) {}

    void setItemPathForm(ItemPathFormat fmt) { itemPathFormat_ = fmt; groupItemNamesWidthBuf_.clear(); }

    void getUnbufferedIconsForPreload(std::vector<std::pair<ptrdiff_t, AbstractPath>>& newLoad) //return (priority, filepath) list
    {
        if (IconBuffer* iconBuf = getIconManager().getIconBuffer())
        {
            const auto& [rowFirst, rowLast] = refGrid().getVisibleRows(refGrid().getMainWin().GetClientSize());
            const ptrdiff_t visibleRowCount = rowLast - rowFirst;

            //preload icons not yet on screen:
            const int preloadSize = 2 * std::max<ptrdiff_t>(20, visibleRowCount); //:= sum of lines above and below of visible range to preload
            //=> use full visible height to handle "next page" command and a minimum of 20 for excessive mouse wheel scrolls

            for (ptrdiff_t i = 0; i < preloadSize; ++i)
            {
                const ptrdiff_t currentRow = rowFirst - (preloadSize + 1) / 2 + getAlternatingPos(i, visibleRowCount + preloadSize); //for odd preloadSize start one row earlier

                if (const FileSystemObject* fsObj = getFsObject(currentRow))
                    if (getIconInfo(*fsObj).type == IconType::standard)
                        if (!iconBuf->readyForRetrieval(fsObj->template getAbstractPath<side>()))
                            newLoad.emplace_back(i, fsObj->template getAbstractPath<side>()); //insert least-important items on outer rim first
            }
        }
        else assert(false);
    }

    void updateNewAndGetUnbufferedIcons(std::vector<AbstractPath>& newLoad) //loads all not yet drawn icons
    {
        if (IconBuffer* iconBuf = getIconManager().getIconBuffer())
        {
            const auto& [rowFirst, rowLast] = refGrid().getVisibleRows(refGrid().getMainWin().GetClientSize());
            const ptrdiff_t visibleRowCount = rowLast - rowFirst;

            for (ptrdiff_t i = 0; i < visibleRowCount; ++i)
            {
                //alternate when adding rows: first, last, first + 1, last - 1 ...
                const ptrdiff_t currentRow = rowFirst + getAlternatingPos(i, visibleRowCount);

                if (isFailedLoad(currentRow)) //find failed attempts to load icon
                    if (const FileSystemObject* fsObj = getFsObject(currentRow))
                        if (getIconInfo(*fsObj).type == IconType::standard)
                        {
                            //test if they are already loaded in buffer:
                            if (iconBuf->readyForRetrieval(fsObj->template getAbstractPath<side>()))
                            {
                                //do a *full* refresh for *every* failed load to update partial DC updates while scrolling
                                refGrid().refreshCell(currentRow, static_cast<ColumnType>(ColumnTypeRim::path));
                                setFailedLoad(currentRow, false);
                            }
                            else //not yet in buffer: mark for async. loading
                                newLoad.push_back(fsObj->template getAbstractPath<side>());
                        }
            }
        }
        else assert(false);
    }

private:
    bool isFailedLoad(size_t row) const { return row < failedLoads_.size() ? failedLoads_[row] != 0 : false; }

    void setFailedLoad(size_t row, bool failed = true)
    {
        if (failedLoads_.size() != refGrid().getRowCount())
            failedLoads_.resize(refGrid().getRowCount());

        if (row < failedLoads_.size())
            failedLoads_[row] = failed;
    }

    //icon buffer will load reversely, i.e. if we want to go from inside out, we need to start from outside in
    static size_t getAlternatingPos(size_t pos, size_t total)
    {
        assert(pos < total);
        return pos % 2 == 0 ? pos / 2 : total - 1 - pos / 2;
    }

private:
    enum class DisplayType
    {
        inactive,
        normal,
        symlink,
    };
    static DisplayType getObjectDisplayType(const FileSystemObject& fsObj)
    {
        if (!fsObj.isActive())
            return DisplayType::inactive;

        DisplayType output = DisplayType::normal;

        visitFSObject(fsObj, [](const FolderPair& folder) {},
        [](const FilePair& file) {},
        [&](const SymlinkPair& symlink) { output = DisplayType::symlink; });

        return output;
    }


    std::wstring getValue(size_t row, ColumnType colType) const override
    {
        if (const FileSystemObject* fsObj = getFsObject(row))
            if (!fsObj->isEmpty<side>())
            {
                if (static_cast<ColumnTypeRim>(colType) == ColumnTypeRim::path)
                    switch (itemPathFormat_)
                    {
                        case ItemPathFormat::name:
                            return utfTo<std::wstring>(fsObj->getItemName<side>());
                        case ItemPathFormat::relative:
                            return utfTo<std::wstring>(fsObj->getRelativePath<side>());
                        case ItemPathFormat::full:
                            return AFS::getDisplayPath(fsObj->getAbstractPath<side>());
                    }

                std::wstring value; //dynamically allocates 16 byte memory! but why? shouldn't SSO make this superfluous?! or is it only in debug?
                switch (static_cast<ColumnTypeRim>(colType))
                {
                    case ColumnTypeRim::path:
                        assert(false);
                        break;

                    case ColumnTypeRim::size:
                        visitFSObject(*fsObj, [&](const FolderPair& folder) { /*value = L'<' + _("Folder") + L'>'; -> redundant!? */ },
                        [&](const FilePair& file) { value = formatNumber(file.getFileSize<side>()); },
                        //[&](const FilePair& file) { value = numberTo<std::wstring>(file.getFilePrint<side>()); }, // -> test file id
                        [&](const SymlinkPair& symlink) { value = L'<' + _("Symlink") + L'>'; });
                        break;

                    case ColumnTypeRim::date:
                        visitFSObject(*fsObj, [](const FolderPair& folder) {},
                        [&](const FilePair&       file) { value = formatUtcToLocalTime(file   .getLastWriteTime<side>()); },
                        [&](const SymlinkPair& symlink) { value = formatUtcToLocalTime(symlink.getLastWriteTime<side>()); });
                        break;

                    case ColumnTypeRim::extension:
                        visitFSObject(*fsObj, [](const FolderPair& folder) {},
                        [&](const FilePair&       file) { value = utfTo<std::wstring>(getFileExtension(file   .getItemName<side>())); },
                        [&](const SymlinkPair& symlink) { value = utfTo<std::wstring>(getFileExtension(symlink.getItemName<side>())); });
                        break;
                }
                return value;
            }
        return {};
    }

    void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override
    {
        const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);

        if (!enabled || !selected)
        {
            const wxColor backCol = [&]
            {
                if (pdi.fsObj && !pdi.fsObj->isEmpty<side>()) //do we need color indication for *inactive* empty rows? probably not...
                    switch (getObjectDisplayType(*pdi.fsObj))
                    {
                        case DisplayType::normal: break;
                        case DisplayType::symlink:  return getColorSymlinkBackground();
                        case DisplayType::inactive: return getColorInactiveBack();
                    }
                return getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0);
            }();
            if (backCol != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW) /*already the default!*/)
                clearArea(dc, rect, backCol);
        }
        else
            GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover);

        //----------------------------------------------------------------------------------
        const wxRect rectLine(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1));
        clearArea(dc, rectLine, row == pdi.groupLastRow - 1 || //last group item
                  (pdi.fsObj == pdi.folderGroupObj &&  //folder item => distinctive separation color against subsequent file items
                   itemPathFormat_ != ItemPathFormat::name) ?
                  getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0));
    }


    int getGroupItemNamesWidth(const wxReadOnlyDC& dc, const FileView::PathDrawInfo& pdi)
    {
        //FileView::updateView() called? => invalidates group item render buffer
        if (pdi.viewUpdateId != viewUpdateIdLast_)
        {
            viewUpdateIdLast_ = pdi.viewUpdateId;
            groupItemNamesWidthBuf_.clear();
        }

        auto& widthBuf = groupItemNamesWidthBuf_;
        if (pdi.groupIdx >= widthBuf.size())
            widthBuf.resize(pdi.groupIdx + 1, -1 /*sentinel value*/);

        int& itemNamesWidth = widthBuf[pdi.groupIdx];
        if (itemNamesWidth < 0)
        {
            itemNamesWidth = 0;
            //const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x;

            std::vector<int> itemWidths;
            for (size_t row2 = pdi.groupFirstRow; row2 < pdi.groupLastRow; ++row2)
                if (const FileSystemObject* fsObj = getDataView().getFsObject(row2))
                    if (itemPathFormat_ == ItemPathFormat::name || fsObj != pdi.folderGroupObj)
#if 0 //render same layout even when items don't exist
                        if (fsObj->isEmpty<side>())
                            itemNamesWidth = ellipsisWidth;
                        else
#endif
                            itemWidths.push_back(getTextExtentBuffered(dc, utfTo<std::wstring>(fsObj->getItemName<side>())).x);

            if (!itemWidths.empty())
            {
                //ignore (small number of) excessive file name widths:
                auto itPercentile = itemWidths.begin() + itemWidths.size() * 8 / 10; //80th percentile
                std::nth_element(itemWidths.begin(), itPercentile, itemWidths.end()); //complexity: O(n)
                itemNamesWidth = std::max(itemNamesWidth, *itPercentile);
            }
            assert(itemNamesWidth >= 0);

            //Note: A better/faster solution would be to get 80th percentile of all std::wstring::size(), then do a *single* getTextExtentBuffered()!
            //      However, we need all the getTextExtentBuffered(itemName) later anyway, so above is fine.
        }
        return itemNamesWidth;
    }


    struct GroupRowLayout
    {
        std::wstring groupParentPart; //... if distributed over multiple rows, otherwise full group parent folder
        std::wstring groupName; //only filled for first row of a group
        std::wstring itemName;
        int groupParentWidth;
        int groupNameWidth;
    };
    GroupRowLayout getGroupRowLayout(const wxReadOnlyDC& dc, size_t row, const FileView::PathDrawInfo& pdi, int maxWidth)
    {
        assert(pdi.fsObj);

        const bool drawFileIcons = getIconManager().getIconBuffer();
        const int iconSize       = getIconManager().getIconWxsize();

        //--------------------------------------------------------------------
        const int ellipsisWidth       = getTextExtentBuffered(dc, ELLIPSIS).x;
        const int arrowRightDownWidth = getTextExtentBuffered(dc, rightArrowDown_).x;
        const int groupItemNamesWidth = getGroupItemNamesWidth(dc, pdi);
        //--------------------------------------------------------------------

        //exception for readability: top row is always group start!
        const size_t groupFirstRow = std::max<size_t>(pdi.groupFirstRow, refGrid().getRowAtWinPos(0));

        const size_t groupRowCount = pdi.groupLastRow - groupFirstRow;

        std::wstring itemName;
        if (itemPathFormat_ == ItemPathFormat::name || //hack: show folder name in item colum since groupName/groupParentFolder are unused!
            pdi.fsObj != pdi.folderGroupObj)           //=> consider groupItemNamesWidth!
            itemName = utfTo<std::wstring>(pdi.fsObj->getItemName<side>());
        //=> doesn't matter if isEmpty()! => only indicates if component should be drawn

        std::wstring groupName;
        std::wstring groupParentFolder;
        switch (itemPathFormat_)
        {
            case ItemPathFormat::name:
                break;

            case ItemPathFormat::relative:
                if (pdi.folderGroupObj)
                {
                    groupName         = utfTo<std::wstring>(pdi.folderGroupObj         ->template getItemName    <side>());
                    groupParentFolder = utfTo<std::wstring>(pdi.folderGroupObj->parent().template getRelativePath<side>());
                }
                break;

            case ItemPathFormat::full:
                if (pdi.folderGroupObj)
                {
                    groupName         = utfTo<std::wstring>(pdi.folderGroupObj         ->template getItemName    <side>());
                    groupParentFolder = AFS::getDisplayPath(pdi.folderGroupObj->parent().template getAbstractPath<side>());
                }
                else //=> BaseFolderPair
                    groupParentFolder = AFS::getDisplayPath(pdi.fsObj->base().getAbstractPath<side>());
                break;
        }

        if (!groupParentFolder.empty())
        {
            const wchar_t pathSep = [&]
            {
                for (auto it = groupParentFolder.end(); it != groupParentFolder.begin();) //reverse iteration: 1. check 2. decrement 3. evaluate
                {
                    --it; //

                    if (*it ==  L'/' ||
                        *it ==  L'\\')
                        return *it;
                }
                return static_cast<wchar_t>(FILE_NAME_SEPARATOR);
            }();
            if (!endsWith(groupParentFolder, pathSep)) //visual hint that this is a parent folder only
                groupParentFolder += pathSep;          //
        }

        /*  group details: single row
            ________________________  ___________________________________  _____________________________________________________
            | (gap | group parent) |  | (gap | icon | gap | group name) |  | (2x gap | vline) | (gap | icon) | gap | item name |
            ------------------------  -----------------------------------  -----------------------------------------------------

            group details: stacked
            __________________________________                  ___________________________________  ___________________________________________________
            | gap | group parent, part 1 | ⤵️ |  <right-aligned> | (gap | icon | gap | group name) |  |                | (gap | icon) | gap | item name |
            |-------------------------------------------------------------------------------------|  | 2x gap | vline |--------------------------------|
            | gap | group parent, part n                                                          |  |                | (gap | icon) | gap | item name |
            ---------------------------------------------------------------------------------------  ---------------------------------------------------

                -> group name on first row
                -> parent name distributed over multiple rows, if needed                                                        */

        int groupParentWidth = groupParentFolder.empty() ? 0 : (gapSize_ + getTextExtentBuffered(dc, groupParentFolder).x);

        int       groupNameWidth    = groupName.empty() ? 0 : (gapSize_ + iconSize + gapSize_ + getTextExtentBuffered(dc, groupName).x);
        const int groupNameMinWidth = groupName.empty() ? 0 : (gapSize_ + iconSize + gapSize_ + ellipsisWidth);

        const int groupSepWidth = (groupParentFolder.empty() && groupName.empty()) ? 0 : (2 * gapSize_ + dipToWxsize(1));

        int       groupItemsWidth    = groupSepWidth + (drawFileIcons ? gapSize_ + iconSize : 0) + gapSize_ + groupItemNamesWidth;
        const int groupItemsMinWidth = groupSepWidth + (drawFileIcons ? gapSize_ + iconSize : 0) + gapSize_ + ellipsisWidth;

        std::wstring groupParentPart;

        //not enough space? => trim or render on multiple rows
        if (int excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth;
            excessWidth > 0)
        {
            //1. shrink group parent
            if (!groupParentFolder.empty())
            {
                const int groupParentMinWidth = !groupName.empty() && groupRowCount > 1 ? //group parent details (possibly) on multiple rows
                                                0 : gapSize_ + ellipsisWidth;

                groupParentWidth = std::max(groupParentWidth - excessWidth, groupParentMinWidth);
                excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth;
            }

            if (excessWidth > 0)
            {
                //2. shrink item rendering
                groupItemsWidth = std::max(groupItemsWidth - excessWidth, groupItemsMinWidth);
                excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth;

                if (excessWidth > 0)
                    //3. shrink group name
                    if (!groupName.empty())
                        groupNameWidth = std::max(groupNameWidth - excessWidth, groupNameMinWidth);
            }

            //group parent details on multiple lines
            if (!groupParentFolder.empty())
            {
                //let's not waste empty row space for medium + large icon sizes: print multiple lines per row!
                const int linesPerRow = std::max(refGrid().getRowHeight() / charHeight_, 1);

                size_t compPos = 0;
                for (size_t i = groupFirstRow; i <= row; ++i)
                    for (int l = 0; l < linesPerRow; ++l)
                    {
                        const size_t compLen = i == pdi.groupLastRow - 1 && l == linesPerRow - 1 ? //not enough rows to show remaining parent folder components?
                                               groupParentFolder.size() - compPos : //=> append the rest: will be truncated with ellipsis
                                               getPathTrimmedSize(dc, makeStringView(groupParentFolder.begin() + compPos, groupParentFolder.end()),
                                                                  groupParentWidth + (i == groupFirstRow ? 0 : groupNameWidth) - gapSize_ - arrowRightDownWidth);

                        if (i == groupFirstRow && !groupName.empty() && groupRowCount > 1 &&
                            getTextExtentBuffered(dc, makeStringView(groupParentFolder.begin() + compPos, compLen)).x > groupParentWidth - gapSize_ - arrowRightDownWidth)
                        {
                            if (i == row && l != 0)
                                groupParentPart.insert(groupParentPart.begin(), linesPerRow - l, L'\n'); //effectively: "align bottom" for first row
                            break; //exception: never truncate parent component on first row, but continue on second row instead
                        }

                        if (i == row)
                            groupParentPart += compPos + compLen == groupParentFolder.size() ?
                                               groupParentFolder.substr(compPos) :
                                               groupParentFolder.substr(compPos, compLen) + rightArrowDown_ + L'\n';
                        compPos += compLen;

                        if (compPos == groupParentFolder.size())
                            goto break2;
                    }
            break2:
                if (endsWith(groupParentPart, L'\n'))
                    groupParentPart.pop_back();
            }
        }
        else
        {
            if (row == groupFirstRow)
                groupParentPart = groupParentFolder;
        }

        //path components should follow the app layout direction and are NOT a single piece of text!
        //caveat: - add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer"
        //        - add *after* getPathTrimmedSize(), otherwise LTR-mark can be confused for path component, e.g. "<LTR>/home" would be two components!
        assert(!contains(groupParentPart, slashBidi_) && !contains(groupParentPart, bslashBidi_));
        replace(groupParentPart, L'/',   slashBidi_);
        replace(groupParentPart, L'\\', bslashBidi_);

        return
        {
            std::move(groupParentPart),
            row == groupFirstRow ? std::move(groupName) : std::wstring{},
            std::move(itemName),
            row == groupFirstRow ? groupParentWidth : groupParentWidth + groupNameWidth,
            row == groupFirstRow ? groupNameWidth : 0,
        };
    }


    void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override
    {
        //-----------------------------------------------
        //don't forget: harmonize with getBestSize()!!!
        //-----------------------------------------------

        if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);
            pdi.fsObj)
        {
            //accessibility: always set both foreground AND background colors!
            wxDCTextColourChanger textColor(dc);
            if (enabled && selected) //=> coordinate with renderRowBackgound()
                textColor.Set(*wxBLACK);
            else if (!pdi.fsObj->isEmpty<side>())
                switch (getObjectDisplayType(*pdi.fsObj))
                {
                    case DisplayType::normal: break;
                    case DisplayType::symlink:  textColor.Set(*wxBLACK); break;
                    case DisplayType::inactive: textColor.Set(getColorInactiveText()); break;
                }

            wxRect rectTmp = rect;

            switch (static_cast<ColumnTypeRim>(colType))
            {
                case ColumnTypeRim::path:
                {
                    auto drawCudHighlight = [&](wxRect rectCud, SyncOperation syncOp)
                    {
                        if (getViewType() == GridViewType::action)
                            if (!enabled || !selected)
                                if (const auto& [cudAction, cudSide] = getCudAction(syncOp);
                                    cudAction != CudAction::noChange && side == cudSide)
                                {
                                    rectCud.width = gapSize_ + screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small));
                                    //fixed-size looks fine for all icon sizes! use same width even if file icons are disabled!
                                    clearArea(dc, rectCud, getBackGroundColorSyncAction(syncOp));

                                    rectCud.x += rectCud.width;
                                    rectCud.width = gapSize_ + dipToWxsize(2);

#if 0 //wxDC::GetPixel() is broken in GTK3! https://github.com/wxWidgets/wxWidgets/issues/14067
                                    wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
                                    dc.GetPixel(rectCud.GetTopRight(), &backCol);
#else
                                    const wxColor backCol = getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0);
#endif
                                    dc.GradientFillLinear(rectCud, getBackGroundColorSyncAction(syncOp), backCol, wxEAST);
                                }
                    };

                    bool navMarkerDrawn = false;
                    auto tryDrawNavMarker = [&](wxRect rectNav)
                    {
                        if (!navMarkerDrawn &&
                            rectNav.x == rect.x && //draw marker *only* if current render group (group parent, group name, item name) is at beginning of a row!
                            isNavMarked(*pdi.fsObj) &&
                            (!enabled || !selected))
                        {
                            rectNav.width = std::min(rectNav.width, dipToWxsize(10));

                            if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line!
                                rectNav.height -= dipToWxsize(1);

                            dc.GradientFillLinear(rectNav, getColorSelectionGradientFrom(), getColorSelectionGradientTo(), wxEAST);
                            navMarkerDrawn = true;
                        }
                    };

                    auto drawIcon = [&](wxImage icon, wxRect rectIcon, bool drawActive)
                    {
                        if (!drawActive)
                            icon = icon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally!

                        if (!enabled)
                            icon = icon.ConvertToDisabled();

                        rectIcon.x += gapSize_;
                        rectIcon.width = getIconManager().getIconWxsize(); //center smaller-than-default icons
                        drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER);
                    };

                    auto drawFileIcon = [this, &drawIcon](const wxImage& fileIcon, bool drawAsLink, const wxRect& rectIcon, const FileSystemObject& fsObj)
                    {
                        if (fileIcon.IsOk())
                            drawIcon(fileIcon, rectIcon, fsObj.isActive());

                        if (drawAsLink)
                            drawIcon(getIconManager().getLinkOverlayIcon(), rectIcon, fsObj.isActive());

                        if (getViewType() == GridViewType::action)
                            if (const auto& [cudAction, cudSide] = getCudAction(fsObj.getSyncOperation());
                                side == cudSide)
                                switch (cudAction)
                                {
                                    case CudAction::create:
                                        assert(!fileIcon.IsOk() && !drawAsLink);
                                        if (const bool isFolder = dynamic_cast<const FolderPair*>(&fsObj) != nullptr)
                                            drawIcon(getIconManager().getGenericDirIcon().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3). //treat all channels equally!
                                                     ConvertToDisabled(), rectIcon, true /*drawActive: [!]*/); //visual hint to distinguish file/folder creation

                                        //too much clutter? => drawIcon(getIconManager().getPlusOverlayIcon(), rectIcon,
                                        //                              true /*drawActive: [!] e.g. disabled folder, exists left only, where child item is copied*/);
                                        break;
                                    case CudAction::delete_:
                                        drawIcon(getIconManager().getMinusOverlayIcon(), rectIcon, true /*drawActive: [!]*/);
                                        break;
                                    case CudAction::noChange:
                                    case CudAction::update:
                                        break;
                                };
                    };
                    //-------------------------------------------------------------------------

                    const auto& [groupParentPart,
                                 groupName,
                                 itemName,
                                 groupParentWidth,
                                 groupNameWidth] = getGroupRowLayout(dc, row, pdi, rectTmp.width);

                    wxRect rectGroup, rectGroupParent, rectGroupName;
                    rectGroup = rectGroupParent = rectGroupName = rectTmp;

                    rectGroup      .width = groupParentWidth + groupNameWidth;
                    rectGroupParent.width = groupParentWidth;
                    rectGroupName  .width = groupNameWidth;
                    rectGroupName.x += groupParentWidth;

                    rectTmp.x     += rectGroup.width;
                    rectTmp.width -= rectGroup.width;

                    wxRect rectGroupItems = rectTmp;

                    if (itemName.empty()) //expand group name to include unused item area (e.g. bigger selection border)
                    {
                        rectGroupName.width += rectGroupItems.width;
                        rectGroupItems.width = 0;
                    }

                    //-------------------------------------------------------------------------
                    {
                        //clear background below parent path => harmonize with renderRowBackgound()
                        wxDCTextColourChanger textColorGroup(dc);
                        if (rectGroup.width > 0 &&
                            (!enabled || !selected))
                        {
                            wxRect rectGroupBack = rectGroup;
                            rectGroupBack.width += 2 * gapSize_; //include gap before vline

                            if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line!
                                rectGroupBack.height -= dipToWxsize(1);

                            clearArea(dc, rectGroupBack, getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0));
                            //clearArea() is surprisingly expensive => call just once!
                            textColorGroup.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
                            //accessibility: always set *both* foreground AND background colors!
                        }

                        if (!groupParentPart.empty() &&
                            (!pdi.folderGroupObj || !pdi.folderGroupObj->isEmpty<side>())) //don't show for missing folders
                        {
                            tryDrawNavMarker(rectGroupParent);

                            wxRect rectGroupParentText = rectGroupParent;
                            rectGroupParentText.x     += gapSize_;
                            rectGroupParentText.width -= gapSize_;

                            //let's not waste empty row space for medium + large icon sizes: print multiple lines per row!
                            split(groupParentPart, L'\n', [&, linesPerRow = std::max(refGrid().getRowHeight() / charHeight_, 1),
                                                           lineNo = 0](const std::wstring_view line) mutable
                            {
                                drawCellText(dc, {
                                    rectGroupParentText.x, //distribute lines evenly across multiple rows:
                                    rectGroupParentText.y + (rectGroupParentText.height * (1 + lineNo++ * 2) - linesPerRow * charHeight_) / (linesPerRow * 2),
                                    rectGroupParentText.width, charHeight_
                                }, line, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, line));
                            });
#if 0
                            drawCellText(dc, rectGroupParentText, groupParentPart, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupParentPart));
#endif
                        }

                        if (!groupName.empty())
                        {
                            wxRect rectGroupNameBack = rectGroupName;

                            if (!itemName.empty())
                                rectGroupNameBack.width += 2 * gapSize_; //include gap left of item vline
                            rectGroupNameBack.height -= dipToWxsize(1); //harmonize with item separation lines

                            wxDCTextColourChanger textColorGroupName(dc);
                            //folder background: coordinate with renderRowBackgound()
                            if (!enabled || !selected)
                                if (!pdi.folderGroupObj->isEmpty<side>() &&
                                    !pdi.folderGroupObj->isActive())
                                {
                                    clearArea(dc, rectGroupNameBack, getColorInactiveBack());
                                    textColorGroupName.Set(getColorInactiveText());
                                }
                            drawCudHighlight(rectGroupNameBack, pdi.folderGroupObj->getSyncOperation());
                            tryDrawNavMarker(rectGroupName);

                            wxImage folderIcon;
                            bool drawAsLink = false;
                            if (!pdi.folderGroupObj->isEmpty<side>())
                            {
                                folderIcon = getIconManager().getGenericDirIcon();
                                drawAsLink = pdi.folderGroupObj->isFollowedSymlink<side>();
                            }
                            drawFileIcon(folderIcon, drawAsLink, rectGroupName, *pdi.folderGroupObj);
                            rectGroupName.x     += gapSize_ + getIconManager().getIconWxsize() + gapSize_;
                            rectGroupName.width -= gapSize_ + getIconManager().getIconWxsize() + gapSize_;

                            //mouse highlight: group name
                            if (static_cast<HoverAreaGroup>(rowHover) == HoverAreaGroup::groupName ||
                                (static_cast<HoverAreaGroup>(rowHover) == HoverAreaGroup::item && pdi.fsObj == pdi.folderGroupObj /*exception: extend highlight*/))
                                drawRectangleBorder(dc, rectGroupNameBack, mouseHighlightColor_, dipToWxsize(1));

                            if (!pdi.folderGroupObj->isEmpty<side>())
                                drawCellText(dc, rectGroupName, groupName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupName));
                        }
                    }

                    //-------------------------------------------------------------------------
                    if (!itemName.empty())
                    {
                        //draw group/items separation line
                        if (rectGroup.width > 0)
                        {
                            rectGroupItems.x     += 2 * gapSize_;
                            rectGroupItems.width -= 2 * gapSize_;

                            wxRect rectLine = rectGroupItems;
                            rectLine.width = dipToWxsize(1);
                            clearArea(dc, rectLine, getColorGridLine());

                            rectGroupItems.x     += dipToWxsize(1);
                            rectGroupItems.width -= dipToWxsize(1);
                        }
                        //-------------------------------------------------------------------------

                        wxRect rectItemsBack = rectGroupItems;
                        rectItemsBack.height -= dipToWxsize(1); //preserve item separation lines!

                        drawCudHighlight(rectItemsBack, pdi.fsObj->getSyncOperation());
                        tryDrawNavMarker(rectGroupItems);

                        if (IconBuffer* iconBuf = getIconManager().getIconBuffer()) //=> draw file icons
                        {
                            /* whenever there's something new to render on screen, start up watching for failed icon drawing:
                               => ideally it would suffice to start watching only when scrolling grid or showing new grid content, but this solution is more robust
                               and the icon updater will stop automatically when finished anyway
                               Note: it's not sufficient to start up on failed icon loads only, since we support prefetching of not yet visible rows!!! */
                            getIconManager().startIconUpdater();

                            wxImage fileIcon;
                            const IconInfo ii = getIconInfo(*pdi.fsObj);
                            switch (ii.type)
                            {
                                case IconType::folder:
                                    fileIcon = getIconManager().getGenericDirIcon();
                                    break;

                                case IconType::standard:
                                    if (std::optional<wxImage> tmpIco = iconBuf->retrieveFileIcon(pdi.fsObj->template getAbstractPath<side>()))
                                        fileIcon = *tmpIco;
                                    else
                                    {
                                        setFailedLoad(row); //save status of failed icon load -> used for async. icon loading
                                        //falsify only! avoid writing incorrect success status when only partially updating the DC, e.g. during scrolling,
                                        //see repaint behavior of ::ScrollWindow() function!
                                        fileIcon = iconBuf->getIconByExtension(pdi.fsObj->template getItemName<side>()); //better than nothing
                                    }
                                    break;

                                case IconType::none:
                                    break;
                            }
                            drawFileIcon(fileIcon, ii.drawAsLink, rectGroupItems, *pdi.fsObj);
                            rectGroupItems.x     += gapSize_ + getIconManager().getIconWxsize();
                            rectGroupItems.width -= gapSize_ + getIconManager().getIconWxsize();
                        }

                        rectGroupItems.x     += gapSize_;
                        rectGroupItems.width -= gapSize_;

                        //mouse highlight: item name
                        if (static_cast<HoverAreaGroup>(rowHover) == HoverAreaGroup::item)
                            drawRectangleBorder(dc, rectItemsBack, mouseHighlightColor_, dipToWxsize(1));

                        if (!pdi.fsObj->isEmpty<side>())
                            drawCellText(dc, rectGroupItems, itemName, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, itemName));
                    }

                    //if not done yet:
                    tryDrawNavMarker(rect);
                }
                break;

                case ColumnTypeRim::size:
                case ColumnTypeRim::date:
                case ColumnTypeRim::extension:
                {
                    if (refGrid().GetLayoutDirection() == wxLayout_RightToLeft || //remain left-justified for RTL languages
                        static_cast<ColumnTypeRim>(colType) == ColumnTypeRim::extension)
                    {
                        rectTmp.x     += gapSize_;
                        rectTmp.width -= gapSize_;
                        drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
                    }
                    else
                    {
                        rectTmp.width -= gapSize_;
                        drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
                        //macOS: wxALIGN_RIGHT also helps mitigate NSDateFormatter not zero-padding dates!
                    }
                }
                break;
            }
        }
    }


    HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override
    {
        if (static_cast<ColumnTypeRim>(colType) == ColumnTypeRim::path)
            if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);
                pdi.fsObj)
            {
                const auto& [groupParentPart,
                             groupName,
                             itemName,
                             groupParentWidth,
                             groupNameWidth] = getGroupRowLayout(dc, row, pdi, cellWidth);

                if (!groupName.empty() && pdi.fsObj != pdi.folderGroupObj)
                {
                    const int groupNameCellBeginX = groupParentWidth;

                    if (groupNameCellBeginX <= cellRelativePosX && cellRelativePosX < groupNameCellBeginX + groupNameWidth + 2 * gapSize_ /*include gap before vline*/)
                        return static_cast<HoverArea>(HoverAreaGroup::groupName);
                }
            }
        return static_cast<HoverArea>(HoverAreaGroup::item);
    }


    int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override
    {
        if (static_cast<ColumnTypeRim>(colType) == ColumnTypeRim::path)
        {
            int bestSize = 0;

            if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);
                pdi.fsObj)
            {
                const int insanelyHugeWidth = 1000'000'000; //(hopefully) still small enough to avoid integer overflows
                /* ________________________  ___________________________________  _____________________________________________________
                   | (gap | group parent) |  | (gap | icon | gap | group name) |  | (2x gap | vline) | (gap | icon) | gap | item name |
                   ------------------------  -----------------------------------  ----------------------------------------------------- */
                const auto& [groupParentPart,
                             groupName,
                             itemName,
                             groupParentWidth,
                             groupNameWidth] = getGroupRowLayout(dc, row, pdi, insanelyHugeWidth);

                const int groupSepWidth = groupParentWidth + groupNameWidth <= 0 ? 0 : (2 * gapSize_ + dipToWxsize(1));
                const int fileIconWidth = getIconManager().getIconBuffer() ? gapSize_ + getIconManager().getIconWxsize() : 0;
                const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x;
                const int itemWidth = itemName.empty() ? 0 :
                                      (groupSepWidth + fileIconWidth + gapSize_ +
                                       (pdi.fsObj->isEmpty<side>() ? ellipsisWidth : getTextExtentBuffered(dc, itemName).x));

                bestSize += groupParentWidth + groupNameWidth + itemWidth + gapSize_ /*[!]*/;
            }
            return bestSize;
        }
        else
        {
            const wxReadOnlyDC& infoDc = dc;
            const std::wstring cellValue = getValue(row, colType);
            return gapSize_ + infoDc.GetTextExtent(cellValue).GetWidth() + gapSize_;
        }
    }


    std::wstring getColumnLabel(ColumnType colType) const override
    {
        switch (static_cast<ColumnTypeRim>(colType))
        {
            case ColumnTypeRim::path:
                switch (itemPathFormat_)
                {
                    case ItemPathFormat::name:     return _("Item name");
                    case ItemPathFormat::relative: return _("Relative path");
                    case ItemPathFormat::full:     return _("Full path");
                }
                assert(false);
                break;
            case ColumnTypeRim::size:      return _("Size");
            case ColumnTypeRim::date:      return _("Date");
            case ColumnTypeRim::extension: return _("Extension");
        }
        //assert(false); may be ColumnType::none
        return std::wstring();
    }

    void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override
    {
        const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
        wxRect rectRemain = rectInner;

        rectRemain.x     += getColumnGapLeft();
        rectRemain.width -= getColumnGapLeft();
        drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled);

        //draw sort marker
        if (auto sortInfo = getDataView().getSortConfig())
            if (const ColumnTypeRim* sortType = std::get_if<ColumnTypeRim>(&sortInfo->sortCol))
                if (*sortType == static_cast<ColumnTypeRim>(colType) && sortInfo->onLeft == (side == SelectSide::left))
                {
                    bool ascending = sortInfo->ascending; //work around MSVC 17.4 compiler bug :( "error C2039: 'sortCol': is not a member of 'fff::FileView::SortInfo'"

                    const wxImage sortMarker = loadImage(ascending ? "sort_ascending" : "sort_descending");
                    drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectInner, wxALIGN_CENTER_HORIZONTAL);
                }
    }

    std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override
    {
        const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);

        std::wstring toolTip;

        if (const FileSystemObject* tipObj = static_cast<HoverAreaGroup>(rowHover) == HoverAreaGroup::groupName ? pdi.folderGroupObj : pdi.fsObj)
        {
            if (getDataView().getEffectiveFolderPairCount() > 1)
                toolTip += AFS::getDisplayPath(tipObj->base().getAbstractPath<side>()) + rightArrowDown_ + L"\n\n";

            toolTip += utfTo<std::wstring>(tipObj->getRelativePath<side>());

            //path components should follow the app layout direction and are NOT a single piece of text!
            //caveat: add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer"
            assert(!contains(toolTip, slashBidi_) && !contains(toolTip, bslashBidi_));
            replace(toolTip, L'/',   slashBidi_);
            replace(toolTip, L'\\', bslashBidi_);

            if (tipObj->isEmpty<side>())
                toolTip += std::wstring(L"\n") + TAB_SPACE + L'<' + _("Item not existing") + L'>';
            else
                visitFSObject(*tipObj, [&](const FolderPair& folder)
            {
                //toolTip += std::wstring(L"\n") + TAB_SPACE + '<' + _("Folder") + L'>'; -> redundant!?
            },
            [&](const FilePair& file)
            {
                toolTip += std::wstring(L"\n") + TAB_SPACE + _("Size:") + L' ' + formatFilesizeShort (file.getFileSize     <side>()) +
                           /**/         L'\n'  + TAB_SPACE + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<side>());
            },
            [&](const SymlinkPair& symlink)
            {
                toolTip += std::wstring(L"\n") + TAB_SPACE + L'<' + _("Symlink") + L'>' +
                           /**/         L'\n'  + TAB_SPACE + _("Date:") + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime<side>());
            });
        }
        return toolTip;
    }


    enum class IconType
    {
        none,
        folder,
        standard,
    };
    struct IconInfo
    {
        IconType type = IconType::none;
        bool drawAsLink = false;
    };
    static IconInfo getIconInfo(const FileSystemObject& fsObj)
    {
        IconInfo out;

        if (!fsObj.isEmpty<side>())
            visitFSObject(fsObj, [&](const FolderPair& folder)
        {
            out.type       = IconType::folder;
            out.drawAsLink = folder.isFollowedSymlink<side>();
        },

        [&](const FilePair& file)
        {
            out.type       = IconType::standard;
            out.drawAsLink = file.isFollowedSymlink<side>() || hasLinkExtension(file.getItemName<side>());
        },

        [&](const SymlinkPair& symlink)
        {
            out.type       = IconType::standard;
            out.drawAsLink = true;
        });
        return out;
    }

    const int gapSize_     = dipToWxsize(FILE_GRID_GAP_SIZE_DIP);
    const int gapSizeWide_ = dipToWxsize(FILE_GRID_GAP_SIZE_WIDE_DIP);

    const wxColor mouseHighlightColor_ = enhanceContrast(*wxBLUE, //primarily needed for dark mode!
                                                         wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 5 /*contrastRatioMin*/); //W3C recommends >= 4.5

    const int charHeight_ = refGrid().getMainWin().GetCharHeight();


    ItemPathFormat itemPathFormat_ = ItemPathFormat::full;

    std::vector<unsigned char> failedLoads_; //effectively a vector<bool> of size "number of rows"

    const std::wstring  slashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring(L"/");
    const std::wstring bslashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring(L"\\");
    //no need for LTR/RTL marks on both sides: text follows main direction if slash is between two strong characters with different directions

    const std::wstring rightArrowDown_ = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ?
                                         std::wstring() + RTL_MARK + LEFT_ARROW_ANTICLOCK :
                                         std::wstring() + LTR_MARK + RIGHT_ARROW_CURV_DOWN;
    //Windows bug: RIGHT_ARROW_CURV_DOWN rendering and extent calculation is buggy (see wx+\tooltip.cpp) => need LTR mark!

    std::vector<int> groupItemNamesWidthBuf_; //buffer! groupItemNamesWidths essentially only depends on (groupIdx, side)
    uint64_t viewUpdateIdLast_ = 0;           //
};


class GridDataLeft : public GridDataRim<SelectSide::left>
{
public:
    GridDataLeft(Grid& grid, const SharedRef<SharedComponents>& sharedComp) : GridDataRim<SelectSide::left>(grid, sharedComp) {}
};

class GridDataRight : public GridDataRim<SelectSide::right>
{
public:
    GridDataRight(Grid& grid, const SharedRef<SharedComponents>& sharedComp) : GridDataRim<SelectSide::right>(grid, sharedComp) {}
};

//########################################################################################################

class GridDataCenter : public GridDataBase
{
public:
    GridDataCenter(Grid& grid, const SharedRef<SharedComponents>& sharedComp) : GridDataBase(grid, sharedComp),
        toolTip_(grid) {} //tool tip must not live longer than grid!

    void onSelectBegin()
    {
        selectionInProgress_ = true;
        refGrid().clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion!
        toolTip_.hide(); //handle custom tooltip
    }

    void onSelectEnd(size_t rowFirst, size_t rowLast, HoverArea rowHover, ptrdiff_t clickInitRow)
    {
        refGrid().clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion!

        //issue custom event
        if (selectionInProgress_) //don't process selections initiated by right-click
            if (rowFirst < rowLast && rowLast <= refGrid().getRowCount()) //empty? probably not in this context
                switch (static_cast<HoverAreaCenter>(rowHover))
                {
                    case HoverAreaCenter::checkbox:
                        if (const FileSystemObject* fsObj = getFsObject(clickInitRow))
                        {
                            const bool setIncluded = !fsObj->isActive();
                            CheckRowsEvent evt(rowFirst, rowLast, setIncluded);
                            refGrid().GetEventHandler()->ProcessEvent(evt);
                        }
                        break;
                    case HoverAreaCenter::dirLeft:
                    {
                        SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::left);
                        refGrid().GetEventHandler()->ProcessEvent(evt);
                    }
                    break;
                    case HoverAreaCenter::dirNone:
                    {
                        SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::none);
                        refGrid().GetEventHandler()->ProcessEvent(evt);
                    }
                    break;
                    case HoverAreaCenter::dirRight:
                    {
                        SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::right);
                        refGrid().GetEventHandler()->ProcessEvent(evt);
                    }
                    break;
                }
        selectionInProgress_ = false;

        //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows)
        wxPoint clientPos = refGrid().getMainWin().ScreenToClient(wxGetMousePosition());
        evalMouseMovement(clientPos);
    }

    void evalMouseMovement(const wxPoint& clientPos)
    {
        //manage block highlighting and custom tooltip
        if (!selectionInProgress_)
        {
            const size_t              row = refGrid().getRowAtWinPos   (clientPos.y); //return -1 for invalid position, rowCount if past the end
            const Grid::ColumnPosInfo cpi = refGrid().getColumnAtWinPos(clientPos.x); //returns ColumnType::none if no column at x position!

            if (row < refGrid().getRowCount() && cpi.colType != ColumnType::none &&
                refGrid().getMainWin().GetClientRect().Contains(clientPos)) //cursor might have moved outside visible client area
                showToolTip(row, static_cast<ColumnTypeCenter>(cpi.colType), refGrid().getMainWin().ClientToScreen(clientPos));
            else
                toolTip_.hide();
        }
    }

    void onMouseLeave() //wxEVT_LEAVE_WINDOW does not respect mouse capture!
    {
        toolTip_.hide(); //handle custom tooltip
    }

private:
    std::wstring getValue(size_t row, ColumnType colType) const override
    {
        if (const FileSystemObject* fsObj = getFsObject(row))
            switch (static_cast<ColumnTypeCenter>(colType))
            {
                case ColumnTypeCenter::checkbox:
                    break;
                case ColumnTypeCenter::difference:
                    return getSymbol(fsObj->getCategory());
                case ColumnTypeCenter::action:
                    return getSymbol(fsObj->getSyncOperation());
            }
        return std::wstring();
    }

    void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override
    {
        const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);

        if (!enabled || !selected)
        {
            const wxColor backCol = [&]
            {
                if (!pdi.fsObj)
                    return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);

                if (!pdi.fsObj->isActive())
                    return getColorInactiveBack();

                return getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 == 0);
            }();
            if (backCol != wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW) /*already the default!*/)
                clearArea(dc, rect, backCol);
        }
        else
            GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover);

        //----------------------------------------------------------------------------------
        const wxRect rectLine(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1));
        clearArea(dc, rectLine, row == pdi.groupLastRow - 1 /*last group item*/ ?
                  getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0));
    }

    enum class HoverAreaCenter //each cell can be divided into four blocks concerning mouse selections
    {
        checkbox,
        dirLeft,
        dirNone,
        dirRight
    };

    void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override
    {
        if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row);
            pdi.fsObj)
        {
            auto drawHighlightBackground = [&](const wxColor& col)
            {
                if ((!enabled || !selected) && pdi.fsObj->isActive()) //coordinate with renderRowBackgound()!
                {
                    wxRect rectBack = rect;
                    if (row == pdi.groupLastRow - 1 /*last group item*/) //preserve the group separation line!
                        rectBack.height -= dipToWxsize(1);

                    clearArea(dc, rectBack, col);
                }
            };

            switch (static_cast<ColumnTypeCenter>(colType))
            {
                case ColumnTypeCenter::checkbox:
                {
                    const bool drawMouseHover = static_cast<HoverAreaCenter>(rowHover) == HoverAreaCenter::checkbox;

                    wxImage icon = loadImage(pdi.fsObj->isActive() ?
                                             (drawMouseHover ? "checkbox_true_hover"  : "checkbox_true") :
                                             (drawMouseHover ? "checkbox_false_hover" : "checkbox_false"));
                    if (!enabled)
                        icon = icon.ConvertToDisabled();

                    drawBitmapRtlNoMirror(dc, icon, rect, wxALIGN_CENTER);
                }
                break;

                case ColumnTypeCenter::difference:
                {
                    if (getViewType() == GridViewType::difference)
                        drawHighlightBackground(getBackGroundColorCmpDifference(pdi.fsObj->getCategory()));

                    wxRect rectTmp = rect;
                    {
                        //draw notch on left side
                        if (notch_.GetHeight() != wxsizeToScreen(rectTmp.height))
                            notch_ = notch_.Scale(notch_.GetWidth(), wxsizeToScreen(rectTmp.height));

                        //wxWidgets screws up again and has wxALIGN_RIGHT off by one pixel! -> use wxALIGN_LEFT instead
                        const wxRect rectNotch(rectTmp.x + rectTmp.width - screenToWxsize(notch_.GetWidth()), rectTmp.y,
                                               screenToWxsize(notch_.GetWidth()), rectTmp.height);
                        drawBitmapRtlNoMirror(dc, notch_, rectNotch, wxALIGN_LEFT);
                        rectTmp.width -= screenToWxsize(notch_.GetWidth());
                    }

                    auto drawIcon = [&](wxImage icon, int alignment)
                    {
                        if (!enabled)
                            icon = icon.ConvertToDisabled();

                        drawBitmapRtlMirror(dc, icon, rectTmp, alignment, renderBufCmp_);
                    };

                    if (getViewType() == GridViewType::difference)
                        drawIcon(getCmpResultImage(pdi.fsObj->getCategory()), wxALIGN_CENTER);
                    else if (pdi.fsObj->getCategory() != FILE_EQUAL) //don't show = in both middle columns
                        drawIcon(greyScale(getCmpResultImage(pdi.fsObj->getCategory())), wxALIGN_CENTER);
                }
                break;

                case ColumnTypeCenter::action:
                {
                    if (getViewType() == GridViewType::action)
                        drawHighlightBackground(getBackGroundColorSyncAction(pdi.fsObj->getSyncOperation()));

                    auto drawIcon = [&](wxImage icon, int alignment)
                    {
                        if (!enabled)
                            icon = icon.ConvertToDisabled();

                        drawBitmapRtlMirror(dc, icon, rect, alignment, renderBufSync_);
                    };

                    //synchronization preview
                    const auto rowHoverCenter = rowHover == HoverArea::none ? HoverAreaCenter::checkbox : static_cast<HoverAreaCenter>(rowHover);
                    switch (rowHoverCenter)
                    {
                        case HoverAreaCenter::dirLeft:
                            drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::left)), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
                            break;
                        case HoverAreaCenter::dirNone:
                            drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::none)), wxALIGN_CENTER);
                            break;
                        case HoverAreaCenter::dirRight:
                            drawIcon(getSyncOpImage(pdi.fsObj->testSyncOperation(SyncDirection::right)), wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
                            break;
                        case HoverAreaCenter::checkbox:
                            if (getViewType() == GridViewType::action)
                                drawIcon(getSyncOpImage(pdi.fsObj->getSyncOperation()), wxALIGN_CENTER);
                            else if (pdi.fsObj->getSyncOperation() != SO_EQUAL) //don't show = in both middle columns
                                drawIcon(greyScale(getSyncOpImage(pdi.fsObj->getSyncOperation())), wxALIGN_CENTER);
                            break;
                    }
                }
                break;
            }
        }
    }

    HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override
    {
        if (const FileSystemObject* const fsObj = getFsObject(row))
            switch (static_cast<ColumnTypeCenter>(colType))
            {
                case ColumnTypeCenter::checkbox:
                case ColumnTypeCenter::difference:
                    return static_cast<HoverArea>(HoverAreaCenter::checkbox);

                case ColumnTypeCenter::action:
                    if (fsObj->getSyncOperation() == SO_EQUAL) //in sync-preview equal files shall be treated like a checkbox
                        return static_cast<HoverArea>(HoverAreaCenter::checkbox);
                    /* cell: ------------------------
                             | left | middle | right|
                             ------------------------    */
                    if (0 <= cellRelativePosX)
                    {
                        if (cellRelativePosX < cellWidth / 3)
                            return static_cast<HoverArea>(HoverAreaCenter::dirLeft);
                        else if (cellRelativePosX < 2 * cellWidth / 3)
                            return static_cast<HoverArea>(HoverAreaCenter::dirNone);
                        else if  (cellRelativePosX < cellWidth)
                            return static_cast<HoverArea>(HoverAreaCenter::dirRight);
                    }
                    break;
            }
        return HoverArea::none;
    }

    std::wstring getColumnLabel(ColumnType colType) const override
    {
        switch (static_cast<ColumnTypeCenter>(colType))
        {
            case ColumnTypeCenter::checkbox:
                break;
            case ColumnTypeCenter::difference:
                return _("Difference");
            case ColumnTypeCenter::action:
                return _("Action");
        }
        return std::wstring();
    }

    std::wstring getToolTip(ColumnType colType) const override { return getColumnLabel(colType) + L" (F11)"; }

    void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override
    {
        const auto colTypeCenter = static_cast<ColumnTypeCenter>(colType);

        const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted && colTypeCenter != ColumnTypeCenter::checkbox);

        wxImage colIcon;
        switch (colTypeCenter)
        {
            case ColumnTypeCenter::checkbox:
                break;

            case ColumnTypeCenter::difference:
                colIcon = greyScaleIfDisabled(loadImage("compare", dipToScreen(getMenuIconDipSize())), getViewType() == GridViewType::difference);
                break;

            case ColumnTypeCenter::action:
                colIcon = greyScaleIfDisabled(loadImage("start_sync", dipToScreen(getMenuIconDipSize())), getViewType() == GridViewType::action);
                break;
        }

        if (colIcon.IsOk())
            drawBitmapRtlNoMirror(dc, enabled ? colIcon : colIcon.ConvertToDisabled(), rectInner, wxALIGN_CENTER);

        //draw sort marker
        if (auto sortInfo = getDataView().getSortConfig())
            if (const ColumnTypeCenter* sortType = std::get_if<ColumnTypeCenter>(&sortInfo->sortCol))
                if (*sortType == colTypeCenter)
                {
                    const int gapLeft = (rectInner.width + screenToWxsize(colIcon.GetWidth())) / 2;
                    wxRect rectRemain = rectInner;
                    rectRemain.x     += gapLeft;
                    rectRemain.width -= gapLeft;

                    const wxImage sortMarker = loadImage(sortInfo->ascending ? "sort_ascending" : "sort_descending");
                    drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
                }
    }

    void showToolTip(size_t row, ColumnTypeCenter colType, wxPoint posScreen)
    {
        if (const FileSystemObject* fsObj = getFsObject(row))
        {
            switch (colType)
            {
                case ColumnTypeCenter::checkbox:
                case ColumnTypeCenter::difference:
                {
                    const char* imageName = [&]
                    {
                        switch (fsObj->getCategory())
                        {
                            case FILE_RENAMED:  //similar to both "equal" and "conflict"
                            case FILE_EQUAL:             return "cat_equal";
                            case FILE_LEFT_ONLY:         return "cat_left_only";
                            case FILE_RIGHT_ONLY:        return "cat_right_only";
                            case FILE_LEFT_NEWER:        return "cat_left_newer";
                            case FILE_RIGHT_NEWER:       return "cat_right_newer";
                            case FILE_DIFFERENT_CONTENT: return "cat_different";
                            case FILE_TIME_INVALID:
                            case FILE_CONFLICT:          return "cat_conflict";
                        }
                        assert(false);
                        return "";
                    }();
                    const auto& img = mirrorIfRtl(loadImage(imageName));
                    toolTip_.show(getCategoryDescription(*fsObj), posScreen, &img);
                }
                break;

                case ColumnTypeCenter::action:
                {
                    const char* imageName = [&]
                    {
                        switch (fsObj->getSyncOperation())
                        {
                            case SO_CREATE_LEFT:         return "so_create_left";
                            case SO_CREATE_RIGHT:        return "so_create_right";
                            case SO_DELETE_LEFT:         return "so_delete_left";
                            case SO_DELETE_RIGHT:        return "so_delete_right";
                            case SO_MOVE_LEFT_FROM:      return "so_move_left_source";
                            case SO_MOVE_LEFT_TO:        return "so_move_left_target";
                            case SO_MOVE_RIGHT_FROM:     return "so_move_right_source";
                            case SO_MOVE_RIGHT_TO:       return "so_move_right_target";
                            case SO_OVERWRITE_LEFT:      return "so_update_left";
                            case SO_OVERWRITE_RIGHT:     return "so_update_right";
                            case SO_RENAME_LEFT:         return "so_move_left";
                            case SO_RENAME_RIGHT:        return "so_move_right";
                            case SO_DO_NOTHING:          return "so_none";
                            case SO_EQUAL:               return "cat_equal";
                            case SO_UNRESOLVED_CONFLICT: return "cat_conflict";
                        };
                        assert(false);
                        return "";
                    }();
                    const auto& img = mirrorIfRtl(loadImage(imageName));
                    toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img);
                }
                break;
            }
        }
        else
            toolTip_.hide(); //if invalid row...
    }

    bool selectionInProgress_ = false;

    std::optional<wxBitmap> renderBufCmp_; //avoid costs of recreating this temporary variable
    std::optional<wxBitmap> renderBufSync_;
    Tooltip toolTip_;
    wxImage notch_ = loadImage("notch");
};

//########################################################################################################

class GridEventManager : private wxEvtHandler
{
public:
    GridEventManager(Grid& gridL,
                     Grid& gridC,
                     Grid& gridR,
                     GridDataCenter& provCenter) :
        gridL_(gridL), gridC_(gridC), gridR_(gridR),
        provCenter_(provCenter)
    {
        gridL_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumn(event, gridL_, gridR_); });
        gridR_.Bind(EVENT_GRID_COL_RESIZE, [this](GridColumnResizeEvent& event) { onResizeColumn(event, gridR_, gridL_); });

        gridL_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridL_); });
        gridC_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridC_); });
        gridR_.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event, gridR_); });

        gridC_.getMainWin().Bind(wxEVT_MOTION,       [this](wxMouseEvent& event) { onCenterMouseMovement(event); });
        gridC_.getMainWin().Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& event) { onCenterMouseLeave   (event); });

        gridC_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent&  event) { onCenterSelectBegin(event); });
        gridC_.Bind(EVENT_GRID_SELECT_RANGE,    [this](GridSelectEvent& event) { onCenterSelectEnd  (event); });

        gridL_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onGridClickRim(event, gridL_); });
        gridR_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onGridClickRim(event, gridR_); });

        //clear selection of other grid when selecting on
        gridL_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN,  [this]( GridClickEvent& event) { onGridLeftClick(event, gridR_); }); //clear immediately,
        gridL_.Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this]( GridClickEvent& event) { onGridRightClick(event, gridR_, gridL_); }); //don't wait for GridSelectEvent
        gridL_.Bind(EVENT_GRID_SELECT_RANGE,     [this](GridSelectEvent& event) { onGridSelection(event, gridR_); });

        gridR_.Bind(EVENT_GRID_MOUSE_LEFT_DOWN,  [this]( GridClickEvent& event) { onGridLeftClick(event, gridL_); });
        gridR_.Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this]( GridClickEvent& event) { onGridRightClick(event, gridL_, gridR_); });
        gridR_.Bind(EVENT_GRID_SELECT_RANGE,     [this](GridSelectEvent& event) { onGridSelection(event, gridL_); });

        //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead:
        gridL_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridL_); event.Skip(); });
        gridC_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridC_); event.Skip(); });
        gridR_.getMainWin().Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintGrid(gridR_); event.Skip(); });


        //-----------------------------------------------------------------------------------------------------
        //scroll master event handling: connect LAST, so that scrollMaster_ is set BEFORE other event handling!
        //-----------------------------------------------------------------------------------------------------
        auto connectGridAccess = [&](Grid& grid, std::function<void(wxEvent& event)> handler)
        {
            grid.Bind(wxEVT_SCROLLWIN_TOP,        handler);
            grid.Bind(wxEVT_SCROLLWIN_BOTTOM,     handler);
            grid.Bind(wxEVT_SCROLLWIN_LINEUP,     handler);
            grid.Bind(wxEVT_SCROLLWIN_LINEDOWN,   handler);
            grid.Bind(wxEVT_SCROLLWIN_PAGEUP,     handler);
            grid.Bind(wxEVT_SCROLLWIN_PAGEDOWN,   handler);
            grid.Bind(wxEVT_SCROLLWIN_THUMBTRACK, handler);
            //wxEVT_KILL_FOCUS -> there's no need to reset "scrollMaster"
            //wxEVT_SET_FOCUS -> not good enough:
            //e.g.: left grid has input, right grid is "scrollMaster" due to dragging scroll thumb via mouse.
            //=> Next keyboard input on left does *not* emit focus change event, but still "scrollMaster" needs to change
            //=> hook keyboard input instead of focus event:
            grid.getMainWin().Bind(wxEVT_CHAR,     handler);
            grid.Bind(wxEVT_KEY_DOWN, handler);
            //grid.getMainWin().Bind(wxEVT_KEY_UP, handler); -> superfluous?

            grid.getMainWin().Bind(wxEVT_LEFT_DOWN,   handler);
            grid.getMainWin().Bind(wxEVT_LEFT_DCLICK, handler);
            grid.getMainWin().Bind(wxEVT_RIGHT_DOWN,  handler);
            grid.getMainWin().Bind(wxEVT_MOUSEWHEEL,  handler);
        };
        connectGridAccess(gridL_, [this](wxEvent& event) { setScrollMaster(gridL_); event.Skip(); }); //
        connectGridAccess(gridC_, [this](wxEvent& event) { setScrollMaster(gridC_); event.Skip(); }); //connect *after* onKeyDown() in order to receive callback *before*!!!
        connectGridAccess(gridR_, [this](wxEvent& event) { setScrollMaster(gridR_); event.Skip(); }); //
    }

    ~GridEventManager()
    {
        //assert(!scrollbarAlignPending_); => false-positives: e.g. start ffs, right-click on grid, close dialog by clicking X
    }

    void setScrollMaster(const Grid& grid)
    {
        if (&grid != &gridC_ &&
            &grid != &gridL_ &&
            &grid != &gridR_)
        {
            assert(false); //does this ever happen?
            return ;
        }
#if 0
        if (const std::string& logtext = "new scroll master: " + printNumber<std::string>("%llx", reinterpret_cast<unsigned long long>(&grid)) + "\n";
            scrollMaster_ != &grid)

            std::cerr << logtext;
#endif
        scrollMaster_ = &grid;
    }

private:
    void onCenterSelectBegin(GridClickEvent& event)
    {
        provCenter_.onSelectBegin();
        event.Skip();
    }

    void onCenterSelectEnd(GridSelectEvent& event)
    {
        if (event.positive_)
        {
            if (event.mouseClick_)
                provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseClick_->hoverArea_, event.mouseClick_->row_);
            else
                provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::none, -1);
        }
        event.Skip();
    }

    void onCenterMouseMovement(wxMouseEvent& event)
    {
        provCenter_.evalMouseMovement(event.GetPosition());
        event.Skip();
    }

    void onCenterMouseLeave(wxMouseEvent& event)
    {
        provCenter_.onMouseLeave();
        event.Skip();
    }

    void onGridClickRim(GridClickEvent& event, Grid& grid)
    {
        if (static_cast<HoverAreaGroup>(event.hoverArea_) == HoverAreaGroup::groupName)
            if (const FileView::PathDrawInfo pdi = provCenter_.getDataView().getDrawInfo(event.row_);
                pdi.fsObj)
            {
                const ptrdiff_t topRowOld = grid.getRowAtWinPos(0);
                grid.makeRowVisible(pdi.groupFirstRow);
                const ptrdiff_t topRowNew = grid.getRowAtWinPos(0);

                if (topRowNew != topRowOld) //=> grid was scrolled: prevent AddPendingEvent() recursion!
                {
                    assert(topRowNew == makeSigned(pdi.groupFirstRow));
                    assert(topRowNew == grid.getRowAtWinPos((event.mousePos_ - grid.getMainWin().GetPosition()).y));
                    //don't waste a click: simulate start of new selection at Grid::MainWin-relative position (0/0):
                    grid.getMainWin().GetEventHandler()->AddPendingEvent(wxMouseEvent(wxEVT_LEFT_DOWN));
                    return;
                }
            }
        event.Skip();
    }

    void onGridLeftClick(GridClickEvent& event, Grid& gridOther)
    {
        //see grid.cpp Grid::MainWin::onMouseDown():
        if (!wxGetKeyState(WXK_CONTROL) && !wxGetKeyState(WXK_SHIFT)) //clear other grid unless user is holding CTRL, or SHIFT
            gridOther.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion!
        event.Skip();
    }

    void onGridRightClick(GridClickEvent& event, Grid& gridOther, Grid& gridThis)
    {
        const std::vector<size_t>& selectedRows = gridThis.getSelectedRows();
        const bool rowSelected = std::find(selectedRows.begin(), selectedRows.end(), makeUnsigned(event.row_)) != selectedRows.end();

        //clear other grid unless GridContextMenuEvent is about to happen, or user is holding CTRL, or SHIFT
        if (!rowSelected && !wxGetKeyState(WXK_CONTROL) && !wxGetKeyState(WXK_SHIFT))
            gridOther.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion!
        event.Skip();
    }

    void onGridSelection(GridSelectEvent& event, Grid& gridOther)
    {
        if (!event.mouseClick_ && !wxGetKeyState(WXK_SHIFT)) //clear other grid during keyboard selection, unless user is holding SHIFT
            gridOther.clearSelection(GridEventPolicy::deny); //don't emit event, prevent recursion!
        event.Skip();
    }

    void onKeyDown(wxKeyEvent& event, const Grid& grid)
    {
        int keyCode = event.GetKeyCode();
        if (grid.GetLayoutDirection() == wxLayout_RightToLeft)
        {
            if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
                keyCode = WXK_RIGHT;
            else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
                keyCode = WXK_LEFT;
        }

        //skip middle component when navigating via keyboard
        const size_t row = grid.getGridCursor();

        if (event.ShiftDown())
            ;
        else if (event.ControlDown())
            ;
        else
            switch (keyCode)
            {
                case WXK_LEFT:
                case WXK_NUMPAD_LEFT:
                    gridL_.setGridCursor(row, GridEventPolicy::allow);
                    gridL_.SetFocus();
                    //since key event is likely originating from right grid, we need to set scrollMaster manually!
                    setScrollMaster(gridL_); //onKeyDown is called *after* onGridAccessL()!
                    return; //swallow event

                case WXK_RIGHT:
                case WXK_NUMPAD_RIGHT:
                    gridR_.setGridCursor(row, GridEventPolicy::allow);
                    gridR_.SetFocus();
                    setScrollMaster(gridR_);
                    return; //swallow event
            }

        event.Skip();
    }

    void onResizeColumn(GridColumnResizeEvent& event, const Grid& grid, Grid& gridOther)
    {
        //find stretch factor of resized column: type is unique due to makeConsistent()!
        std::vector<Grid::ColAttributes> cfgSrc = grid.getColumnConfig();
        auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == event.colType_; });
        if (it == cfgSrc.end())
            return;
        const int stretchSrc = it->stretch;

        //we do not propagate resizings on stretched columns to the other side: awkward user experience
        if (stretchSrc > 0)
            return;

        //apply resized offset to other side, but only if stretch factors match!
        std::vector<Grid::ColAttributes> cfgTrg = gridOther.getColumnConfig();
        for (Grid::ColAttributes& ca : cfgTrg)
            if (ca.type == event.colType_ && ca.stretch == stretchSrc)
                ca.offset = event.offset_;
        gridOther.setColumnConfig(cfgTrg);
    }

    void onPaintGrid(const Grid& grid)
    {
#if 0
        const std::string& logtext = "wxEVT_PAINT: " + printNumber<std::string>("%llx", reinterpret_cast<unsigned long long>(&grid)) + "\n";
        std::cerr << logtext;
#endif
        /* keep scroll positions of all three grids in sync

           wxGrid::Scroll() *during* vs *after* paint event:
           ------------------------------------------------
           macOS: doesn't matter; 3 paint events per mouse scroll

           Linux: no visible perf issue, but
                    a) *during* paint event: 6 paint events
                    b) *after* paint event: 4 paint events

           Windows: a) *during* paint event:
                        1. double-buffering(WS_EX_COMPOSITED) => excessive amount of additional paint events and accidental RECURSION!!!
                           Apparently multiple paint events sent (with clipped DC), then during wxGrid::Scroll() -> wxWindow::Update() -> onPaintGrid() for *SAME* grid!
                        2. no double buffering                => 4 paint events per mouse scroll
                    b) *after* paint event:
                        1. double-buffering(WS_EX_COMPOSITED) => 6 paint events per mouse scroll
                            => no visible perf-difference compared to 2. but 60% higher CPU time during excessive scrolling
                        2. no double buffering                => 4 paint events per mouse scroll             */
        if (&grid == scrollMaster_ && !scrollPosAlignPending_)
        {
            scrollPosAlignPending_ = true;

            CallAfter([this]
            {
                auto scroll = [this](Grid& target, int y) //support polling
                {
                    if (&target != scrollMaster_)
                    {
                        //scroll vertically only - scrolling horizontally becomes annoying if left and right sides have different widths;
                        //e.g. h-scroll on left would be undone when scrolling vertically on right which doesn't have a h-scrollbar
                        int yOld = 0;
                        target.GetViewStart(nullptr, &yOld);
                        if (yOld != y)
                            target.Scroll(-1, y); //empirical test Windows/Ubuntu: this call does NOT trigger a wxEVT_SCROLLWIN event,
                        //                          which would incorrectly set "scrollMaster" to "&target"!
                        //CAVEAT: wxScrolledWindow::Scroll() internally calls wxWindow::Update(), leading to immediate WM_PAINT handling in the target grid!
                        //        and this while we're still in our WM_PAINT handler! => no recursion, thanks to scrollMaster_ (hopefully)
                    }
                };
                int y = 0;
                scrollMaster_->GetViewStart(nullptr, &y);
                scroll(gridC_, y);
                scroll(gridL_, y);
                scroll(gridR_, y);

                assert(scrollPosAlignPending_);
                scrollPosAlignPending_ = false;
            });
        }

        //harmonize placement of horizontal scrollbar to avoid grids getting out of sync!
        //since this affects the grid that is currently repainted, run asynchronously!
        if (!scrollbarAlignPending_) //send one async event at most, else they may accumulate and create perf issues, see grid.cpp
        {
            scrollbarAlignPending_ = true;

            CallAfter([this] //update *outside* of wxPaint event
            {
                auto needsHorizontalScrollbars = [](const Grid& target)
                {
                    const wxWindow& mainWin = target.getMainWin();
                    return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth();
                    //assuming Grid::updateWindowSizes() does its job well, this should suffice!
                    //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other
                    //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, etc...)
                    //while in fact both are NOT needed, this special case results in a bogus need for scrollbars!
                    //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083
                    // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant
                };

                Grid::ScrollBarStatus sbStatusX = needsHorizontalScrollbars(gridL_) ||
                                                  needsHorizontalScrollbars(gridR_) ?
                                                  Grid::SB_SHOW_ALWAYS : Grid::SB_SHOW_NEVER;
                gridL_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER);
                gridC_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER);
                gridR_.showScrollBars(sbStatusX, Grid::SB_SHOW_AUTOMATIC);

                assert(scrollbarAlignPending_);
                scrollbarAlignPending_ = false;
            });
        }
    }

    Grid& gridL_;
    Grid& gridC_;
    Grid& gridR_;

    const Grid* scrollMaster_ = &gridL_; //for address check only; this needn't be the grid having focus!
    //e.g. mouse wheel events should set window under cursor as scrollMaster, but *not* change focus

    GridDataCenter& provCenter_;
    bool scrollbarAlignPending_ = false;
    bool scrollPosAlignPending_ = false;
};
}

//########################################################################################################

void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight)
{
    auto sharedComp = makeSharedRef<SharedComponents>();

    auto provLeft_   = std::make_shared<GridDataLeft  >(gridLeft,   sharedComp);
    auto provCenter_ = std::make_shared<GridDataCenter>(gridCenter, sharedComp);
    auto provRight_  = std::make_shared<GridDataRight >(gridRight,  sharedComp);

    sharedComp.ref().evtMgr = std::make_unique<GridEventManager>(gridLeft, gridCenter, gridRight, *provCenter_);

    gridLeft  .setDataProvider(provLeft_);   //data providers reference grid =>
    gridCenter.setDataProvider(provCenter_); //ownership must belong *exclusively* to grid!
    gridRight .setDataProvider(provRight_);

    gridCenter.enableColumnMove  (false);
    gridCenter.enableColumnResize(false);

    gridCenter.showRowLabel(false);
    gridRight .showRowLabel(false);

    //gridLeft  .showScrollBars(Grid::SB_SHOW_AUTOMATIC, Grid::SB_SHOW_NEVER); -> redundant: configuration happens in GridEventManager::onPaintGrid()
    //gridCenter.showScrollBars(Grid::SB_SHOW_NEVER,     Grid::SB_SHOW_NEVER);

    const int widthCheckbox   = screenToWxsize(    loadImage("checkbox_true").GetWidth() + dipToScreen(3));
    const int widthDifference = screenToWxsize(2 * loadImage("sort_ascending").GetWidth() + loadImage("cat_left_only_sicon").GetWidth() + loadImage("notch").GetWidth());
    const int widthAction     = screenToWxsize(3 * loadImage("so_create_left_sicon").GetWidth());
    gridCenter.SetSize(widthDifference + widthCheckbox + widthAction, -1);

    gridCenter.setColumnConfig(
    {
        {static_cast<ColumnType>(ColumnTypeCenter::checkbox),   widthCheckbox,   0, true},
        {static_cast<ColumnType>(ColumnTypeCenter::difference), widthDifference, 0, true},
        {static_cast<ColumnType>(ColumnTypeCenter::action),     widthAction,     0, true},
    });
}


void filegrid::setData(Grid& grid, FolderComparison& folderCmp)
{
    if (auto* prov = dynamic_cast<GridDataBase*>(grid.getDataProvider()))
        return prov->setData(folderCmp);

    throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__));
}


FileView& filegrid::getDataView(Grid& grid)
{
    if (auto* prov = dynamic_cast<GridDataBase*>(grid.getDataProvider()))
        return prov->getDataView();

    throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__));
}


namespace
{
//resolve circular linker dependencies
void IconUpdater::loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons
{
    std::vector<std::pair<ptrdiff_t, AbstractPath>> prefetchLoad;
    provLeft_ .getUnbufferedIconsForPreload(prefetchLoad);
    provRight_.getUnbufferedIconsForPreload(prefetchLoad);

    //make sure least-important prefetch rows are inserted first into workload (=> processed last)
    //priority index nicely considers both grids at the same time!
    std::sort(prefetchLoad.begin(), prefetchLoad.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });

    //last inserted items are processed first in icon buffer:
    std::vector<AbstractPath> newLoad;
    for (const auto& [priority, filePath] : prefetchLoad)
        newLoad.push_back(filePath);

    provRight_.updateNewAndGetUnbufferedIcons(newLoad);
    provLeft_ .updateNewAndGetUnbufferedIcons(newLoad);

    iconBuffer_.setWorkload(newLoad);

    if (newLoad.empty()) //let's only pay for IconUpdater while needed
        stop();
}
}


void filegrid::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool showFileIcons, IconBuffer::IconSize sz)
{
    auto* provLeft  = dynamic_cast<GridDataLeft*>(gridLeft .getDataProvider());
    auto* provRight = dynamic_cast<GridDataRight*>(gridRight.getDataProvider());

    if (provLeft && provRight)
    {
        auto iconMgr = makeSharedRef<IconManager>(*provLeft, *provRight, sz, showFileIcons);
        provLeft ->setIconManager(iconMgr);

        const int newRowHeight = std::max(iconMgr.ref().getIconWxsize(), gridLeft.getMainWin().GetCharHeight()) + dipToWxsize(1); //add some space

        gridLeft  .setRowHeight(newRowHeight);
        gridCenter.setRowHeight(newRowHeight);
        gridRight .setRowHeight(newRowHeight);
    }
    else
        assert(false);
}


void filegrid::setItemPathForm(Grid& grid, ItemPathFormat fmt)
{
    if (auto* provLeft  = dynamic_cast<GridDataLeft*>(grid.getDataProvider()))
        provLeft->setItemPathForm(fmt);
    else if (auto* provRight = dynamic_cast<GridDataRight*>(grid.getDataProvider()))
        provRight->setItemPathForm(fmt);
    else
        assert(false);
    grid.Refresh();
}


void filegrid::refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight)
{
    gridLeft  .Refresh();
    gridCenter.Refresh();
    gridRight .Refresh();
}


void filegrid::setScrollMaster(Grid& grid)
{
    if (auto prov = dynamic_cast<GridDataBase*>(grid.getDataProvider()))
        if (auto evtMgr = prov->getEventManager())
        {
            evtMgr->setScrollMaster(grid);
            return;
        }
    assert(false);
}


void filegrid::setNavigationMarker(Grid& gridLeft,
                                   zen::Grid& gridRight,
                                   std::unordered_set<const FileSystemObject*>&& markedFilesAndLinks,
                                   std::unordered_set<const ContainerObject*>&& markedContainer)
{
    if (auto grid = dynamic_cast<GridDataBase*>(gridLeft.getDataProvider()))
        grid->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer));
    else
        assert(false);
    gridLeft .Refresh();
    gridRight.Refresh();
}


void filegrid::setViewType(Grid& gridCenter, GridViewType vt)
{
    if (auto prov = dynamic_cast<GridDataBase*>(gridCenter.getDataProvider()))
        prov->setViewType(vt);
    else
        assert(false);
    gridCenter.Refresh();
}


wxImage fff::getSyncOpImage(SyncOperation syncOp)
{
    switch (syncOp) //evaluate comparison result and sync direction
    {
        case SO_CREATE_LEFT:         return loadImage("so_create_left_sicon");
        case SO_CREATE_RIGHT:        return loadImage("so_create_right_sicon");
        case SO_DELETE_LEFT:         return loadImage("so_delete_left_sicon");
        case SO_DELETE_RIGHT:        return loadImage("so_delete_right_sicon");
        case SO_MOVE_LEFT_FROM:      return loadImage("so_move_left_source_sicon");
        case SO_MOVE_LEFT_TO:        return loadImage("so_move_left_target_sicon");
        case SO_MOVE_RIGHT_FROM:     return loadImage("so_move_right_source_sicon");
        case SO_MOVE_RIGHT_TO:       return loadImage("so_move_right_target_sicon");
        case SO_OVERWRITE_LEFT:      return loadImage("so_update_left_sicon");
        case SO_OVERWRITE_RIGHT:     return loadImage("so_update_right_sicon");
        case SO_RENAME_LEFT:         return loadImage("so_move_left_sicon");
        case SO_RENAME_RIGHT:        return loadImage("so_move_right_sicon");
        case SO_DO_NOTHING:          return loadImage("so_none_sicon");
        case SO_EQUAL:               return loadImage("cat_equal_sicon");
        case SO_UNRESOLVED_CONFLICT: return loadImage("cat_conflict_small");
    }
    assert(false);
    return wxNullImage;
}


wxImage fff::getCmpResultImage(CompareFileResult cmpResult)
{
    switch (cmpResult)
    {
        case FILE_RENAMED: //similar to both "equal" and "conflict"
        case FILE_EQUAL:             return loadImage("cat_equal_sicon");
        case FILE_LEFT_ONLY:         return loadImage("cat_left_only_sicon");
        case FILE_RIGHT_ONLY:        return loadImage("cat_right_only_sicon");
        case FILE_LEFT_NEWER:        return loadImage("cat_left_newer_sicon");
        case FILE_RIGHT_NEWER:       return loadImage("cat_right_newer_sicon");
        case FILE_DIFFERENT_CONTENT: return loadImage("cat_different_sicon");
        case FILE_TIME_INVALID:
        case FILE_CONFLICT:          return loadImage("cat_conflict_small");
    }
    assert(false);
    return wxNullImage;
}
