1860 lines
84 KiB
C++
1860 lines
84 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 "sync_cfg.h"
|
|
#include <memory>
|
|
#include <zen/http.h>
|
|
#include <wx/wupdlock.h>
|
|
#include <wx/valtext.h>
|
|
#include <wx+/rtl.h>
|
|
#include <wx+/no_flicker.h>
|
|
#include <wx+/context_menu.h>
|
|
#include <wx+/choice_enum.h>
|
|
#include <wx+/image_tools.h>
|
|
#include <wx+/window_layout.h>
|
|
#include <wx+/popup_dlg.h>
|
|
#include <wx+/image_resources.h>
|
|
#include "gui_generated.h"
|
|
#include "folder_selector.h"
|
|
#include "../base/norm_filter.h"
|
|
#include "../base/file_hierarchy.h"
|
|
#include "../base/icon_loader.h"
|
|
#include "../afs/concrete.h"
|
|
#include "../base_tools.h"
|
|
|
|
|
|
|
|
using namespace zen;
|
|
using namespace fff;
|
|
|
|
|
|
namespace
|
|
{
|
|
const int CFG_DESCRIPTION_WIDTH_DIP = 250;
|
|
const wchar_t arrowRight[] = L"\u2192"; //"RIGHTWARDS ARROW"
|
|
|
|
|
|
void initBitmapRadioButtons(const std::vector<std::pair<ToggleButton*, std::string /*imgName*/>>& buttons, bool alignLeft)
|
|
{
|
|
const bool physicalLeft = alignLeft == (wxTheApp->GetLayoutDirection() != wxLayout_RightToLeft);
|
|
|
|
auto generateSelectImage = [physicalLeft](wxButton& btn, const std::string& imgName, bool selected)
|
|
{
|
|
wxImage imgTxt = createImageFromText(btn.GetLabelText(), btn.GetFont(),
|
|
selected ? *wxBLACK : //accessibility: always set both foreground AND background colors! see renderSelectedButton()
|
|
btn.GetForegroundColour());
|
|
|
|
wxImage imgIco = mirrorIfRtl(loadImage(imgName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize())));
|
|
|
|
if (imgName == "delete_recycler") //use system icon if available (can fail on Linux??)
|
|
try { imgIco = extractWxImage(fff::getTrashIcon(dipToScreen(getMenuIconDipSize()))); /*throw SysError*/ }
|
|
catch (SysError&) { assert(false); }
|
|
|
|
if (!selected)
|
|
imgIco = greyScale(imgIco);
|
|
|
|
wxImage imgStack = physicalLeft ?
|
|
stackImages(imgIco, imgTxt, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) :
|
|
stackImages(imgTxt, imgIco, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5));
|
|
|
|
return resizeCanvas(imgStack, imgStack.GetSize() + wxSize(dipToScreen(14), dipToScreen(12)), wxALIGN_CENTER);
|
|
};
|
|
|
|
wxSize maxExtent;
|
|
std::unordered_map<const ToggleButton*, wxImage> labelsNotSel;
|
|
for (auto& [btn, imgName] : buttons)
|
|
{
|
|
wxImage img = generateSelectImage(*btn, imgName, false /*selected*/);
|
|
maxExtent.x = std::max(maxExtent.x, img.GetWidth());
|
|
maxExtent.y = std::max(maxExtent.y, img.GetHeight());
|
|
|
|
labelsNotSel[btn] = std::move(img);
|
|
}
|
|
|
|
for (auto& [btn, imgName] : buttons)
|
|
{
|
|
btn->init(layOver(rectangleImage(maxExtent, getColorToggleButtonFill(), getColorToggleButtonBorder(), dipToScreen(1)),
|
|
generateSelectImage(*btn, imgName, true /*selected*/), wxALIGN_CENTER_VERTICAL | (physicalLeft ? wxALIGN_LEFT : wxALIGN_RIGHT)),
|
|
resizeCanvas(labelsNotSel[btn], maxExtent, wxALIGN_CENTER_VERTICAL | (physicalLeft ? wxALIGN_LEFT : wxALIGN_RIGHT)));
|
|
|
|
btn->SetMinSize({screenToWxsize(maxExtent.x),
|
|
screenToWxsize(maxExtent.y)}); //get rid of selection border on Windows + macOS :)
|
|
//SetMinSize() instead of SetSize() is needed here for wxWindows layout determination to work correctly
|
|
}
|
|
}
|
|
|
|
|
|
bool sanitizeFilter(FilterConfig& filterCfg, const std::vector<AbstractPath>& baseFolderPaths, wxWindow* parent)
|
|
{
|
|
//include filter must not be empty!
|
|
if (trimCpy(filterCfg.includeFilter).empty())
|
|
filterCfg.includeFilter = FilterConfig().includeFilter; //no need to show error message, just correct user input
|
|
|
|
|
|
//replace full paths by relative ones: frequent user error => help out: https://freefilesync.org/forum/viewtopic.php?t=9225
|
|
auto normalizeForSearch = [](Zstring str)
|
|
{
|
|
//1. ignore Unicode normalization form 2. ignore case 3. normalize path separator
|
|
str = getUpperCase(str); //getUnicodeNormalForm() is implied by getUpperCase()
|
|
|
|
if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) std::replace(str.begin(), str.end(), Zstr('/'), FILE_NAME_SEPARATOR);
|
|
if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) std::replace(str.begin(), str.end(), Zstr('\\'), FILE_NAME_SEPARATOR);
|
|
|
|
return str;
|
|
};
|
|
|
|
std::vector<Zstring> folderPathsPf; //normalized + postfix path separator
|
|
{
|
|
const Zstring includeFilterNorm = normalizeForSearch(filterCfg.includeFilter);
|
|
const Zstring excludeFilterNorm = normalizeForSearch(filterCfg.excludeFilter);
|
|
|
|
for (const AbstractPath& folderPath : baseFolderPaths)
|
|
if (!AFS::isNullPath(folderPath))
|
|
if (const std::wstring& displayPath = AFS::getDisplayPath(folderPath);
|
|
!displayPath.empty())
|
|
if (displayPath != L"/") //Linux/macOS: https://freefilesync.org/forum/viewtopic.php?t=9713
|
|
if (const Zstring pathNormPf = appendSeparator(normalizeForSearch(utfTo<Zstring>(displayPath)));
|
|
contains(includeFilterNorm, pathNormPf) || //perf!?
|
|
contains(excludeFilterNorm, pathNormPf)) //
|
|
folderPathsPf.push_back(pathNormPf);
|
|
|
|
removeDuplicates(folderPathsPf);
|
|
}
|
|
|
|
|
|
std::vector<std::pair<Zstring /*from*/, Zstring /*to*/>> replacements;
|
|
|
|
auto replaceFullPaths = [&](Zstring& filterPhrase)
|
|
{
|
|
Zstring filterPhraseNew;
|
|
const Zchar* itFilterOrig = filterPhrase.begin();
|
|
|
|
split2(filterPhrase, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n'); }, //delimiters
|
|
[&](const ZstringView phrase)
|
|
{
|
|
const ZstringView phraseTrm = trimCpy(phrase);
|
|
if (!phraseTrm.empty())
|
|
{
|
|
const Zstring phraseNorm = normalizeForSearch(Zstring{phraseTrm});
|
|
|
|
for (const Zstring& pathNormPf : folderPathsPf)
|
|
if (startsWith(phraseNorm, pathNormPf))
|
|
{
|
|
//emulate a "normalized afterFirst()":
|
|
ptrdiff_t sepCount = std::count(pathNormPf.begin(), pathNormPf.end(), FILE_NAME_SEPARATOR);
|
|
assert(sepCount > 0);
|
|
|
|
for (auto it = phraseTrm.begin(); it != phraseTrm.end(); ++it)
|
|
if (*it == Zstr('/') ||
|
|
*it == Zstr('\\'))
|
|
if (--sepCount == 0)
|
|
{
|
|
const Zstring relPath(it, phraseTrm.end()); //include first path separator
|
|
|
|
filterPhraseNew.append(itFilterOrig, phraseTrm.data());
|
|
filterPhraseNew += relPath;
|
|
itFilterOrig = phraseTrm.data() + phraseTrm.size();
|
|
|
|
replacements.emplace_back(phraseTrm, relPath);
|
|
return; //... to next block
|
|
}
|
|
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
|
|
}
|
|
}
|
|
});
|
|
|
|
if (itFilterOrig != filterPhrase.begin()) //perf!?
|
|
{
|
|
filterPhraseNew.append(itFilterOrig, filterPhrase.cend());
|
|
filterPhrase = std::move(filterPhraseNew);
|
|
}
|
|
};
|
|
replaceFullPaths(filterCfg.includeFilter);
|
|
replaceFullPaths(filterCfg.excludeFilter);
|
|
|
|
if (!replacements.empty())
|
|
{
|
|
std::wstring detailsMsg;
|
|
for (const auto& [from, to] : replacements)
|
|
if (to.empty())
|
|
detailsMsg += _("Remove:") + L' ' + utfTo<std::wstring>(from) + L'\n';
|
|
else
|
|
detailsMsg += utfTo<std::wstring>(from) + L' ' + arrowRight + L' ' + utfTo<std::wstring>(to) + L'\n';
|
|
detailsMsg.pop_back();
|
|
|
|
switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg().
|
|
setMainInstructions(_("Each filter item must be a path relative to the selected folder pairs. The following changes are suggested:")).
|
|
setDetailInstructions(detailsMsg), _("&Change")))
|
|
{
|
|
case ConfirmationButton::accept: //change
|
|
break;
|
|
|
|
case ConfirmationButton::cancel:
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//==========================================================================
|
|
|
|
class ConfigDialog : public ConfigDlgGenerated
|
|
{
|
|
public:
|
|
ConfigDialog(wxWindow* parent,
|
|
SyncConfigPanel panelToShow,
|
|
int localPairIndexToShow, bool showMultipleCfgs,
|
|
GlobalPairConfig& globalPairCfg,
|
|
std::vector<LocalPairConfig>& localPairCfg,
|
|
FilterConfig& defaultFilter,
|
|
std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected,
|
|
std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase,
|
|
size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected,
|
|
std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
|
|
std::vector<Zstring>& commandHistory, size_t commandHistoryMax);
|
|
|
|
~ConfigDialog();
|
|
|
|
private:
|
|
void onOkay (wxCommandEvent& event) override;
|
|
void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
|
|
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
|
|
|
|
void onAddNotes(wxCommandEvent& event) override;
|
|
|
|
void onLocalKeyEvent(wxKeyEvent& event);
|
|
void onListBoxKeyEvent(wxKeyEvent& event) override;
|
|
void onSelectFolderPair(wxCommandEvent& event) override;
|
|
|
|
enum class ConfigTypeImage
|
|
{
|
|
compare = 0, //used as zero-based wxImageList index!
|
|
compareGrey,
|
|
filter,
|
|
filterGrey,
|
|
sync,
|
|
syncGrey,
|
|
};
|
|
|
|
//------------- comparison panel ----------------------
|
|
void onToggleLocalCompSettings(wxCommandEvent& event) override { updateCompGui(); updateSyncGui(); /*affects sync settings, too!*/ }
|
|
void onToggleIgnoreErrors (wxCommandEvent& event) override { updateMiscGui(); }
|
|
void onToggleAutoRetry (wxCommandEvent& event) override { updateMiscGui(); }
|
|
|
|
void onCompByTimeSize (wxCommandEvent& event) override { localCmpVar_ = CompareVariant::timeSize; updateCompGui(); updateSyncGui(); } //
|
|
void onCompByContent (wxCommandEvent& event) override { localCmpVar_ = CompareVariant::content; updateCompGui(); updateSyncGui(); } //affects sync settings, too!
|
|
void onCompBySize (wxCommandEvent& event) override { localCmpVar_ = CompareVariant::size; updateCompGui(); updateSyncGui(); } //
|
|
void onCompByTimeSizeDouble(wxMouseEvent& event) override;
|
|
void onCompByContentDouble (wxMouseEvent& event) override;
|
|
void onCompBySizeDouble (wxMouseEvent& event) override;
|
|
void onChangeCompOption (wxCommandEvent& event) override { updateCompGui(); }
|
|
|
|
std::optional<CompConfig> getCompConfig() const;
|
|
void setCompConfig(const CompConfig* compCfg);
|
|
|
|
void updateCompGui();
|
|
|
|
CompareVariant localCmpVar_ = CompareVariant::timeSize;
|
|
|
|
std::set<AfsDevice> devicesForEdit_; //helper data for deviceParallelOps
|
|
std::map<AfsDevice, size_t> deviceParallelOps_; //
|
|
|
|
//------------- filter panel --------------------------
|
|
void onChangeFilterOption(wxCommandEvent& event) override { updateFilterGui(); }
|
|
void onFilterClear (wxCommandEvent& event) override { setFilterConfig(FilterConfig()); }
|
|
void onFilterDefault (wxCommandEvent& event) override { setFilterConfig(defaultFilterOut_); }
|
|
|
|
void onFilterDefaultContext (wxCommandEvent& event) override { onFilterDefaultContext(static_cast<wxEvent&>(event)); }
|
|
void onFilterDefaultContextMouse(wxMouseEvent& event) override { onFilterDefaultContext(static_cast<wxEvent&>(event)); }
|
|
void onFilterDefaultContext(wxEvent& event);
|
|
|
|
FilterConfig getFilterConfig() const;
|
|
void setFilterConfig(const FilterConfig& filter);
|
|
|
|
void updateFilterGui();
|
|
|
|
EnumDescrList<UnitTime> enumTimeDescr_
|
|
{
|
|
*m_choiceUnitTimespan,
|
|
{
|
|
{UnitTime::none, L'(' + _("None") + L')', {}}, //meta options should be enclosed in parentheses
|
|
{UnitTime::today, _("Today"), {}},
|
|
//{UnitTime::THIS_WEEK, _("This week"), {}},
|
|
{UnitTime::thisMonth, _("This month"), {}},
|
|
{UnitTime::thisYear, _("This year"), {}},
|
|
{UnitTime::lastDays, _("Last x days:"), {}},
|
|
}
|
|
};
|
|
EnumDescrList<UnitSize> enumMinSizeDescr_
|
|
{
|
|
*m_choiceUnitMinSize,
|
|
{
|
|
{UnitSize::none, L'(' + _("None") + L')', {}}, //meta options should be enclosed in parentheses
|
|
{UnitSize::byte, _("Byte"), {}},
|
|
{UnitSize::kb, _("KB"), {}},
|
|
{UnitSize::mb, _("MB"), {}},
|
|
}
|
|
};
|
|
|
|
EnumDescrList<UnitSize> enumMaxSizeDescr_{*m_choiceUnitMaxSize, enumMinSizeDescr_.getConfig()};
|
|
|
|
//------------- synchronization panel -----------------
|
|
void onSyncTwoWay(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::twoWay); updateSyncGui(); }
|
|
void onSyncMirror(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::mirror); updateSyncGui(); }
|
|
void onSyncUpdate(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::update); updateSyncGui(); }
|
|
void onSyncCustom(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::custom); updateSyncGui(); }
|
|
|
|
void onToggleLocalSyncSettings(wxCommandEvent& event) override { updateSyncGui(); }
|
|
void onToggleUseDatabase (wxCommandEvent& event) override;
|
|
void onChangeVersioningStyle (wxCommandEvent& event) override { updateSyncGui(); }
|
|
void onToggleVersioningLimit (wxCommandEvent& event) override { updateSyncGui(); }
|
|
|
|
void onSyncTwoWayDouble(wxMouseEvent& event) override;
|
|
void onSyncMirrorDouble(wxMouseEvent& event) override;
|
|
void onSyncUpdateDouble(wxMouseEvent& event) override;
|
|
void onSyncCustomDouble(wxMouseEvent& event) override;
|
|
|
|
void onLeftOnly (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByDiff::leftOnly); }
|
|
void onRightOnly (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByDiff::rightOnly); }
|
|
void onLeftNewer (wxCommandEvent& event) override;
|
|
void onRightNewer(wxCommandEvent& event) override;
|
|
void onDifferent (wxCommandEvent& event) override;
|
|
void toggleSyncDirButton(SyncDirection DirectionByDiff::* dir);
|
|
|
|
void onLeftCreate (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::create); }
|
|
void onLeftUpdate (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::update); }
|
|
void onLeftDelete (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::delete_); }
|
|
void onRightCreate(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::create); }
|
|
void onRightUpdate(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::update); }
|
|
void onRightDelete(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::delete_); }
|
|
void toggleSyncDirButton(DirectionByChange::Changes DirectionByChange::* side, SyncDirection DirectionByChange::Changes::* dir);
|
|
|
|
void onDeletionPermanent (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::permanent; updateSyncGui(); }
|
|
void onDeletionRecycler (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::recycler; updateSyncGui(); }
|
|
void onDeletionVersioning(wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::versioning; updateSyncGui(); }
|
|
|
|
void onToggleMiscOption(wxCommandEvent& event) override { updateMiscGui(); }
|
|
void onToggleMiscEmail (wxCommandEvent& event) override
|
|
{
|
|
onToggleMiscOption(event);
|
|
if (event.IsChecked()) //optimize UX
|
|
m_comboBoxEmail->SetFocus(); //
|
|
}
|
|
void onEmailAlways (wxCommandEvent& event) override { emailNotifyCondition_ = ResultsNotification::always; updateMiscGui(); }
|
|
void onEmailErrorWarning(wxCommandEvent& event) override { emailNotifyCondition_ = ResultsNotification::errorWarning; updateMiscGui(); }
|
|
void onEmailErrorOnly (wxCommandEvent& event) override { emailNotifyCondition_ = ResultsNotification::errorOnly; updateMiscGui(); }
|
|
|
|
void onShowLogFolder(wxCommandEvent& event) override;
|
|
|
|
std::optional<SyncConfig> getSyncConfig() const;
|
|
void setSyncConfig(const SyncConfig* syncCfg);
|
|
|
|
bool leftRightNewerCombined() const;
|
|
|
|
void updateSyncGui();
|
|
//-----------------------------------------------------
|
|
|
|
//parameters with ownership NOT within GUI controls!
|
|
SyncDirectionConfig directionsCfg_;
|
|
DeletionVariant deletionVariant_ = DeletionVariant::recycler; //use Recycler, delete permanently or move to user-defined location
|
|
|
|
const std::function<size_t(const Zstring& folderPathPhrase)> getDeviceParallelOps_;
|
|
const std::function<void (const Zstring& folderPathPhrase, size_t parallelOps)> setDeviceParallelOps_;
|
|
|
|
FolderSelector versioningFolder_;
|
|
EnumDescrList<VersioningStyle> enumVersioningStyle_
|
|
{
|
|
*m_choiceVersioningStyle,
|
|
{
|
|
{VersioningStyle::replace, _("Replace"), _("Move files and replace if existing")},
|
|
{VersioningStyle::timestampFolder, _("Time stamp") + L" [" + _("Folder") + L']', _("Move files into a time-stamped subfolder")},
|
|
{VersioningStyle::timestampFile, _("Time stamp") + L" [" + _("File") + L']', _("Append a time stamp to each file name")},
|
|
}
|
|
};
|
|
|
|
ResultsNotification emailNotifyCondition_ = ResultsNotification::always;
|
|
|
|
EnumDescrList<PostSyncCondition> enumPostSyncCondition_
|
|
{
|
|
*m_choicePostSyncCondition,
|
|
{
|
|
{PostSyncCondition::completion, _("On completion:"), {}},
|
|
{PostSyncCondition::errors, _("On errors:"), {}},
|
|
{PostSyncCondition::success, _("On success:"), {}},
|
|
}
|
|
};
|
|
|
|
FolderSelector logFolderSelector_;
|
|
//-----------------------------------------------------
|
|
|
|
MiscSyncConfig getMiscSyncOptions() const;
|
|
void setMiscSyncOptions(const MiscSyncConfig& miscCfg);
|
|
|
|
void updateMiscGui();
|
|
|
|
//-----------------------------------------------------
|
|
|
|
void selectFolderPairConfig(int newPairIndexToShow);
|
|
bool unselectFolderPairConfig(bool validateParams); //returns false on error: shows message box!
|
|
|
|
//output parameters (sync config)
|
|
GlobalPairConfig& globalPairCfgOut_;
|
|
std::vector<LocalPairConfig>& localPairCfgOut_;
|
|
//output parameters (global) -> ignores OK/Cancel
|
|
FilterConfig& defaultFilterOut_;
|
|
std::vector<Zstring>& versioningFolderHistoryOut_;
|
|
std::vector<Zstring>& logFolderHistoryOut_;
|
|
std::vector<Zstring>& emailHistoryOut_;
|
|
std::vector<Zstring>& commandHistoryOut_;
|
|
|
|
//working copy of ALL config parameters: only one folder pair is selected at a time!
|
|
GlobalPairConfig globalPairCfg_;
|
|
std::vector<LocalPairConfig> localPairCfg_;
|
|
|
|
int selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED;
|
|
static constexpr int EMPTY_PAIR_INDEX_SELECTED = -2;
|
|
|
|
bool showNotesPanel_ = false;
|
|
|
|
const bool enableExtraFeatures_;
|
|
const bool showMultipleCfgs_;
|
|
|
|
const Zstring globalLogFolderPhrase_;
|
|
};
|
|
|
|
//#################################################################################################################
|
|
|
|
std::wstring getCompVariantDescription(CompareVariant var)
|
|
{
|
|
switch (var)
|
|
{
|
|
case CompareVariant::timeSize:
|
|
return _("Identify equal files by comparing modification time and size.");
|
|
case CompareVariant::content:
|
|
return _("Identify equal files by comparing the file content.");
|
|
case CompareVariant::size:
|
|
return _("Identify equal files by comparing their file size.");
|
|
}
|
|
assert(false);
|
|
return _("Error");
|
|
}
|
|
|
|
|
|
std::wstring getSyncVariantDescription(SyncVariant var)
|
|
{
|
|
switch (var)
|
|
{
|
|
case SyncVariant::twoWay:
|
|
return _("Identify and propagate changes on both sides. Deletions, moves and conflicts are detected automatically using a database.");
|
|
case SyncVariant::mirror:
|
|
return _("Create a mirror backup of the left folder by adapting the right folder to match.");
|
|
case SyncVariant::update:
|
|
return _("Copy new and updated files to the right folder.");
|
|
case SyncVariant::custom:
|
|
return _("Configure your own synchronization rules.");
|
|
}
|
|
assert(false);
|
|
return _("Error");
|
|
}
|
|
|
|
//==========================================================================
|
|
|
|
ConfigDialog::ConfigDialog(wxWindow* parent,
|
|
SyncConfigPanel panelToShow,
|
|
int localPairIndexToShow, bool showMultipleCfgs,
|
|
GlobalPairConfig& globalPairCfg,
|
|
std::vector<LocalPairConfig>& localPairCfg,
|
|
FilterConfig& defaultFilter,
|
|
std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected,
|
|
std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected,
|
|
const Zstring& globalLogFolderPhrase,
|
|
size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected,
|
|
std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
|
|
std::vector<Zstring>& commandHistory, size_t commandHistoryMax) :
|
|
ConfigDlgGenerated(parent),
|
|
|
|
getDeviceParallelOps_([this](const Zstring& folderPathPhrase)
|
|
{
|
|
assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size());
|
|
const auto& deviceParallelOps = selectedPairIndexToShow_ < 0 ? getMiscSyncOptions().deviceParallelOps : globalPairCfg_.miscCfg.deviceParallelOps; //ternary-WTF!
|
|
|
|
return getDeviceParallelOps(deviceParallelOps, folderPathPhrase);
|
|
}),
|
|
|
|
setDeviceParallelOps_([this](const Zstring& folderPathPhrase, size_t parallelOps) //setDeviceParallelOps()
|
|
{
|
|
assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size());
|
|
if (selectedPairIndexToShow_ < 0)
|
|
{
|
|
MiscSyncConfig miscCfg = getMiscSyncOptions();
|
|
setDeviceParallelOps(miscCfg.deviceParallelOps, folderPathPhrase, parallelOps);
|
|
setMiscSyncOptions(miscCfg);
|
|
}
|
|
else
|
|
setDeviceParallelOps(globalPairCfg_.miscCfg.deviceParallelOps, folderPathPhrase, parallelOps);
|
|
}),
|
|
|
|
versioningFolder_(this, *m_panelVersioning, *m_buttonSelectVersioningFolder, *m_bpButtonSelectVersioningAltFolder, *m_versioningFolderPath, versioningFolderLastSelected, sftpKeyFileLastSelected,
|
|
nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, getDeviceParallelOps_, setDeviceParallelOps_),
|
|
|
|
logFolderSelector_(this, *m_panelLogfile, *m_buttonSelectLogFolder, *m_bpButtonSelectAltLogFolder, *m_logFolderPath, logFolderLastSelected, sftpKeyFileLastSelected,
|
|
nullptr /*staticText*/, nullptr /*dropWindow2*/, nullptr /*droppedPathsFilter*/, getDeviceParallelOps_, setDeviceParallelOps_),
|
|
|
|
globalPairCfgOut_(globalPairCfg),
|
|
localPairCfgOut_(localPairCfg),
|
|
defaultFilterOut_(defaultFilter),
|
|
versioningFolderHistoryOut_(versioningFolderHistory),
|
|
logFolderHistoryOut_(logFolderHistory),
|
|
emailHistoryOut_(emailHistory),
|
|
commandHistoryOut_(commandHistory),
|
|
globalPairCfg_(globalPairCfg),
|
|
localPairCfg_(localPairCfg),
|
|
showNotesPanel_(!globalPairCfg.miscCfg.notes.empty()),
|
|
enableExtraFeatures_(false),
|
|
showMultipleCfgs_(showMultipleCfgs),
|
|
globalLogFolderPhrase_(globalLogFolderPhrase)
|
|
{
|
|
assert(!AFS::isNullPath(createAbstractPath(globalLogFolderPhrase_)));
|
|
|
|
setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel));
|
|
|
|
|
|
setBitmapTextLabel(*m_buttonAddNotes, loadImage("notes", dipToScreen(16)), m_buttonAddNotes->GetLabelText());
|
|
|
|
setImage(*m_bitmapNotes, loadImage("notes", dipToScreen(20)));
|
|
|
|
//set reasonable default height for notes: simplistic algorithm neglecting line-wrap!
|
|
int notesRows = 1;
|
|
for (wchar_t c : trimCpy(globalPairCfg.miscCfg.notes))
|
|
if (c == L'\n')
|
|
++notesRows;
|
|
|
|
double visibleRows = 5;
|
|
if (showNotesPanel_)
|
|
visibleRows = notesRows <= 10 ? notesRows : 10.5; //add half a row as visual hint
|
|
m_textCtrNotes->SetMinSize({-1, getTextCtrlHeight(*m_textCtrNotes, visibleRows)});
|
|
|
|
|
|
m_notebook->SetPadding(wxSize(dipToWxsize(2), 0)); //height cannot be changed
|
|
|
|
//fill image list to cope with wxNotebook image setting design desaster...
|
|
const int imgListSize = dipToWxsize(16); //also required by GTK => don't use getMenuIconDipSize()
|
|
auto imgList = std::make_unique<wxImageList>(imgListSize, imgListSize);
|
|
|
|
auto addToImageList = [&](const wxImage& img)
|
|
{
|
|
imgList->Add(toScaledBitmap(img));
|
|
imgList->Add(toScaledBitmap(greyScale(img)));
|
|
};
|
|
//add images in same sequence like ConfigTypeImage enum!!!
|
|
addToImageList(loadImage("options_compare", wxsizeToScreen(imgListSize)));
|
|
addToImageList(loadImage("options_filter", wxsizeToScreen(imgListSize)));
|
|
addToImageList(loadImage("options_sync", wxsizeToScreen(imgListSize)));
|
|
assert(imgList->GetImageCount() == static_cast<int>(ConfigTypeImage::syncGrey) + 1);
|
|
|
|
m_notebook->AssignImageList(imgList.release()); //pass ownership
|
|
|
|
|
|
m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::compare), _("Comparison") + L" (F6)");
|
|
m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::filter ), _("Filter") + L" (F7)");
|
|
m_notebook->SetPageText(static_cast<size_t>(SyncConfigPanel::sync ), _("Synchronization") + L" (F8)");
|
|
|
|
m_notebook->ChangeSelection(static_cast<size_t>(panelToShow));
|
|
|
|
//------------- comparison panel ----------------------
|
|
setRelativeFontSize(*m_buttonByTimeSize, 1.25);
|
|
setRelativeFontSize(*m_buttonByContent, 1.25);
|
|
setRelativeFontSize(*m_buttonBySize, 1.25);
|
|
|
|
initBitmapRadioButtons(
|
|
{
|
|
{m_buttonByTimeSize, "cmp_time" },
|
|
{m_buttonByContent, "cmp_content"},
|
|
{m_buttonBySize, "cmp_size" },
|
|
}, true /*alignLeft*/);
|
|
|
|
m_buttonByTimeSize->SetToolTip(getCompVariantDescription(CompareVariant::timeSize));
|
|
m_buttonByContent ->SetToolTip(getCompVariantDescription(CompareVariant::content));
|
|
m_buttonBySize ->SetToolTip(getCompVariantDescription(CompareVariant::size));
|
|
|
|
m_staticTextCompVarDescription->SetMinSize({dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP), -1});
|
|
|
|
m_scrolledWindowPerf->SetMinSize({dipToWxsize(220), -1});
|
|
setImage(*m_bitmapPerf, greyScaleIfDisabled(loadImage("speed"), enableExtraFeatures_));
|
|
|
|
const int scrollDelta = GetCharHeight();
|
|
m_scrolledWindowPerf->SetScrollRate(scrollDelta, scrollDelta);
|
|
|
|
setDefaultWidth(*m_spinCtrlAutoRetryCount);
|
|
setDefaultWidth(*m_spinCtrlAutoRetryDelay);
|
|
|
|
//ignore invalid input for time shift control:
|
|
wxTextValidator inputValidator(wxFILTER_DIGITS | wxFILTER_INCLUDE_CHAR_LIST);
|
|
inputValidator.SetCharIncludes(L"+-;,: ");
|
|
m_textCtrlTimeShift->SetValidator(inputValidator);
|
|
|
|
//------------- filter panel --------------------------
|
|
m_textCtrlInclude->SetMinSize({dipToWxsize(280), -1});
|
|
|
|
assert(!contains(m_buttonClear->GetLabel(), L"&C") && !contains(m_buttonClear->GetLabel(), L"&c")); //gazillionth wxWidgets bug on OS X: Command + C mistakenly hits "&C" access key!
|
|
|
|
setDefaultWidth(*m_spinCtrlMinSize);
|
|
setDefaultWidth(*m_spinCtrlMaxSize);
|
|
setDefaultWidth(*m_spinCtrlTimespan);
|
|
|
|
setImage(*m_bpButtonDefaultContext, mirrorIfRtl(loadImage("button_arrow_right")));
|
|
|
|
//------------- synchronization panel -----------------
|
|
m_buttonTwoWay->SetToolTip(getSyncVariantDescription(SyncVariant::twoWay));
|
|
m_buttonMirror->SetToolTip(getSyncVariantDescription(SyncVariant::mirror));
|
|
m_buttonUpdate->SetToolTip(getSyncVariantDescription(SyncVariant::update));
|
|
m_buttonCustom->SetToolTip(getSyncVariantDescription(SyncVariant::custom));
|
|
|
|
const int catSizeMax = loadImage("cat_left_only").GetWidth() * 8 / 10;
|
|
setImage(*m_bitmapLeftOnly, mirrorIfRtl(greyScale(loadImage("cat_left_only", catSizeMax))));
|
|
setImage(*m_bitmapRightOnly, mirrorIfRtl(greyScale(loadImage("cat_right_only", catSizeMax))));
|
|
setImage(*m_bitmapLeftNewer, mirrorIfRtl(greyScale(loadImage("cat_left_newer", catSizeMax))));
|
|
setImage(*m_bitmapRightNewer, mirrorIfRtl(greyScale(loadImage("cat_right_newer", catSizeMax))));
|
|
setImage(*m_bitmapDifferent, mirrorIfRtl(greyScale(loadImage("cat_different", catSizeMax))));
|
|
|
|
setRelativeFontSize(*m_buttonTwoWay, 1.25);
|
|
setRelativeFontSize(*m_buttonMirror, 1.25);
|
|
setRelativeFontSize(*m_buttonUpdate, 1.25);
|
|
setRelativeFontSize(*m_buttonCustom, 1.25);
|
|
|
|
initBitmapRadioButtons(
|
|
{
|
|
{m_buttonTwoWay, "sync_twoway"},
|
|
{m_buttonMirror, "sync_mirror"},
|
|
{m_buttonUpdate, "sync_update"},
|
|
{m_buttonCustom, "sync_custom"},
|
|
}, false /*alignLeft*/);
|
|
|
|
m_staticTextSyncVarDescription->SetMinSize({dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP), -1});
|
|
|
|
m_buttonRecycler ->SetToolTip(_("Retain deleted and overwritten files in the recycle bin"));
|
|
m_buttonPermanent ->SetToolTip(_("Delete and overwrite files permanently"));
|
|
m_buttonVersioning->SetToolTip(_("Move files to a user-defined folder"));
|
|
|
|
initBitmapRadioButtons(
|
|
{
|
|
{m_buttonRecycler, "delete_recycler" },
|
|
{m_buttonPermanent, "delete_permanently"},
|
|
{m_buttonVersioning, "delete_versioning" },
|
|
}, true /*alignLeft*/);
|
|
|
|
setDefaultWidth(*m_spinCtrlVersionMaxDays );
|
|
setDefaultWidth(*m_spinCtrlVersionCountMin);
|
|
setDefaultWidth(*m_spinCtrlVersionCountMax);
|
|
|
|
m_versioningFolderPath->setHistory(std::make_shared<HistoryList>(versioningFolderHistory, folderHistoryMax));
|
|
|
|
|
|
const wxImage imgFileManagerSmall_([]
|
|
{
|
|
try { return extractWxImage(fff::getFileManagerIcon(dipToScreen(20))); /*throw SysError*/ }
|
|
catch (SysError&) { assert(false); return loadImage("file_manager", dipToScreen(20)); }
|
|
}());
|
|
setImage(*m_bpButtonShowLogFolder, imgFileManagerSmall_);
|
|
m_bpButtonShowLogFolder->SetToolTip(translate(extCommandFileManager.description));//translate default external apps on the fly: "Show in Explorer"
|
|
|
|
m_logFolderPath->SetHint(utfTo<wxString>(globalLogFolderPhrase_));
|
|
//1. no text shown when control is disabled! 2. apparently there's a refresh problem on GTK
|
|
|
|
m_logFolderPath->setHistory(std::make_shared<HistoryList>(logFolderHistory, folderHistoryMax));
|
|
|
|
m_comboBoxEmail->SetHint(/*_("Example:") + */ L"john.doe@example.com");
|
|
m_comboBoxEmail->setHistory(emailHistory, emailHistoryMax);
|
|
|
|
m_comboBoxEmail ->Enable(enableExtraFeatures_);
|
|
m_bpButtonEmailAlways ->Enable(enableExtraFeatures_);
|
|
m_bpButtonEmailErrorWarning ->Enable(enableExtraFeatures_);
|
|
m_bpButtonEmailErrorOnly ->Enable(enableExtraFeatures_);
|
|
|
|
//m_staticTextPostSync->SetMinSize({dipToWxsize(180), -1});
|
|
|
|
m_comboBoxPostSyncCommand->SetHint(_("Example:") + L" systemctl poweroff");
|
|
|
|
m_comboBoxPostSyncCommand->setHistory(commandHistory, commandHistoryMax);
|
|
|
|
//-----------------------------------------------------
|
|
//
|
|
//enable dialog-specific key events
|
|
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); });
|
|
|
|
assert(!m_listBoxFolderPair->IsSorted());
|
|
|
|
m_listBoxFolderPair->Append(_("All folder pairs"));
|
|
for (const LocalPairConfig& lpc : localPairCfg)
|
|
{
|
|
std::wstring fpName = getShortDisplayNameForFolderPair(createAbstractPath(lpc.folderPathPhraseLeft),
|
|
createAbstractPath(lpc.folderPathPhraseRight));
|
|
if (trimCpy(fpName).empty())
|
|
fpName = L"<" + _("empty") + L">";
|
|
|
|
m_listBoxFolderPair->Append(TAB_SPACE + fpName);
|
|
}
|
|
|
|
if (!showMultipleCfgs)
|
|
{
|
|
m_listBoxFolderPair->Hide();
|
|
m_staticTextFolderPairLabel->Hide();
|
|
}
|
|
|
|
//temporarily set main config as reference for window min size calculations:
|
|
globalPairCfg_ = GlobalPairConfig();
|
|
globalPairCfg_.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::twoWay);
|
|
globalPairCfg_.syncCfg.deletionVariant = DeletionVariant::versioning;
|
|
globalPairCfg_.syncCfg.versioningFolderPhrase = Zstr("dummy");
|
|
globalPairCfg_.syncCfg.versioningStyle = VersioningStyle::timestampFile;
|
|
globalPairCfg_.syncCfg.versionMaxAgeDays = 30;
|
|
globalPairCfg_.miscCfg.autoRetryCount = 1;
|
|
globalPairCfg_.miscCfg.altLogFolderPathPhrase = Zstr("dummy");
|
|
globalPairCfg_.miscCfg.emailNotifyAddress = "dummy";
|
|
|
|
selectFolderPairConfig(-1);
|
|
|
|
GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize()
|
|
#ifdef __WXGTK3__
|
|
Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088
|
|
//Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404
|
|
#endif
|
|
Center(); //apply *after* dialog size change!
|
|
|
|
//keep stable sizer height: change-based directions are taller than difference-based ones => init with SyncVariant::twoWay
|
|
bSizerSyncDirHolder ->SetMinSize(-1, bSizerSyncDirsChanges ->GetSize().y);
|
|
bSizerVersioningHolder->SetMinSize(-1, bSizerVersioningHolder->GetSize().y);
|
|
|
|
unselectFolderPairConfig(false /*validateParams*/);
|
|
globalPairCfg_ = globalPairCfg; //restore proper value
|
|
|
|
//set actual sync config
|
|
selectFolderPairConfig(localPairIndexToShow);
|
|
|
|
//more useful and Enter is redirected to m_buttonOK anyway:
|
|
(m_listBoxFolderPair->IsShown() ? static_cast<wxWindow*>(m_listBoxFolderPair) : m_notebook)->SetFocus();
|
|
}
|
|
|
|
|
|
void ConfigDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
|
|
{
|
|
auto changeSelection = [&](SyncConfigPanel panel)
|
|
{
|
|
m_notebook->ChangeSelection(static_cast<size_t>(panel));
|
|
(m_listBoxFolderPair->IsShown() ? static_cast<wxWindow*>(m_listBoxFolderPair) : m_notebook)->SetFocus(); //GTK ignores F-keys if focus is on hidden item!
|
|
};
|
|
|
|
switch (event.GetKeyCode())
|
|
{
|
|
case WXK_RETURN:
|
|
case WXK_NUMPAD_ENTER:
|
|
if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter
|
|
{
|
|
wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED);
|
|
m_buttonOK->Command(dummy); //simulate click
|
|
return;
|
|
}
|
|
break;
|
|
case WXK_F6:
|
|
changeSelection(SyncConfigPanel::compare);
|
|
return; //handled!
|
|
case WXK_F7:
|
|
changeSelection(SyncConfigPanel::filter);
|
|
return;
|
|
case WXK_F8:
|
|
changeSelection(SyncConfigPanel::sync);
|
|
return;
|
|
}
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void ConfigDialog::onListBoxKeyEvent(wxKeyEvent& event)
|
|
{
|
|
int keyCode = event.GetKeyCode();
|
|
if (m_listBoxFolderPair->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;
|
|
}
|
|
|
|
switch (keyCode)
|
|
{
|
|
case WXK_LEFT:
|
|
case WXK_NUMPAD_LEFT:
|
|
switch (static_cast<SyncConfigPanel>(m_notebook->GetSelection()))
|
|
{
|
|
case SyncConfigPanel::compare:
|
|
break;
|
|
case SyncConfigPanel::filter:
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::compare));
|
|
break;
|
|
case SyncConfigPanel::sync:
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::filter));
|
|
break;
|
|
}
|
|
m_listBoxFolderPair->SetFocus(); //needed! wxNotebook::ChangeSelection() leads to focus change!
|
|
return; //handled!
|
|
|
|
case WXK_RIGHT:
|
|
case WXK_NUMPAD_RIGHT:
|
|
switch (static_cast<SyncConfigPanel>(m_notebook->GetSelection()))
|
|
{
|
|
case SyncConfigPanel::compare:
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::filter));
|
|
break;
|
|
case SyncConfigPanel::filter:
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync));
|
|
break;
|
|
case SyncConfigPanel::sync:
|
|
break;
|
|
}
|
|
m_listBoxFolderPair->SetFocus();
|
|
return; //handled!
|
|
}
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
|
|
void ConfigDialog::onSelectFolderPair(wxCommandEvent& event)
|
|
{
|
|
assert(!m_listBoxFolderPair->HasMultipleSelection()); //single-choice!
|
|
const int selPos = event.GetSelection();
|
|
assert(0 <= selPos && selPos < makeSigned(m_listBoxFolderPair->GetCount()));
|
|
|
|
//m_listBoxFolderPair has no parameter ownership! => selectedPairIndexToShow has!
|
|
|
|
if (!unselectFolderPairConfig(true /*validateParams*/))
|
|
{
|
|
//restore old selection:
|
|
m_listBoxFolderPair->SetSelection(selectedPairIndexToShow_ + 1);
|
|
return;
|
|
}
|
|
selectFolderPairConfig(selPos - 1);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onCompByTimeSizeDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onCompByTimeSize(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onCompBySizeDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onCompBySize(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onCompByContentDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onCompByContent(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
std::optional<CompConfig> ConfigDialog::getCompConfig() const
|
|
{
|
|
if (!m_checkBoxUseLocalCmpOptions->GetValue())
|
|
return {};
|
|
|
|
CompConfig compCfg;
|
|
compCfg.compareVar = localCmpVar_;
|
|
compCfg.handleSymlinks = !m_checkBoxSymlinksInclude->GetValue() ? SymLinkHandling::exclude : m_radioBtnSymlinksDirect->GetValue() ? SymLinkHandling::asLink : SymLinkHandling::follow;
|
|
compCfg.ignoreTimeShiftMinutes = fromTimeShiftPhrase(copyStringTo<std::wstring>(m_textCtrlTimeShift->GetValue()));
|
|
|
|
return compCfg;
|
|
}
|
|
|
|
|
|
void ConfigDialog::setCompConfig(const CompConfig* compCfg)
|
|
{
|
|
m_checkBoxUseLocalCmpOptions->SetValue(compCfg);
|
|
|
|
//when local settings are inactive, display (current) global settings instead:
|
|
const CompConfig tmpCfg = compCfg ? *compCfg : globalPairCfg_.cmpCfg;
|
|
|
|
localCmpVar_ = tmpCfg.compareVar;
|
|
|
|
switch (tmpCfg.handleSymlinks)
|
|
{
|
|
case SymLinkHandling::exclude:
|
|
m_checkBoxSymlinksInclude->SetValue(false);
|
|
m_radioBtnSymlinksFollow ->SetValue(true);
|
|
break;
|
|
case SymLinkHandling::follow:
|
|
m_checkBoxSymlinksInclude->SetValue(true);
|
|
m_radioBtnSymlinksFollow->SetValue(true);
|
|
break;
|
|
case SymLinkHandling::asLink:
|
|
m_checkBoxSymlinksInclude->SetValue(true);
|
|
m_radioBtnSymlinksDirect->SetValue(true);
|
|
break;
|
|
}
|
|
|
|
m_textCtrlTimeShift->ChangeValue(toTimeShiftPhrase(tmpCfg.ignoreTimeShiftMinutes));
|
|
|
|
updateCompGui();
|
|
}
|
|
|
|
|
|
void ConfigDialog::updateCompGui()
|
|
{
|
|
const bool compOptionsEnabled = m_checkBoxUseLocalCmpOptions->GetValue();
|
|
|
|
m_panelComparisonSettings->Enable(compOptionsEnabled);
|
|
|
|
m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::compare),
|
|
static_cast<int>(compOptionsEnabled ? ConfigTypeImage::compare : ConfigTypeImage::compareGrey));
|
|
|
|
//update toggle buttons -> they have no parameter-ownership at all!
|
|
m_buttonByTimeSize->setActive(CompareVariant::timeSize == localCmpVar_ && compOptionsEnabled);
|
|
m_buttonByContent ->setActive(CompareVariant::content == localCmpVar_ && compOptionsEnabled);
|
|
m_buttonBySize ->setActive(CompareVariant::size == localCmpVar_ && compOptionsEnabled);
|
|
//compOptionsEnabled: nudge wxWidgets to render inactive config state (needed on Windows, NOT on Linux!)
|
|
|
|
switch (localCmpVar_) //unconditionally update image, including "local options off"
|
|
{
|
|
case CompareVariant::timeSize:
|
|
//help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!)
|
|
setImage(*m_bitmapCompVariant, greyScaleIfDisabled(loadImage("cmp_time"), compOptionsEnabled));
|
|
break;
|
|
case CompareVariant::content:
|
|
setImage(*m_bitmapCompVariant, greyScaleIfDisabled(loadImage("cmp_content"), compOptionsEnabled));
|
|
break;
|
|
case CompareVariant::size:
|
|
setImage(*m_bitmapCompVariant, greyScaleIfDisabled(loadImage("cmp_size"), compOptionsEnabled));
|
|
break;
|
|
}
|
|
|
|
//active variant description:
|
|
setText(*m_staticTextCompVarDescription, getCompVariantDescription(localCmpVar_));
|
|
m_staticTextCompVarDescription->Wrap(dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP)); //needs to be reapplied after SetLabel()
|
|
|
|
m_radioBtnSymlinksDirect->Enable(m_checkBoxSymlinksInclude->GetValue() && compOptionsEnabled); //help wxWidgets a little to render inactive config state (needed on Windows, NOT on Linux!)
|
|
m_radioBtnSymlinksFollow->Enable(m_checkBoxSymlinksInclude->GetValue() && compOptionsEnabled); //
|
|
}
|
|
|
|
|
|
void ConfigDialog::onFilterDefaultContext(wxEvent& event)
|
|
{
|
|
const FilterConfig activeCfg = getFilterConfig();
|
|
const FilterConfig defaultFilter = GlobalConfig().defaultFilter;
|
|
|
|
ContextMenu menu;
|
|
menu.addItem(_("&Save"), [&] { defaultFilterOut_ = activeCfg; updateFilterGui(); },
|
|
loadImage("cfg_save", dipToScreen(getMenuIconDipSize())), defaultFilterOut_ != activeCfg);
|
|
|
|
menu.addItem(_("&Load factory default"), [&] { setFilterConfig(defaultFilter); }, wxNullImage, activeCfg != defaultFilter);
|
|
|
|
menu.popup(*m_bpButtonDefaultContext, {m_bpButtonDefaultContext->GetSize().x, 0});
|
|
}
|
|
|
|
|
|
FilterConfig ConfigDialog::getFilterConfig() const
|
|
{
|
|
auto sanitizeFilter = [](wxString str)
|
|
{
|
|
//macOS: Ctrl+Enter inserts Unicode LINE_SEPARATOR which is indistinguishable from new line!
|
|
replace(str, LINE_SEPARATOR, L'\n');
|
|
replace(str, PARAGRAPH_SEPARATOR, L'\n');
|
|
|
|
return utfTo<Zstring>(str);
|
|
};
|
|
|
|
return
|
|
{
|
|
sanitizeFilter(m_textCtrlInclude->GetValue()), sanitizeFilter(m_textCtrlExclude->GetValue()),
|
|
makeUnsigned(m_spinCtrlTimespan->GetValue()),
|
|
enumTimeDescr_.get(),
|
|
makeUnsigned(m_spinCtrlMinSize->GetValue()), enumMinSizeDescr_.get(),
|
|
makeUnsigned(m_spinCtrlMaxSize->GetValue()), enumMaxSizeDescr_.get()};
|
|
}
|
|
|
|
|
|
void ConfigDialog::setFilterConfig(const FilterConfig& filter)
|
|
{
|
|
m_textCtrlInclude->ChangeValue(utfTo<wxString>(filter.includeFilter));
|
|
m_textCtrlExclude->ChangeValue(utfTo<wxString>(filter.excludeFilter));
|
|
|
|
enumTimeDescr_ .set(filter.unitTimeSpan);
|
|
enumMinSizeDescr_.set(filter.unitSizeMin);
|
|
enumMaxSizeDescr_.set(filter.unitSizeMax);
|
|
|
|
m_spinCtrlTimespan->SetValue(static_cast<int>(filter.timeSpan));
|
|
m_spinCtrlMinSize ->SetValue(static_cast<int>(filter.sizeMin));
|
|
m_spinCtrlMaxSize ->SetValue(static_cast<int>(filter.sizeMax));
|
|
|
|
updateFilterGui();
|
|
}
|
|
|
|
|
|
void ConfigDialog::updateFilterGui()
|
|
{
|
|
const FilterConfig activeCfg = getFilterConfig();
|
|
|
|
m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::filter),
|
|
static_cast<int>(!isNullFilter(activeCfg) ? ConfigTypeImage::filter: ConfigTypeImage::filterGrey));
|
|
|
|
setImage(*m_bitmapInclude, greyScaleIfDisabled(loadImage("filter_include"), !NameFilter::isNull(activeCfg.includeFilter, FilterConfig().excludeFilter)));
|
|
setImage(*m_bitmapExclude, greyScaleIfDisabled(loadImage("filter_exclude"), !NameFilter::isNull(FilterConfig().includeFilter, activeCfg.excludeFilter)));
|
|
setImage(*m_bitmapFilterDate, greyScaleIfDisabled(loadImage("cmp_time"), activeCfg.unitTimeSpan != UnitTime::none));
|
|
setImage(*m_bitmapFilterSize, greyScaleIfDisabled(loadImage("cmp_size"), activeCfg.unitSizeMin != UnitSize::none || activeCfg.unitSizeMax != UnitSize::none));
|
|
|
|
m_spinCtrlTimespan->Enable(activeCfg.unitTimeSpan == UnitTime::lastDays);
|
|
m_spinCtrlMinSize ->Enable(activeCfg.unitSizeMin != UnitSize::none);
|
|
m_spinCtrlMaxSize ->Enable(activeCfg.unitSizeMax != UnitSize::none);
|
|
|
|
m_buttonDefault->Enable(activeCfg != defaultFilterOut_);
|
|
m_buttonClear ->Enable(activeCfg != FilterConfig());
|
|
}
|
|
|
|
|
|
void ConfigDialog::onToggleUseDatabase(wxCommandEvent& event)
|
|
{
|
|
if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs))
|
|
directionsCfg_.dirs = getChangesDirDefault(*diffDirs);
|
|
else
|
|
{
|
|
const DirectionByChange& changeDirs = std::get<DirectionByChange>(directionsCfg_.dirs);
|
|
directionsCfg_.dirs = getDiffDirDefault(changeDirs);
|
|
}
|
|
updateSyncGui();
|
|
}
|
|
|
|
|
|
void ConfigDialog::onSyncTwoWayDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onSyncTwoWay(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onSyncMirrorDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onSyncMirror(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onSyncUpdateDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onSyncUpdate(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onSyncCustomDouble(wxMouseEvent& event)
|
|
{
|
|
wxCommandEvent dummy;
|
|
onSyncCustom(dummy);
|
|
onOkay(dummy);
|
|
}
|
|
|
|
|
|
void toggleSyncDirection(SyncDirection& current)
|
|
{
|
|
switch (current)
|
|
{
|
|
case SyncDirection::right:
|
|
current = SyncDirection::left;
|
|
break;
|
|
case SyncDirection::left:
|
|
current = SyncDirection::none;
|
|
break;
|
|
case SyncDirection::none:
|
|
current = SyncDirection::right;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void ConfigDialog::toggleSyncDirButton(SyncDirection DirectionByDiff::* dir)
|
|
{
|
|
if (DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs))
|
|
{
|
|
toggleSyncDirection(diffDirs->*dir);
|
|
updateSyncGui();
|
|
}
|
|
else assert(false);
|
|
}
|
|
|
|
|
|
void ConfigDialog::onLeftNewer(wxCommandEvent& event)
|
|
{
|
|
toggleSyncDirButton(&DirectionByDiff::leftNewer);
|
|
assert(!leftRightNewerCombined());
|
|
}
|
|
|
|
|
|
void ConfigDialog::onRightNewer(wxCommandEvent& event)
|
|
{
|
|
toggleSyncDirButton(&DirectionByDiff::rightNewer);
|
|
assert(!leftRightNewerCombined());
|
|
}
|
|
|
|
|
|
void ConfigDialog::onDifferent(wxCommandEvent& event)
|
|
{
|
|
toggleSyncDirButton(&DirectionByDiff::leftNewer);
|
|
|
|
if (DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs))
|
|
//simulate category "different" as leftNewer/rightNewer combined:
|
|
diffDirs->rightNewer = diffDirs->leftNewer;
|
|
else assert(false);
|
|
assert(leftRightNewerCombined());
|
|
}
|
|
|
|
|
|
void ConfigDialog::toggleSyncDirButton(DirectionByChange::Changes DirectionByChange::* side, SyncDirection DirectionByChange::Changes::* dir)
|
|
{
|
|
if (DirectionByChange* changeDirs = std::get_if<DirectionByChange>(&directionsCfg_.dirs))
|
|
{
|
|
toggleSyncDirection(changeDirs->*side.*dir);
|
|
updateSyncGui();
|
|
}
|
|
else assert(false);
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
auto updateDirButton(wxBitmapButton& button, SyncDirection dir,
|
|
const char* imgNameLeft, const char* imgNameNone, const char* imgNameRight,
|
|
SyncOperation opLeft, SyncOperation opNone, SyncOperation opRight)
|
|
{
|
|
const char* imgName = nullptr;
|
|
switch (dir)
|
|
{
|
|
case SyncDirection::left:
|
|
imgName = imgNameLeft;
|
|
button.SetToolTip(getSyncOpDescription(opLeft));
|
|
break;
|
|
case SyncDirection::none:
|
|
imgName = imgNameNone;
|
|
button.SetToolTip(getSyncOpDescription(opNone));
|
|
break;
|
|
case SyncDirection::right:
|
|
imgName = imgNameRight;
|
|
button.SetToolTip(getSyncOpDescription(opRight));
|
|
break;
|
|
}
|
|
wxImage img = mirrorIfRtl(loadImage(imgName));
|
|
button.SetBitmapLabel (toScaledBitmap( img));
|
|
button.SetBitmapDisabled(toScaledBitmap(greyScale(img))); //fix wxWidgets' all-too-clever multi-state!
|
|
//=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets!
|
|
}
|
|
|
|
|
|
void updateDiffDirButtons(const DirectionByDiff& diffDirs,
|
|
wxBitmapButton& buttonLeftOnly,
|
|
wxBitmapButton& buttonRightOnly,
|
|
wxBitmapButton& buttonLeftNewer,
|
|
wxBitmapButton& buttonRightNewer,
|
|
wxBitmapButton& buttonDifferent)
|
|
{
|
|
updateDirButton(buttonLeftOnly, diffDirs.leftOnly, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT);
|
|
updateDirButton(buttonRightOnly, diffDirs.rightOnly, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT);
|
|
updateDirButton(buttonLeftNewer, diffDirs.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT);
|
|
updateDirButton(buttonRightNewer, diffDirs.rightNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT);
|
|
//simulate category "different" as leftNewer/rightNewer combined:
|
|
updateDirButton(buttonDifferent, diffDirs.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT);
|
|
}
|
|
|
|
|
|
void updateChangeDirButtons(const DirectionByChange& changeDirs,
|
|
wxBitmapButton& buttonLeftCreate,
|
|
wxBitmapButton& buttonLeftUpdate,
|
|
wxBitmapButton& buttonLeftDelete,
|
|
wxBitmapButton& buttonRightCreate,
|
|
wxBitmapButton& buttonRightUpdate,
|
|
wxBitmapButton& buttonRightDelete)
|
|
{
|
|
updateDirButton(buttonLeftCreate, changeDirs.left.create, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT);
|
|
updateDirButton(buttonLeftUpdate, changeDirs.left.update, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT);
|
|
updateDirButton(buttonLeftDelete, changeDirs.left.delete_, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT);
|
|
|
|
updateDirButton(buttonRightCreate, changeDirs.right.create, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT);
|
|
updateDirButton(buttonRightUpdate, changeDirs.right.update, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT);
|
|
updateDirButton(buttonRightDelete, changeDirs.right.delete_, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT);
|
|
}
|
|
}
|
|
|
|
void ConfigDialog::onShowLogFolder(wxCommandEvent& event)
|
|
{
|
|
assert(selectedPairIndexToShow_ < 0);
|
|
if (selectedPairIndexToShow_ < 0)
|
|
try
|
|
{
|
|
AbstractPath logFolderPath = createAbstractPath(getMiscSyncOptions().altLogFolderPathPhrase); //optional
|
|
if (AFS::isNullPath(logFolderPath))
|
|
logFolderPath = createAbstractPath(globalLogFolderPhrase_);
|
|
|
|
openFolderInFileBrowser(logFolderPath); //throw FileError
|
|
}
|
|
catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); }
|
|
}
|
|
|
|
|
|
bool ConfigDialog::leftRightNewerCombined() const
|
|
{
|
|
assert(std::get_if<DirectionByDiff>(&directionsCfg_.dirs));
|
|
const CompareVariant activeCmpVar = m_checkBoxUseLocalCmpOptions->GetValue() ? localCmpVar_ : globalPairCfg_.cmpCfg.compareVar;
|
|
return activeCmpVar == CompareVariant::content || activeCmpVar == CompareVariant::size;
|
|
}
|
|
|
|
|
|
std::optional<SyncConfig> ConfigDialog::getSyncConfig() const
|
|
{
|
|
if (!m_checkBoxUseLocalSyncOptions->GetValue())
|
|
return {};
|
|
|
|
SyncConfig syncCfg;
|
|
syncCfg.directionCfg = directionsCfg_;
|
|
syncCfg.deletionVariant = deletionVariant_;
|
|
syncCfg.versioningFolderPhrase = versioningFolder_.getPath();
|
|
syncCfg.versioningStyle = enumVersioningStyle_.get();
|
|
if (syncCfg.versioningStyle != VersioningStyle::replace)
|
|
{
|
|
syncCfg.versionMaxAgeDays = m_checkBoxVersionMaxDays ->GetValue() ? m_spinCtrlVersionMaxDays->GetValue() : 0;
|
|
syncCfg.versionCountMin = m_checkBoxVersionCountMin->GetValue() && m_checkBoxVersionMaxDays->GetValue() ? m_spinCtrlVersionCountMin->GetValue() : 0;
|
|
syncCfg.versionCountMax = m_checkBoxVersionCountMax->GetValue() ? m_spinCtrlVersionCountMax->GetValue() : 0;
|
|
}
|
|
|
|
//simulate category "different" as leftNewer/rightNewer combined:
|
|
if (DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&syncCfg.directionCfg.dirs))
|
|
if (leftRightNewerCombined())
|
|
diffDirs->rightNewer = diffDirs->leftNewer;
|
|
|
|
return syncCfg;
|
|
}
|
|
|
|
|
|
void ConfigDialog::setSyncConfig(const SyncConfig* syncCfg)
|
|
{
|
|
m_checkBoxUseLocalSyncOptions->SetValue(syncCfg);
|
|
|
|
//when local settings are inactive, display (current) global settings instead:
|
|
const SyncConfig tmpCfg = syncCfg ? *syncCfg : globalPairCfg_.syncCfg;
|
|
|
|
directionsCfg_ = tmpCfg.directionCfg; //make working copy; ownership *not* on GUI
|
|
deletionVariant_ = tmpCfg.deletionVariant;
|
|
versioningFolder_.setPath(tmpCfg.versioningFolderPhrase);
|
|
enumVersioningStyle_.set(tmpCfg.versioningStyle);
|
|
|
|
const bool useVersionLimits = tmpCfg.versioningStyle != VersioningStyle::replace;
|
|
|
|
m_checkBoxVersionMaxDays ->SetValue(useVersionLimits && tmpCfg.versionMaxAgeDays > 0);
|
|
m_checkBoxVersionCountMin->SetValue(useVersionLimits && tmpCfg.versionCountMin > 0 && tmpCfg.versionMaxAgeDays > 0);
|
|
m_checkBoxVersionCountMax->SetValue(useVersionLimits && tmpCfg.versionCountMax > 0);
|
|
|
|
m_spinCtrlVersionMaxDays ->SetValue(m_checkBoxVersionMaxDays ->GetValue() ? tmpCfg.versionMaxAgeDays : 30);
|
|
m_spinCtrlVersionCountMin->SetValue(m_checkBoxVersionCountMin->GetValue() ? tmpCfg.versionCountMin : 1);
|
|
m_spinCtrlVersionCountMax->SetValue(m_checkBoxVersionCountMax->GetValue() ? tmpCfg.versionCountMax : 1);
|
|
|
|
updateSyncGui();
|
|
}
|
|
|
|
|
|
void ConfigDialog::updateSyncGui()
|
|
{
|
|
const bool syncOptionsEnabled = m_checkBoxUseLocalSyncOptions->GetValue();
|
|
|
|
m_panelSyncSettings->Enable(syncOptionsEnabled);
|
|
|
|
m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::sync),
|
|
static_cast<int>(syncOptionsEnabled ? ConfigTypeImage::sync: ConfigTypeImage::syncGrey));
|
|
|
|
const bool setDirsByDifferences = std::get_if<DirectionByDiff>(&directionsCfg_.dirs);
|
|
|
|
m_checkBoxUseDatabase->SetValue(!setDirsByDifferences);
|
|
|
|
//display only relevant sync options
|
|
bSizerSyncDirsDiff ->Show( setDirsByDifferences);
|
|
bSizerSyncDirsChanges->Show(!setDirsByDifferences);
|
|
|
|
if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs)) //sync directions by differences
|
|
{
|
|
updateDiffDirButtons(*diffDirs,
|
|
*m_bpButtonLeftOnly,
|
|
*m_bpButtonRightOnly,
|
|
*m_bpButtonLeftNewer,
|
|
*m_bpButtonRightNewer,
|
|
*m_bpButtonDifferent);
|
|
|
|
//simulate category "different" as leftNewer/rightNewer combined:
|
|
const bool haveLeftRightNewerCombined = leftRightNewerCombined();
|
|
m_bitmapLeftNewer ->Show(!haveLeftRightNewerCombined);
|
|
m_bpButtonLeftNewer ->Show(!haveLeftRightNewerCombined);
|
|
m_bitmapRightNewer ->Show(!haveLeftRightNewerCombined);
|
|
m_bpButtonRightNewer->Show(!haveLeftRightNewerCombined);
|
|
|
|
m_bitmapDifferent ->Show(haveLeftRightNewerCombined);
|
|
m_bpButtonDifferent->Show(haveLeftRightNewerCombined);
|
|
}
|
|
else //sync directions by changes
|
|
{
|
|
const DirectionByChange& changeDirs = std::get<DirectionByChange>(directionsCfg_.dirs);
|
|
|
|
updateChangeDirButtons(changeDirs,
|
|
*m_bpButtonLeftCreate,
|
|
*m_bpButtonLeftUpdate,
|
|
*m_bpButtonLeftDelete,
|
|
*m_bpButtonRightCreate,
|
|
*m_bpButtonRightUpdate,
|
|
*m_bpButtonRightDelete);
|
|
}
|
|
|
|
const bool useDatabaseFile = std::get_if<DirectionByChange>(&directionsCfg_.dirs);
|
|
|
|
setImage(*m_bitmapDatabase, greyScaleIfDisabled(loadImage("database", dipToScreen(22)), useDatabaseFile && syncOptionsEnabled));
|
|
|
|
//"detect move files" is always active iff database is used:
|
|
setImage(*m_bitmapMoveLeft, greyScaleIfDisabled(loadImage("so_move_left", dipToScreen(20)), useDatabaseFile && syncOptionsEnabled));
|
|
setImage(*m_bitmapMoveRight, greyScaleIfDisabled(loadImage("so_move_right", dipToScreen(20)), useDatabaseFile && syncOptionsEnabled));
|
|
m_staticTextDetectMove->Enable(useDatabaseFile);
|
|
|
|
const SyncVariant syncVar = getSyncVariant(directionsCfg_);
|
|
|
|
//active variant description:
|
|
setText(*m_staticTextSyncVarDescription, getSyncVariantDescription(syncVar));
|
|
m_staticTextSyncVarDescription->Wrap(dipToWxsize(CFG_DESCRIPTION_WIDTH_DIP)); //needs to be reapplied after SetLabel()
|
|
|
|
//update toggle buttons -> they have no parameter-ownership at all!
|
|
m_buttonTwoWay->setActive(SyncVariant::twoWay == syncVar && syncOptionsEnabled);
|
|
m_buttonMirror->setActive(SyncVariant::mirror == syncVar && syncOptionsEnabled);
|
|
m_buttonUpdate->setActive(SyncVariant::update == syncVar && syncOptionsEnabled);
|
|
m_buttonCustom->setActive(SyncVariant::custom == syncVar && syncOptionsEnabled);
|
|
//syncOptionsEnabled: nudge wxWidgets to render inactive config state (needed on Windows, NOT on Linux!)
|
|
|
|
m_buttonRecycler ->setActive(DeletionVariant::recycler == deletionVariant_ && syncOptionsEnabled);
|
|
m_buttonPermanent ->setActive(DeletionVariant::permanent == deletionVariant_ && syncOptionsEnabled);
|
|
m_buttonVersioning->setActive(DeletionVariant::versioning == deletionVariant_ && syncOptionsEnabled);
|
|
|
|
switch (deletionVariant_) //unconditionally update image, including "local options off"
|
|
{
|
|
case DeletionVariant::recycler:
|
|
{
|
|
wxImage imgTrash = loadImage("delete_recycler");
|
|
//use system icon if available (can fail on Linux??)
|
|
try { imgTrash = extractWxImage(fff::getTrashIcon(imgTrash.GetHeight())); /*throw SysError*/ }
|
|
catch (SysError&) { assert(false); }
|
|
|
|
setImage(*m_bitmapDeletionType, greyScaleIfDisabled(imgTrash, syncOptionsEnabled));
|
|
setText(*m_staticTextDeletionTypeDescription, _("Retain deleted and overwritten files in the recycle bin"));
|
|
}
|
|
break;
|
|
case DeletionVariant::permanent:
|
|
setImage(*m_bitmapDeletionType, greyScaleIfDisabled(loadImage("delete_permanently"), syncOptionsEnabled));
|
|
setText(*m_staticTextDeletionTypeDescription, _("Delete and overwrite files permanently"));
|
|
break;
|
|
case DeletionVariant::versioning:
|
|
setImage(*m_bitmapVersioning, greyScaleIfDisabled(loadImage("delete_versioning"), syncOptionsEnabled));
|
|
break;
|
|
}
|
|
//m_staticTextDeletionTypeDescription->Wrap(dipToWxsize(200)); //needs to be reapplied after SetLabel()
|
|
|
|
const bool versioningSelected = deletionVariant_ == DeletionVariant::versioning;
|
|
|
|
m_bitmapDeletionType ->Show(!versioningSelected);
|
|
m_staticTextDeletionTypeDescription->Show(!versioningSelected);
|
|
m_panelVersioning ->Show( versioningSelected);
|
|
|
|
if (versioningSelected)
|
|
{
|
|
enumVersioningStyle_.updateTooltip();
|
|
|
|
const VersioningStyle versioningStyle = enumVersioningStyle_.get();
|
|
const std::wstring pathSep = utfTo<std::wstring>(FILE_NAME_SEPARATOR);
|
|
|
|
switch (versioningStyle)
|
|
{
|
|
case VersioningStyle::replace:
|
|
setText(*m_staticTextNamingCvtPart1, pathSep + _("Folder") + pathSep + _("File") + L".doc");
|
|
setText(*m_staticTextNamingCvtPart2Bold, L"");
|
|
setText(*m_staticTextNamingCvtPart3, L"");
|
|
break;
|
|
|
|
case VersioningStyle::timestampFolder:
|
|
setText(*m_staticTextNamingCvtPart1, pathSep);
|
|
setText(*m_staticTextNamingCvtPart2Bold, _("YYYY-MM-DD hhmmss"));
|
|
setText(*m_staticTextNamingCvtPart3, pathSep + _("Folder") + pathSep + _("File") + L".doc ");
|
|
break;
|
|
|
|
case VersioningStyle::timestampFile:
|
|
setText(*m_staticTextNamingCvtPart1, pathSep + _("Folder") + pathSep + _("File") + L".doc ");
|
|
setText(*m_staticTextNamingCvtPart2Bold, _("YYYY-MM-DD hhmmss"));
|
|
setText(*m_staticTextNamingCvtPart3, L".doc");
|
|
break;
|
|
}
|
|
|
|
const bool enableLimitCtrls = syncOptionsEnabled && versioningStyle != VersioningStyle::replace;
|
|
const bool showLimitCtrls = m_checkBoxVersionMaxDays->GetValue() || m_checkBoxVersionCountMax->GetValue();
|
|
//m_checkBoxVersionCountMin->GetValue() => irrelevant if !m_checkBoxVersionMaxDays->GetValue()!
|
|
|
|
if (!m_checkBoxVersionMaxDays->GetValue() && m_checkBoxVersionCountMin->GetValue())
|
|
m_checkBoxVersionCountMin->SetValue(false); //make this dependency cristal-clear (don't just disable)
|
|
|
|
m_staticTextLimitVersions->Show(!showLimitCtrls);
|
|
|
|
m_spinCtrlVersionMaxDays ->Show(showLimitCtrls);
|
|
m_spinCtrlVersionCountMin->Show(showLimitCtrls);
|
|
m_spinCtrlVersionCountMax->Show(showLimitCtrls);
|
|
|
|
m_staticTextLimitVersions->Enable(enableLimitCtrls);
|
|
m_checkBoxVersionMaxDays ->Enable(enableLimitCtrls);
|
|
m_checkBoxVersionCountMin->Enable(enableLimitCtrls && m_checkBoxVersionMaxDays->GetValue());
|
|
m_checkBoxVersionCountMax->Enable(enableLimitCtrls);
|
|
|
|
m_spinCtrlVersionMaxDays ->Enable(enableLimitCtrls && m_checkBoxVersionMaxDays ->GetValue());
|
|
m_spinCtrlVersionCountMin->Enable(enableLimitCtrls && m_checkBoxVersionMaxDays->GetValue() && m_checkBoxVersionCountMin->GetValue());
|
|
m_spinCtrlVersionCountMax->Enable(enableLimitCtrls && m_checkBoxVersionCountMax->GetValue());
|
|
}
|
|
|
|
m_panelSyncSettings->Layout();
|
|
|
|
//Refresh(); //removes a few artifacts when toggling display of versioning folder
|
|
}
|
|
|
|
|
|
MiscSyncConfig ConfigDialog::getMiscSyncOptions() const
|
|
{
|
|
MiscSyncConfig miscCfg;
|
|
|
|
// Avoid "fake" changed configs! =>
|
|
// - don't touch items corresponding to paths not currently used
|
|
// - don't store parallel ops == 1
|
|
miscCfg.deviceParallelOps = deviceParallelOps_;
|
|
assert(fgSizerPerf->GetItemCount() == 2 * devicesForEdit_.size());
|
|
int i = 0;
|
|
for (const AfsDevice& afsDevice : devicesForEdit_)
|
|
{
|
|
wxSpinCtrl* spinCtrlParallelOps = dynamic_cast<wxSpinCtrl*>(fgSizerPerf->GetItem(i * 2)->GetWindow());
|
|
setDeviceParallelOps(miscCfg.deviceParallelOps, afsDevice, spinCtrlParallelOps->GetValue());
|
|
++i;
|
|
}
|
|
//----------------------------------------------------------------------------
|
|
miscCfg.ignoreErrors = m_checkBoxIgnoreErrors->GetValue();
|
|
miscCfg.autoRetryCount = m_checkBoxAutoRetry ->GetValue() ? m_spinCtrlAutoRetryCount->GetValue() : 0;
|
|
miscCfg.autoRetryDelay = std::chrono::seconds(m_spinCtrlAutoRetryDelay->GetValue());
|
|
//----------------------------------------------------------------------------
|
|
miscCfg.postSyncCommand = m_comboBoxPostSyncCommand->getValue();
|
|
miscCfg.postSyncCondition = enumPostSyncCondition_.get();
|
|
//----------------------------------------------------------------------------
|
|
Zstring altLogFolderPhrase = logFolderSelector_.getPath();
|
|
if (altLogFolderPhrase.empty()) //"empty" already means "unchecked"
|
|
altLogFolderPhrase = Zstr(' '); //=> trigger error message on dialog close
|
|
miscCfg.altLogFolderPathPhrase = m_checkBoxOverrideLogPath->GetValue() ? altLogFolderPhrase : Zstring();
|
|
//----------------------------------------------------------------------------
|
|
std::string emailAddress = utfTo<std::string>(m_comboBoxEmail->getValue());
|
|
if (emailAddress.empty())
|
|
emailAddress = ' '; //trigger error message on dialog close
|
|
miscCfg.emailNotifyAddress = m_checkBoxSendEmail->GetValue() ? emailAddress : std::string();
|
|
miscCfg.emailNotifyCondition = emailNotifyCondition_;
|
|
//----------------------------------------------------------------------------
|
|
miscCfg.notes = trimCpy(utfTo<std::wstring>(m_textCtrNotes->GetValue()));
|
|
|
|
return miscCfg;
|
|
}
|
|
|
|
|
|
void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg)
|
|
{
|
|
// Avoid "fake" changed configs! =>
|
|
//- when editting, consider only the deviceParallelOps items corresponding to the currently-used folder paths
|
|
//- keep parallel ops == 1 only temporarily during edit
|
|
deviceParallelOps_ = miscCfg.deviceParallelOps;
|
|
|
|
assert(fgSizerPerf->GetItemCount() % 2 == 0);
|
|
const int rowsToCreate = static_cast<int>(devicesForEdit_.size()) - static_cast<int>(fgSizerPerf->GetItemCount() / 2);
|
|
if (rowsToCreate >= 0)
|
|
for (int i = 0; i < rowsToCreate; ++i)
|
|
{
|
|
wxSpinCtrl* spinCtrlParallelOps = new wxSpinCtrl(m_scrolledWindowPerf, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 2000'000'000, 1);
|
|
setDefaultWidth(*spinCtrlParallelOps);
|
|
spinCtrlParallelOps->Enable(enableExtraFeatures_);
|
|
fgSizerPerf->Add(spinCtrlParallelOps, 0, wxALIGN_CENTER_VERTICAL);
|
|
|
|
wxStaticText* staticTextDevice = new wxStaticText(m_scrolledWindowPerf, wxID_ANY, wxEmptyString);
|
|
staticTextDevice->Enable(enableExtraFeatures_);
|
|
fgSizerPerf->Add(staticTextDevice, 0, wxALIGN_CENTER_VERTICAL);
|
|
}
|
|
else
|
|
for (int i = 0; i < -rowsToCreate * 2; ++i)
|
|
fgSizerPerf->GetItem(size_t(0))->GetWindow()->Destroy();
|
|
assert(fgSizerPerf->GetItemCount() == 2 * devicesForEdit_.size());
|
|
|
|
int i = 0;
|
|
for (const AfsDevice& afsDevice : devicesForEdit_)
|
|
{
|
|
wxSpinCtrl* spinCtrlParallelOps = dynamic_cast<wxSpinCtrl*> (fgSizerPerf->GetItem(i * 2 )->GetWindow());
|
|
wxStaticText* staticTextDevice = dynamic_cast<wxStaticText*>(fgSizerPerf->GetItem(i * 2 + 1)->GetWindow());
|
|
|
|
spinCtrlParallelOps->SetValue(static_cast<int>(getDeviceParallelOps(deviceParallelOps_, afsDevice)));
|
|
staticTextDevice->SetLabelText(AFS::getDisplayPath(AbstractPath(afsDevice, AfsPath())));
|
|
++i;
|
|
}
|
|
m_staticTextPerfParallelOps->Enable(enableExtraFeatures_ && !devicesForEdit_.empty());
|
|
|
|
m_panelComparisonSettings->Layout(); //*after* setting text labels
|
|
|
|
//----------------------------------------------------------------------------
|
|
m_checkBoxIgnoreErrors ->SetValue(miscCfg.ignoreErrors);
|
|
m_checkBoxAutoRetry ->SetValue(miscCfg.autoRetryCount > 0);
|
|
m_spinCtrlAutoRetryCount->SetValue(std::max<size_t>(miscCfg.autoRetryCount, 0));
|
|
m_spinCtrlAutoRetryDelay->SetValue(miscCfg.autoRetryDelay.count());
|
|
//----------------------------------------------------------------------------
|
|
m_comboBoxPostSyncCommand->setValue(miscCfg.postSyncCommand);
|
|
enumPostSyncCondition_.set(miscCfg.postSyncCondition);
|
|
//----------------------------------------------------------------------------
|
|
m_checkBoxOverrideLogPath->SetValue(!miscCfg.altLogFolderPathPhrase.empty()); //only "empty path" means unchecked! everything else (e.g. " "): "checked"
|
|
logFolderSelector_.setPath(m_checkBoxOverrideLogPath->GetValue() ? miscCfg.altLogFolderPathPhrase : globalLogFolderPhrase_);
|
|
//----------------------------------------------------------------------------
|
|
Zstring defaultEmail;
|
|
if (const std::vector<Zstring>& history = m_comboBoxEmail->getHistory();
|
|
!history.empty())
|
|
defaultEmail = history[0];
|
|
|
|
m_checkBoxSendEmail->SetValue(!trimCpy(miscCfg.emailNotifyAddress).empty());
|
|
m_comboBoxEmail->setValue(m_checkBoxSendEmail->GetValue() ? utfTo<Zstring>(miscCfg.emailNotifyAddress) : defaultEmail);
|
|
emailNotifyCondition_ = miscCfg.emailNotifyCondition;
|
|
//----------------------------------------------------------------------------
|
|
m_textCtrNotes->ChangeValue(utfTo<wxString>(miscCfg.notes));
|
|
|
|
updateMiscGui();
|
|
}
|
|
|
|
|
|
void ConfigDialog::updateMiscGui()
|
|
{
|
|
if (selectedPairIndexToShow_ == -1)
|
|
{
|
|
const MiscSyncConfig miscCfg = getMiscSyncOptions();
|
|
|
|
setImage(*m_bitmapIgnoreErrors, greyScaleIfDisabled(loadImage("error_ignore_active"), miscCfg.ignoreErrors));
|
|
setImage(*m_bitmapRetryErrors, greyScaleIfDisabled(loadImage("error_retry"), miscCfg.autoRetryCount > 0 ));
|
|
|
|
fgSizerAutoRetry->Show(miscCfg.autoRetryCount > 0);
|
|
|
|
m_panelComparisonSettings->Layout(); //showing "retry count" can affect bSizerPerformance!
|
|
//----------------------------------------------------------------------------
|
|
const bool sendEmailEnabled = m_checkBoxSendEmail->GetValue();
|
|
setImage(*m_bitmapEmail, greyScaleIfDisabled(loadImage("email"), sendEmailEnabled));
|
|
m_comboBoxEmail->Show(sendEmailEnabled);
|
|
|
|
auto updateButton = [successIcon = loadImage("msg_success", dipToScreen(getMenuIconDipSize())),
|
|
warningIcon = loadImage("msg_warning", dipToScreen(getMenuIconDipSize())),
|
|
errorIcon = loadImage("msg_error", dipToScreen(getMenuIconDipSize())),
|
|
sendEmailEnabled, this](wxBitmapButton& button, ResultsNotification notifyCondition)
|
|
{
|
|
button.Show(sendEmailEnabled);
|
|
if (sendEmailEnabled)
|
|
{
|
|
wxString tooltip = _("Error");
|
|
wxImage label = errorIcon;
|
|
|
|
if (notifyCondition == ResultsNotification::always ||
|
|
notifyCondition == ResultsNotification::errorWarning)
|
|
{
|
|
tooltip += (L" | ") + _("Warning");
|
|
label = stackImages(label, warningIcon, ImageStackLayout::horizontal, ImageStackAlignment::center);
|
|
}
|
|
else
|
|
label = resizeCanvas(label, {label.GetWidth() + warningIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT);
|
|
|
|
if (notifyCondition == ResultsNotification::always)
|
|
{
|
|
tooltip += (L" | ") + _("Success");
|
|
label = stackImages(label, successIcon, ImageStackLayout::horizontal, ImageStackAlignment::center);
|
|
}
|
|
else
|
|
label = resizeCanvas(label, {label.GetWidth() + successIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT);
|
|
|
|
button.SetToolTip(tooltip);
|
|
button.SetBitmapLabel (toScaledBitmap(notifyCondition == emailNotifyCondition_ && sendEmailEnabled ? label : greyScale(label)));
|
|
button.SetBitmapDisabled(toScaledBitmap(greyScale(label))); //fix wxWidgets' all-too-clever multi-state!
|
|
//=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets!
|
|
}
|
|
};
|
|
updateButton(*m_bpButtonEmailAlways, ResultsNotification::always);
|
|
updateButton(*m_bpButtonEmailErrorWarning, ResultsNotification::errorWarning);
|
|
updateButton(*m_bpButtonEmailErrorOnly, ResultsNotification::errorOnly);
|
|
|
|
m_hyperlinkPerfDeRequired2->Show(!enableExtraFeatures_); //required after each bSizerSyncMisc->Show()
|
|
|
|
//----------------------------------------------------------------------------
|
|
setImage(*m_bitmapLogFile, greyScaleIfDisabled(loadImage("log_file", dipToScreen(20)), m_checkBoxOverrideLogPath->GetValue()));
|
|
m_logFolderPath ->Enable(m_checkBoxOverrideLogPath->GetValue()); //
|
|
m_buttonSelectLogFolder ->Show(m_checkBoxOverrideLogPath->GetValue()); //enabled status can't be derived from resolved config!
|
|
m_bpButtonSelectAltLogFolder->Show(m_checkBoxOverrideLogPath->GetValue()); //
|
|
|
|
m_panelSyncSettings->Layout(); //after showing/hiding m_buttonSelectLogFolder
|
|
|
|
m_panelSyncSettings->Refresh(); //removes a few artifacts when toggling email notifications
|
|
m_panelLogfile ->Refresh();//
|
|
}
|
|
//----------------------------------------------------------------------------
|
|
m_buttonAddNotes->Show(!showNotesPanel_);
|
|
m_panelNotes ->Show(showNotesPanel_);
|
|
}
|
|
|
|
|
|
void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow)
|
|
{
|
|
assert(selectedPairIndexToShow_ == EMPTY_PAIR_INDEX_SELECTED);
|
|
assert(newPairIndexToShow == -1 || makeUnsigned(newPairIndexToShow) < localPairCfg_.size());
|
|
newPairIndexToShow = std::clamp(newPairIndexToShow, -1, static_cast<int>(localPairCfg_.size()) - 1);
|
|
|
|
selectedPairIndexToShow_ = newPairIndexToShow;
|
|
m_listBoxFolderPair->SetSelection(newPairIndexToShow + 1);
|
|
|
|
//show/hide controls that are only relevant for main/local config
|
|
const bool mainConfigSelected = newPairIndexToShow < 0;
|
|
//comparison panel:
|
|
m_staticTextMainCompSettings->Show( mainConfigSelected && showMultipleCfgs_);
|
|
m_checkBoxUseLocalCmpOptions->Show(!mainConfigSelected && showMultipleCfgs_);
|
|
m_staticlineCompHeader->Show(showMultipleCfgs_);
|
|
//filter panel
|
|
m_staticTextMainFilterSettings ->Show( mainConfigSelected && showMultipleCfgs_);
|
|
m_staticTextLocalFilterSettings->Show(!mainConfigSelected && showMultipleCfgs_);
|
|
m_staticlineFilterHeader->Show(showMultipleCfgs_);
|
|
//sync panel:
|
|
m_staticTextMainSyncSettings ->Show( mainConfigSelected && showMultipleCfgs_);
|
|
m_checkBoxUseLocalSyncOptions->Show(!mainConfigSelected && showMultipleCfgs_);
|
|
m_staticlineSyncHeader->Show(showMultipleCfgs_);
|
|
//misc
|
|
bSizerPerformance->Show(mainConfigSelected); //caveat: recursively shows hidden child items!
|
|
bSizerCompMisc ->Show(mainConfigSelected);
|
|
bSizerSyncMisc ->Show(mainConfigSelected);
|
|
|
|
if (mainConfigSelected)
|
|
{
|
|
m_hyperlinkPerfDeRequired->Show(!enableExtraFeatures_); //keep after bSizerPerformance->Show()
|
|
|
|
//update the devices list for "parallel file operations" before calling setMiscSyncOptions():
|
|
// => should be enough to do this when selecting the main config
|
|
// => to be "perfect" we'd have to update already when the user drags & drops a different versioning folder
|
|
devicesForEdit_.clear();
|
|
auto addDevicePath = [&](const Zstring& folderPathPhrase)
|
|
{
|
|
const AfsDevice& afsDevice = createAbstractPath(folderPathPhrase).afsDevice;
|
|
if (!AFS::isNullDevice(afsDevice))
|
|
devicesForEdit_.insert(afsDevice);
|
|
};
|
|
for (const LocalPairConfig& fpCfg : localPairCfg_)
|
|
{
|
|
addDevicePath(fpCfg.folderPathPhraseLeft);
|
|
addDevicePath(fpCfg.folderPathPhraseRight);
|
|
|
|
if (fpCfg.localSyncCfg && fpCfg.localSyncCfg->deletionVariant == DeletionVariant::versioning)
|
|
addDevicePath(fpCfg.localSyncCfg->versioningFolderPhrase);
|
|
}
|
|
if (globalPairCfg_.syncCfg.deletionVariant == DeletionVariant::versioning) //let's always add, even if *all* folder pairs use a local sync config (=> strange!)
|
|
addDevicePath(globalPairCfg_.syncCfg.versioningFolderPhrase);
|
|
//---------------------------------------------------------------------------------------------------------------
|
|
|
|
setCompConfig (&globalPairCfg_.cmpCfg);
|
|
setSyncConfig (&globalPairCfg_.syncCfg);
|
|
setFilterConfig(globalPairCfg_.filter);
|
|
}
|
|
else
|
|
{
|
|
setCompConfig(get(localPairCfg_[selectedPairIndexToShow_].localCmpCfg));
|
|
setSyncConfig(get(localPairCfg_[selectedPairIndexToShow_].localSyncCfg));
|
|
setFilterConfig (localPairCfg_[selectedPairIndexToShow_].localFilter);
|
|
}
|
|
setMiscSyncOptions(globalPairCfg_.miscCfg);
|
|
|
|
m_panelCompSettingsTab ->Layout(); //fix comp panel glitch on Win 7 125% font size + perf panel
|
|
m_panelFilterSettingsTab->Layout();
|
|
m_panelSyncSettingsTab ->Layout();
|
|
}
|
|
|
|
|
|
bool ConfigDialog::unselectFolderPairConfig(bool validateParams)
|
|
{
|
|
assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size());
|
|
|
|
std::optional<CompConfig> compCfg = getCompConfig();
|
|
std::optional<SyncConfig> syncCfg = getSyncConfig();
|
|
FilterConfig filterCfg = getFilterConfig();
|
|
|
|
MiscSyncConfig miscCfg = getMiscSyncOptions(); //some "misc" options are always visible, e.g. "notes"
|
|
|
|
//------- parameter validation (BEFORE writing output!) -------
|
|
if (validateParams)
|
|
{
|
|
//parameter validation and correction:
|
|
|
|
std::vector<AbstractPath> baseFolderPaths; //display paths to fix filter if user pastes full folder paths
|
|
if (selectedPairIndexToShow_ < 0)
|
|
for (const LocalPairConfig& lpc : localPairCfg_)
|
|
{
|
|
baseFolderPaths.push_back(createAbstractPath(lpc.folderPathPhraseLeft));
|
|
baseFolderPaths.push_back(createAbstractPath(lpc.folderPathPhraseRight));
|
|
}
|
|
else
|
|
{
|
|
baseFolderPaths.push_back(createAbstractPath(localPairCfg_[selectedPairIndexToShow_].folderPathPhraseLeft));
|
|
baseFolderPaths.push_back(createAbstractPath(localPairCfg_[selectedPairIndexToShow_].folderPathPhraseRight));
|
|
}
|
|
if (!sanitizeFilter(filterCfg, baseFolderPaths, this))
|
|
{
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::filter));
|
|
m_textCtrlExclude->SetFocus();
|
|
return false;
|
|
}
|
|
|
|
if (syncCfg && syncCfg->deletionVariant == DeletionVariant::versioning)
|
|
{
|
|
if (AFS::isNullPath(createAbstractPath(syncCfg->versioningFolderPhrase)))
|
|
{
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync));
|
|
showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a target folder.")));
|
|
//don't show error icon to follow "Windows' encouraging tone"
|
|
m_versioningFolderPath->SetFocus();
|
|
return false;
|
|
}
|
|
m_versioningFolderPath->getHistory()->addItem(syncCfg->versioningFolderPhrase);
|
|
|
|
if (syncCfg->versioningStyle != VersioningStyle::replace &&
|
|
syncCfg->versionMaxAgeDays > 0 &&
|
|
syncCfg->versionCountMin > 0 &&
|
|
syncCfg->versionCountMax > 0 &&
|
|
syncCfg->versionCountMin >= syncCfg->versionCountMax)
|
|
{
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync));
|
|
showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Minimum version count must be smaller than maximum count.")));
|
|
m_spinCtrlVersionCountMin->SetFocus();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (selectedPairIndexToShow_ < 0)
|
|
{
|
|
if (AFS::isNullPath(createAbstractPath(miscCfg.altLogFolderPathPhrase)) &&
|
|
!miscCfg.altLogFolderPathPhrase.empty())
|
|
{
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync));
|
|
showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a folder path.")));
|
|
m_logFolderPath->SetFocus();
|
|
return false;
|
|
}
|
|
m_logFolderPath->getHistory()->addItem(miscCfg.altLogFolderPathPhrase);
|
|
|
|
if (!miscCfg.emailNotifyAddress.empty() &&
|
|
!isValidEmail(trimCpy(miscCfg.emailNotifyAddress)))
|
|
{
|
|
m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync));
|
|
showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a valid email address.")));
|
|
m_comboBoxEmail->SetFocus();
|
|
return false;
|
|
}
|
|
m_comboBoxEmail ->addItemHistory();
|
|
m_comboBoxPostSyncCommand->addItemHistory();
|
|
}
|
|
}
|
|
//-------------------------------------------------------------
|
|
|
|
if (selectedPairIndexToShow_ < 0)
|
|
{
|
|
globalPairCfg_.cmpCfg = *compCfg;
|
|
globalPairCfg_.syncCfg = *syncCfg;
|
|
globalPairCfg_.filter = filterCfg;
|
|
}
|
|
else
|
|
{
|
|
localPairCfg_[selectedPairIndexToShow_].localCmpCfg = compCfg;
|
|
localPairCfg_[selectedPairIndexToShow_].localSyncCfg = syncCfg;
|
|
localPairCfg_[selectedPairIndexToShow_].localFilter = filterCfg;
|
|
}
|
|
globalPairCfg_.miscCfg = miscCfg;
|
|
|
|
selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED;
|
|
//m_listBoxFolderPair->SetSelection(wxNOT_FOUND); not needed, selectedPairIndexToShow has parameter ownership
|
|
return true;
|
|
}
|
|
|
|
|
|
void ConfigDialog::onAddNotes(wxCommandEvent& event)
|
|
{
|
|
showNotesPanel_ = true;
|
|
updateMiscGui();
|
|
|
|
//=> enlarge dialog height!
|
|
GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize()
|
|
|
|
m_textCtrNotes->SetFocus();
|
|
}
|
|
|
|
|
|
void ConfigDialog::onOkay(wxCommandEvent& event)
|
|
{
|
|
if (!unselectFolderPairConfig(true /*validateParams*/))
|
|
return;
|
|
|
|
globalPairCfgOut_ = globalPairCfg_;
|
|
localPairCfgOut_ = localPairCfg_;
|
|
|
|
EndModal(static_cast<int>(ConfirmationButton::accept));
|
|
}
|
|
|
|
|
|
//save global settings: should NOT be impacted by OK/Cancel
|
|
ConfigDialog::~ConfigDialog()
|
|
{
|
|
versioningFolderHistoryOut_ = m_versioningFolderPath->getHistory()->getList();
|
|
logFolderHistoryOut_ = m_logFolderPath ->getHistory()->getList();
|
|
|
|
commandHistoryOut_ = m_comboBoxPostSyncCommand->getHistory();
|
|
emailHistoryOut_ = m_comboBoxEmail ->getHistory();
|
|
}
|
|
}
|
|
|
|
//########################################################################################
|
|
|
|
ConfirmationButton fff::showSyncConfigDlg(wxWindow* parent,
|
|
SyncConfigPanel panelToShow,
|
|
int localPairIndexToShow, bool showMultipleCfgs,
|
|
|
|
GlobalPairConfig& globalPairCfg,
|
|
std::vector<LocalPairConfig>& localPairCfg,
|
|
|
|
FilterConfig& defaultFilter,
|
|
std::vector<Zstring>& versioningFolderHistory, Zstring& versioningFolderLastSelected,
|
|
std::vector<Zstring>& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase,
|
|
size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected,
|
|
std::vector<Zstring>& emailHistory, size_t emailHistoryMax,
|
|
std::vector<Zstring>& commandHistory, size_t commandHistoryMax)
|
|
{
|
|
|
|
ConfigDialog syncDlg(parent,
|
|
panelToShow,
|
|
localPairIndexToShow, showMultipleCfgs,
|
|
globalPairCfg,
|
|
localPairCfg,
|
|
defaultFilter,
|
|
versioningFolderHistory, versioningFolderLastSelected,
|
|
logFolderHistory, logFolderLastSelected, globalLogFolderPhrase,
|
|
folderHistoryMax, sftpKeyFileLastSelected,
|
|
emailHistory,
|
|
emailHistoryMax,
|
|
commandHistory,
|
|
commandHistoryMax);
|
|
return static_cast<ConfirmationButton>(syncDlg.ShowModal());
|
|
}
|