// ***************************************************************************** // * 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 "cfg_grid.h" #include #include #include #include #include #include #include #include #include #include "../ffs_paths.h" #include "../afs/native.h" using namespace zen; using namespace fff; using AFS = AbstractFileSystem; Zstring fff::getLastRunConfigPath() { return appendPath(getConfigDirPath(), Zstr("LastRun.ffs_gui")); } std::vector ConfigView::get() const { std::map> itemsSorted; //sort by last use; put most recent items *first* (looks better in XML than reverted) for (const auto& [filePath, details] : cfgList_) itemsSorted.emplace(details.lastUseIndex, details.cfgItem); std::vector cfgHistory; for (const auto& [lastUseIndex, cfgItem] : itemsSorted) cfgHistory.emplace_back(cfgItem); return cfgHistory; } void ConfigView::set(const std::vector& cfgItems) { std::vector filePaths; for (const ConfigFileItem& item : cfgItems) filePaths.push_back(item.cfgFilePath); //list is stored with last used files first in XML, however addCfgFilesImpl() expects them last!!! std::reverse(filePaths.begin(), filePaths.end()); cfgList_ .clear(); cfgListView_.clear(); addCfgFilesImpl(filePaths); for (const ConfigFileItem& item : cfgItems) cfgList_.find(item.cfgFilePath)->second.cfgItem = item; //cfgFilePath must exist after addCfgFilesImpl()! sortListView(); } void ConfigView::addCfgFiles(const std::vector& filePaths) { addCfgFilesImpl(filePaths); sortListView(); } void ConfigView::addCfgFilesImpl(const std::vector& filePaths) { //determine highest "last use" index number of m_listBoxHistory int lastUseIndexMax = 0; for (const auto& [filePath, details] : cfgList_) lastUseIndexMax = std::max(lastUseIndexMax, details.lastUseIndex); for (const Zstring& filePath : filePaths) if (auto it = cfgList_.find(filePath); it == cfgList_.end()) { Details detail{.lastUseIndex = ++lastUseIndexMax}; detail.cfgItem.cfgFilePath = filePath; std::tie(detail.name, detail.cfgType, detail.isLastRunCfg) = [&] { if (equalNativePath(filePath, lastRunConfigPath_)) return std::make_tuple(utfTo(L'[' + _("Last session") + L']'), Details::CFG_TYPE_GUI, true); const Zstring fileName = getItemName(filePath); if (endsWithAsciiNoCase(fileName, ".ffs_gui")) return std::make_tuple(beforeLast(fileName, Zstr('.'), IfNotFoundReturn::none), Details::CFG_TYPE_GUI, false); else if (endsWithAsciiNoCase(fileName, ".ffs_batch")) return std::make_tuple(beforeLast(fileName, Zstr('.'), IfNotFoundReturn::none), Details::CFG_TYPE_BATCH, false); else return std::make_tuple(fileName, Details::CFG_TYPE_NONE, false); }(); auto itNew = cfgList_.emplace_hint(cfgList_.end(), filePath, std::move(detail)); cfgListView_.push_back(itNew); } else it->second.lastUseIndex = ++lastUseIndexMax; } void ConfigView::removeItems(const std::vector& filePaths) { for (const Zstring& filePath : filePaths) if (auto it = cfgList_.find(filePath); it != cfgList_.end()) { std::erase(cfgListView_, it); cfgList_.erase(it); } else assert(false); assert(cfgList_.size() == cfgListView_.size()); if (sortColumn_ == ColumnTypeCfg::name) sortListView(); //needed if top element of colored-group is removed } void ConfigView::renameItem(const Zstring& pathFrom, const Zstring& pathTo) { auto it = cfgList_.find(pathFrom); assert(it != cfgList_.end()); if (it != cfgList_.end()) { const Details detailsOld = it->second; std::erase(cfgListView_, it); cfgList_.erase(it); assert(cfgList_.size() == cfgListView_.size()); addCfgFilesImpl({pathTo}); //let's not lose certain metadata after renaming! auto it2 = cfgList_.find(pathTo); assert(it2 != cfgList_.end()); if (it2 != cfgList_.end()) { it2->second.cfgItem.lastRunStats = detailsOld.cfgItem.lastRunStats; it2->second.cfgItem.backColor = detailsOld.cfgItem.backColor; it2->second.lastUseIndex = detailsOld.lastUseIndex; it2->second.notes = detailsOld.notes; } sortListView(); } } void ConfigView::setNotes(const Zstring& filePath, const std::wstring& notes) { if (auto it = cfgList_.find(filePath); it != cfgList_.end()) it->second.notes = notes; else assert(false); } void ConfigView::setLastRunStats(const std::vector& filePaths, const LastRunStats& lastRun) { for (const Zstring& filePath : filePaths) { auto it = cfgList_.find(filePath); assert(it != cfgList_.end()); if (it != cfgList_.end()) it->second.cfgItem.lastRunStats = lastRun; } if (sortColumn_ != ColumnTypeCfg::name) sortListView(); //needed if sorted by last sync time, or log } void ConfigView::setLastInSyncTime(const std::vector& filePaths, time_t lastRunTime) { for (const Zstring& filePath : filePaths) { auto it = cfgList_.find(filePath); assert(it != cfgList_.end()); if (it != cfgList_.end()) it->second.cfgItem.lastRunStats.startTime = lastRunTime; } if (sortColumn_ != ColumnTypeCfg::name) sortListView(); //needed if sorted by last sync time, or log } void ConfigView::setBackColor(const std::vector& filePaths, const wxColor& col, bool previewOnly) { for (const Zstring& filePath : filePaths) if (auto it = cfgList_.find(filePath); it != cfgList_.end()) { if (previewOnly) it->second.cfgItem.backColorPreview = col; else { it->second.cfgItem.backColor = col; it->second.cfgItem.backColorPreview = wxNullColour; } } else assert(false); if (!previewOnly && sortColumn_ == ColumnTypeCfg::name) sortListView(); //needed if top element of colored-group is removed } const ConfigView::Details* ConfigView::getItem(size_t row) const { if (row < cfgListView_.size()) return &cfgListView_[row]->second; return nullptr; } std::pair ConfigView::getItem(const Zstring& filePath) const { if (auto it = cfgList_.find(filePath); it != cfgList_.end()) return {&it->second, std::find(cfgListView_.begin(), cfgListView_.end(), it) - cfgListView_.begin()}; return {}; } void ConfigView::setSortDirection(ColumnTypeCfg colType, bool ascending) { sortColumn_ = colType; sortAscending_ = ascending; sortListView(); } template void ConfigView::sortListViewImpl() { const auto lessCfgName = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) { if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) return lhs->second.isLastRunCfg; //"last session" should be at top position! return LessNaturalSort()(lhs->second.name, rhs->second.name); }; const auto lessLastSync = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) { if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) return lhs->second.isLastRunCfg < rhs->second.isLastRunCfg; //"last session" label should be (always) last return makeSortDirection(std::greater(), std::bool_constant())( lhs->second.cfgItem.lastRunStats.startTime, rhs->second.cfgItem.lastRunStats.startTime); //[!] ascending lastSync shows lowest "days past" first <=> highest lastSyncTime first }; const auto lessSyncResult = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) { if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) return lhs->second.isLastRunCfg < rhs->second.isLastRunCfg; //"last session" label should be (always) last const bool haveResultL = !AFS::isNullPath(lhs->second.cfgItem.lastRunStats.logFilePath); const bool haveResultR = !AFS::isNullPath(rhs->second.cfgItem.lastRunStats.logFilePath); if (haveResultL != haveResultR) return haveResultL > haveResultR; //move sync jobs that were never run to the back //primary sort order if (haveResultL && lhs->second.cfgItem.lastRunStats.syncResult != rhs->second.cfgItem.lastRunStats.syncResult) return makeSortDirection(std::greater(), std::bool_constant())(lhs->second.cfgItem.lastRunStats.syncResult, rhs->second.cfgItem.lastRunStats.syncResult); //secondary sort order return LessNaturalSort()(lhs->second.name, rhs->second.name); }; switch (sortColumn_) { case ColumnTypeCfg::name: //pre-sort by name std::sort(cfgListView_.begin(), cfgListView_.end(), lessCfgName); //aggregate groups by color (*almost* like a std::stable_sort) for (auto it = cfgListView_.begin(); it != cfgListView_.end(); ) if ((*it)->second.cfgItem.backColor.IsOk()) it = std::stable_partition(it + 1, cfgListView_.end(), [&groupCol = (*it)->second.cfgItem.backColor](CfgFileList::iterator item) { return item->second.cfgItem.backColor == groupCol; }); else ++it; //simplify aggregation logic by not having to consider "ascending/descending" if (!ascending) std::reverse(cfgListView_.begin(), cfgListView_.end()); break; case ColumnTypeCfg::lastSync: std::sort(cfgListView_.begin(), cfgListView_.end(), lessLastSync); break; case ColumnTypeCfg::lastLog: std::sort(cfgListView_.begin(), cfgListView_.end(), lessSyncResult); break; } } void ConfigView::sortListView() { if (sortAscending_) sortListViewImpl(); else sortListViewImpl(); } //------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------- namespace { class GridDataCfg : private wxEvtHandler, public GridData { public: GridDataCfg(Grid& grid) : grid_(grid) { grid.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onMouseLeft (event); }); grid.Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onMouseLeftDouble(event); }); } ConfigView& getDataView() { return cfgView_; } static int getRowDefaultHeight(const Grid& grid) { return std::max(dipToWxsize(getMenuIconDipSize()), grid.getMainWin().GetCharHeight()) + dipToWxsize(1) /*extra space*/; } int getSyncOverdueDays() const { return syncOverdueDays_; } void setSyncOverdueDays(int syncOverdueDays) { syncOverdueDays_ = syncOverdueDays; } private: size_t getRowCount() const override { return cfgView_.getRowCount(); } static int getDaysPast(time_t last) { time_t now = std::time(nullptr); const TimeComp tcNow = getLocalTime(now); const TimeComp tcLast = getLocalTime(last); if (tcNow == TimeComp() || tcLast == TimeComp()) { assert(false); return 0; } //truncate down to midnight => incorrect during DST switches, but doesn't matter due to rounding below now -= tcNow .hour * 3600 + tcNow .minute * 60 + tcNow .second; last -= tcLast.hour * 3600 + tcLast.minute * 60 + tcLast.second; return numeric::intDivRound(now - last, 24 * 3600); } std::wstring getValue(size_t row, ColumnType colType) const override { if (const ConfigView::Details* item = cfgView_.getItem(row)) switch (static_cast(colType)) { case ColumnTypeCfg::name: return utfTo(item->name); case ColumnTypeCfg::lastSync: if (!item->isLastRunCfg && item->cfgItem.lastRunStats.startTime > 0) { //if (item->cfgItem.lastRunStats.startTime == 0) // return std::wstring(1, EN_DASH); //return utfTo(formatTime(formatDateTimeTag, getLocalTime(item->cfgItem.lastRunStats.startTime))); const int daysPast = getDaysPast(item->cfgItem.lastRunStats.startTime); return daysPast == 0 ? utfTo(formatTime(Zstr("%R") /*equivalent to "%H:%M"*/, getLocalTime(item->cfgItem.lastRunStats.startTime))) : //_("Today") : _P("1 day", "%x days", daysPast); } break; case ColumnTypeCfg::lastLog: if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) return getSyncResultLabel(item->cfgItem.lastRunStats.syncResult); break; } return std::wstring(); } void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override { if (selected) clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); //else: wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW) already the default! } enum class HoverAreaConfig { name, link, }; void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override { wxDCTextColourChanger textColor(dc); //accessibility: always set both foreground AND background colors! if (selected) textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); //else: wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT) already the default! if (const ConfigView::Details* item = cfgView_.getItem(row)) switch (static_cast(colType)) { case ColumnTypeCfg::name: { wxColor backColor = item->cfgItem.backColor; if (item->cfgItem.backColorPreview.IsOk()) backColor = item->cfgItem.backColorPreview; if (backColor.IsOk()) { wxRect rectTmp = rect; if (!selected || item->cfgItem.backColorPreview.IsOk()) { rectTmp.width = rect.width * 2 / 3; clearArea(dc, rectTmp, backColor); //accessibility: always set both foreground AND background colors! textColor.Set(relativeContrast(backColor, *wxWHITE) > relativeContrast(backColor, *wxBLACK) ? *wxWHITE : *wxBLACK); // rectTmp.x += rectTmp.width; rectTmp.width = rect.width - rectTmp.width; dc.GradientFillLinear(rectTmp, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), wxEAST); } else //always show a glimpse of the background color { rectTmp.width = getColumnGapLeft() + dipToWxsize(getMenuIconDipSize()); clearArea(dc, rectTmp, backColor); rectTmp.x += rectTmp.width; rectTmp.width = getColumnGapLeft(); dc.GradientFillLinear(rectTmp, backColor, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), wxEAST); } } if (!selected && static_cast(rowHover) == HoverAreaConfig::name) drawRectangleBorder(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT), dipToWxsize(1)); //------------------------------------------------------------------------------------- wxRect rectTmp = rect; rectTmp.x += getColumnGapLeft(); rectTmp.width -= getColumnGapLeft(); const wxImage cfgIcon = [&] { switch (item->cfgType) { case ConfigView::Details::CFG_TYPE_NONE: return wxNullImage; case ConfigView::Details::CFG_TYPE_GUI: return loadImage("start_sync", dipToScreen(getMenuIconDipSize())); case ConfigView::Details::CFG_TYPE_BATCH: return loadImage("cfg_batch", dipToScreen(getMenuIconDipSize())); } assert(false); return wxNullImage; }(); if (cfgIcon.IsOk()) drawBitmapRtlNoMirror(dc, enabled ? cfgIcon : cfgIcon.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); rectTmp.x += dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft(); rectTmp.width -= dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft(); if (!item->notes.empty()) rectTmp.width -= dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft(); drawCellText(dc, rectTmp, getValue(row, colType)); if (!item->notes.empty()) { rectTmp.x += rectTmp.width; rectTmp.width = dipToWxsize(getMenuIconDipSize()); const wxImage notesIcon = loadImage("notes", dipToScreen(getMenuIconDipSize())); drawBitmapRtlNoMirror(dc, enabled ? notesIcon : notesIcon.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); } } break; case ColumnTypeCfg::lastSync: { wxDCTextColourChanger textColor2(dc); if (syncOverdueDays_ > 0) if (getDaysPast(item->cfgItem.lastRunStats.startTime) >= syncOverdueDays_) textColor2.Set(*wxRED); //text barely readable when selected, for 4.5 contrast would need to be white :( drawCellText(dc, rect, getValue(row, colType), wxALIGN_CENTER); } break; case ColumnTypeCfg::lastLog: if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) { const wxImage statusIcon = [&] { switch (item->cfgItem.lastRunStats.syncResult) { case TaskResult::success: return loadImage("msg_success", dipToScreen(getMenuIconDipSize())); case TaskResult::warning: return loadImage("msg_warning", dipToScreen(getMenuIconDipSize())); case TaskResult::error: case TaskResult::cancelled: return loadImage("msg_error", dipToScreen(getMenuIconDipSize())); } assert(false); return wxNullImage; }(); drawBitmapRtlNoMirror(dc, enabled ? statusIcon : statusIcon.ConvertToDisabled(), rect, wxALIGN_CENTER); } if (static_cast(rowHover) == HoverAreaConfig::link) drawBitmapRtlNoMirror(dc, loadImage("file_link_16"), rect, wxALIGN_CENTER); break; } } int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override { // -> synchronize renderCell() <-> getBestSize() switch (static_cast(colType)) { case ColumnTypeCfg::name: return getColumnGapLeft() + dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth() + getColumnGapLeft(); case ColumnTypeCfg::lastSync: return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth() + getColumnGapLeft(); case ColumnTypeCfg::lastLog: return dipToWxsize(getMenuIconDipSize()); } assert(false); return 0; } HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override { if (const ConfigView::Details* item = cfgView_.getItem(row)) { switch (static_cast(colType)) { case ColumnTypeCfg::name: case ColumnTypeCfg::lastSync: //if (!item->notes.empty() && cellRelativePosX >= cellWidth - (getColumnGapLeft() + dipToWxsize(getMenuIconDipSize()) + getColumnGapLeft())) break; case ColumnTypeCfg::lastLog: if (!item->isLastRunCfg && !getNativeItemPath(item->cfgItem.lastRunStats.logFilePath).empty()) return static_cast(HoverAreaConfig::link); break; } return static_cast(HoverAreaConfig::name); } return HoverArea::none; } void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override { const auto colTypeCfg = static_cast(colType); const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted); wxRect rectRemain = rectInner; wxImage sortMarker; if (const auto [sortCol, ascending] = cfgView_.getSortDirection(); colTypeCfg == sortCol) { sortMarker = loadImage(ascending ? "sort_ascending" : "sort_descending"); if (!enabled) sortMarker = sortMarker.ConvertToDisabled(); } switch (colTypeCfg) { case ColumnTypeCfg::name: case ColumnTypeCfg::lastSync: rectRemain.x += getColumnGapLeft(); rectRemain.width -= getColumnGapLeft(); drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled); if (sortMarker.IsOk()) drawBitmapRtlNoMirror(dc, sortMarker, rectInner, wxALIGN_CENTER_HORIZONTAL); break; case ColumnTypeCfg::lastLog: { const wxImage logIcon = loadImage("log_file", dipToScreen(getMenuIconDipSize())); drawBitmapRtlNoMirror(dc, enabled ? logIcon : logIcon.ConvertToDisabled(), rectInner, wxALIGN_CENTER); if (sortMarker.IsOk()) { const int gapLeft = (rectInner.width + logIcon.GetWidth()) / 2; rectRemain.x += gapLeft; rectRemain.width -= gapLeft; drawBitmapRtlNoMirror(dc, sortMarker, rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); } } break; } } std::wstring getColumnLabel(ColumnType colType) const override { switch (static_cast(colType)) { case ColumnTypeCfg::name: return _("Name"); case ColumnTypeCfg::lastSync: return _("Last sync"); case ColumnTypeCfg::lastLog: return _("Log"); } return std::wstring(); } std::wstring getToolTip(ColumnType colType) const override { switch (static_cast(colType)) { case ColumnTypeCfg::name: case ColumnTypeCfg::lastSync: break; case ColumnTypeCfg::lastLog: return getColumnLabel(colType); } return std::wstring(); } std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override { if (const ConfigView::Details* item = cfgView_.getItem(row)) { switch (static_cast(colType)) { case ColumnTypeCfg::name: case ColumnTypeCfg::lastSync: break; case ColumnTypeCfg::lastLog: if (!item->isLastRunCfg && !AFS::isNullPath(item->cfgItem.lastRunStats.logFilePath)) { std::wstring tooltip = getSyncResultLabel(item->cfgItem.lastRunStats.syncResult) + L"\n"; if (item->cfgItem.lastRunStats.errors > 0) tooltip += TAB_SPACE + _("Errors:") + L' ' + formatNumber(item->cfgItem.lastRunStats.errors) + L"\n"; if (item->cfgItem.lastRunStats.warnings > 0) tooltip += TAB_SPACE + _("Warnings:") + L' ' + formatNumber(item->cfgItem.lastRunStats.warnings) + L"\n"; tooltip += TAB_SPACE + _("Items processed:") + L' ' + formatNumber(item->cfgItem.lastRunStats.itemsProcessed) + L" (" + formatFilesizeShort(item->cfgItem.lastRunStats.bytesProcessed) + L")\n"; const int64_t totalTimeSec = std::chrono::duration_cast(item->cfgItem.lastRunStats.totalTime).count(); tooltip += TAB_SPACE + _("Total time:") + L' ' + utfTo(formatTimeSpan(totalTimeSec)); //non-native path won't be clickable => at least show in tooltip: if (getNativeItemPath(item->cfgItem.lastRunStats.logFilePath).empty()) tooltip += L"\n" + AFS::getDisplayPath(item->cfgItem.lastRunStats.logFilePath); return tooltip; } break; } return item->notes; } return std::wstring(); } void onMouseLeft(GridClickEvent& event) { if (const ConfigView::Details* item = cfgView_.getItem(event.row_)) switch (static_cast(event.hoverArea_)) { case HoverAreaConfig::name: break; case HoverAreaConfig::link: try { if (const Zstring& nativePath = getNativeItemPath(item->cfgItem.lastRunStats.logFilePath); !nativePath.empty()) openWithDefaultApp(nativePath); //throw FileError else assert(false); //see getMouseHover() } catch (const FileError& e) { showNotificationDialog(&grid_, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } return; } event.Skip(); } void onMouseLeftDouble(GridClickEvent& event) { switch (static_cast(event.hoverArea_)) { case HoverAreaConfig::name: break; case HoverAreaConfig::link: return; //swallow event here before MainDialog considers it as a request to start comparison } event.Skip(); } private: Grid& grid_; ConfigView cfgView_; int syncOverdueDays_ = 0; }; } void cfggrid::init(Grid& grid) { const int rowHeight = GridDataCfg::getRowDefaultHeight(grid); grid.setDataProvider(std::make_shared(grid)); grid.showRowLabel(false); grid.setRowHeight(rowHeight); grid.setColumnLabelHeight(rowHeight + dipToWxsize(2)); } ConfigView& cfggrid::getDataView(Grid& grid) { if (auto* prov = dynamic_cast(grid.getDataProvider())) return prov->getDataView(); throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] cfggrid was not initialized."); } void cfggrid::addAndSelect(Grid& grid, const std::vector& filePaths, bool scrollToSelection) { getDataView(grid).addCfgFiles(filePaths); grid.Refresh(); //[!] let Grid know about changed row count *before* fiddling with selection!!! const std::set pathsSorted(filePaths.begin(), filePaths.end()); std::vector rowsToSelect; for (size_t row = 0; row < grid.getRowCount(); ++row) if (pathsSorted.contains(getDataView(grid).getItem(row)->cfgItem.cfgFilePath)) rowsToSelect.push_back(row); if (scrollToSelection && !rowsToSelect.empty()) grid.makeRowVisible(rowsToSelect[0]); //don't also set grid cursor: will confuse keyboard selection using shift and arrow keys grid.clearSelection(GridEventPolicy::deny); for (size_t row : rowsToSelect) grid.selectRow(row, GridEventPolicy::deny); } int cfggrid::getSyncOverdueDays(Grid& grid) { if (auto* prov = dynamic_cast(grid.getDataProvider())) return prov->getSyncOverdueDays(); throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] cfggrid was not initialized."); } void cfggrid::setSyncOverdueDays(Grid& grid, int syncOverdueDays) { auto* prov = dynamic_cast(grid.getDataProvider()); if (!prov) throw std::runtime_error(std::string(__FILE__) + '[' + numberTo(__LINE__) + "] cfggrid was not initialized."); prov->setSyncOverdueDays(syncOverdueDays); grid.Refresh(); }