Files
2025-12-10 14:38:26 -08:00

783 lines
30 KiB
C++

// *****************************************************************************
// * 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 <zen/time.h>
#include <zen/process_exec.h>
#include <wx+/color_tools.h>
#include <wx+/dc.h>
#include <wx+/rtl.h>
#include <wx+/image_resources.h>
#include <wx+/popup_dlg.h>
#include <wx+/std_button_layout.h>
#include <wx/settings.h>
#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<ConfigFileItem> ConfigView::get() const
{
std::map<int, ConfigFileItem, std::greater<>> 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<ConfigFileItem> cfgHistory;
for (const auto& [lastUseIndex, cfgItem] : itemsSorted)
cfgHistory.emplace_back(cfgItem);
return cfgHistory;
}
void ConfigView::set(const std::vector<ConfigFileItem>& cfgItems)
{
std::vector<Zstring> 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<Zstring>& filePaths)
{
addCfgFilesImpl(filePaths);
sortListView();
}
void ConfigView::addCfgFilesImpl(const std::vector<Zstring>& 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<Zstring>(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<Zstring>& 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<Zstring>& 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<Zstring>& 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<Zstring>& 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<const ConfigView::Details*, size_t/*row*/> 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 <bool ascending>
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<ascending>())(
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<ascending>())(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<true>();
else
sortListViewImpl<false>();
}
//-------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------
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<ColumnTypeCfg>(colType))
{
case ColumnTypeCfg::name:
return utfTo<std::wstring>(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<std::wstring>(formatTime(formatDateTimeTag, getLocalTime(item->cfgItem.lastRunStats.startTime)));
const int daysPast = getDaysPast(item->cfgItem.lastRunStats.startTime);
return daysPast == 0 ?
utfTo<std::wstring>(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<ColumnTypeCfg>(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<HoverAreaConfig>(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<HoverAreaConfig>(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<ColumnTypeCfg>(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<ColumnTypeCfg>(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<HoverArea>(HoverAreaConfig::link);
break;
}
return static_cast<HoverArea>(HoverAreaConfig::name);
}
return HoverArea::none;
}
void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override
{
const auto colTypeCfg = static_cast<ColumnTypeCfg>(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<ColumnTypeCfg>(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<ColumnTypeCfg>(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<ColumnTypeCfg>(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<std::chrono::seconds>(item->cfgItem.lastRunStats.totalTime).count();
tooltip += TAB_SPACE + _("Total time:") + L' ' + utfTo<std::wstring>(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<HoverAreaConfig>(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<HoverAreaConfig>(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<GridDataCfg>(grid));
grid.showRowLabel(false);
grid.setRowHeight(rowHeight);
grid.setColumnLabelHeight(rowHeight + dipToWxsize(2));
}
ConfigView& cfggrid::getDataView(Grid& grid)
{
if (auto* prov = dynamic_cast<GridDataCfg*>(grid.getDataProvider()))
return prov->getDataView();
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] cfggrid was not initialized.");
}
void cfggrid::addAndSelect(Grid& grid, const std::vector<Zstring>& filePaths, bool scrollToSelection)
{
getDataView(grid).addCfgFiles(filePaths);
grid.Refresh(); //[!] let Grid know about changed row count *before* fiddling with selection!!!
const std::set<Zstring, LessNativePath> pathsSorted(filePaths.begin(), filePaths.end());
std::vector<size_t> 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<GridDataCfg*>(grid.getDataProvider()))
return prov->getSyncOverdueDays();
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] cfggrid was not initialized.");
}
void cfggrid::setSyncOverdueDays(Grid& grid, int syncOverdueDays)
{
auto* prov = dynamic_cast<GridDataCfg*>(grid.getDataProvider());
if (!prov)
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] cfggrid was not initialized.");
prov->setSyncOverdueDays(syncOverdueDays);
grid.Refresh();
}