783 lines
30 KiB
C++
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();
|
|
}
|