// ***************************************************************************** // * 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 #include #include #include #include #include #include #include #include #include #include #include #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>& 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 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& 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 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(displayPath))); contains(includeFilterNorm, pathNormPf) || //perf!? contains(excludeFilterNorm, pathNormPf)) // folderPathsPf.push_back(pathNormPf); removeDuplicates(folderPathsPf); } std::vector> 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(__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(from) + L'\n'; else detailsMsg += utfTo(from) + L' ' + arrowRight + L' ' + utfTo(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& localPairCfg, FilterConfig& defaultFilter, std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, std::vector& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, std::vector& emailHistory, size_t emailHistoryMax, std::vector& commandHistory, size_t commandHistoryMax); ~ConfigDialog(); private: void onOkay (wxCommandEvent& event) override; void onCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton::cancel)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast(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 getCompConfig() const; void setCompConfig(const CompConfig* compCfg); void updateCompGui(); CompareVariant localCmpVar_ = CompareVariant::timeSize; std::set devicesForEdit_; //helper data for deviceParallelOps std::map 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(event)); } void onFilterDefaultContextMouse(wxMouseEvent& event) override { onFilterDefaultContext(static_cast(event)); } void onFilterDefaultContext(wxEvent& event); FilterConfig getFilterConfig() const; void setFilterConfig(const FilterConfig& filter); void updateFilterGui(); EnumDescrList 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 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 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 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 getDeviceParallelOps_; const std::function setDeviceParallelOps_; FolderSelector versioningFolder_; EnumDescrList 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 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& localPairCfgOut_; //output parameters (global) -> ignores OK/Cancel FilterConfig& defaultFilterOut_; std::vector& versioningFolderHistoryOut_; std::vector& logFolderHistoryOut_; std::vector& emailHistoryOut_; std::vector& commandHistoryOut_; //working copy of ALL config parameters: only one folder pair is selected at a time! GlobalPairConfig globalPairCfg_; std::vector 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& localPairCfg, FilterConfig& defaultFilter, std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, std::vector& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, std::vector& emailHistory, size_t emailHistoryMax, std::vector& 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(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(ConfigTypeImage::syncGrey) + 1); m_notebook->AssignImageList(imgList.release()); //pass ownership m_notebook->SetPageText(static_cast(SyncConfigPanel::compare), _("Comparison") + L" (F6)"); m_notebook->SetPageText(static_cast(SyncConfigPanel::filter ), _("Filter") + L" (F7)"); m_notebook->SetPageText(static_cast(SyncConfigPanel::sync ), _("Synchronization") + L" (F8)"); m_notebook->ChangeSelection(static_cast(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(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(globalLogFolderPhrase_)); //1. no text shown when control is disabled! 2. apparently there's a refresh problem on GTK m_logFolderPath->setHistory(std::make_shared(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(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(panel)); (m_listBoxFolderPair->IsShown() ? static_cast(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(m_notebook->GetSelection())) { case SyncConfigPanel::compare: break; case SyncConfigPanel::filter: m_notebook->ChangeSelection(static_cast(SyncConfigPanel::compare)); break; case SyncConfigPanel::sync: m_notebook->ChangeSelection(static_cast(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(m_notebook->GetSelection())) { case SyncConfigPanel::compare: m_notebook->ChangeSelection(static_cast(SyncConfigPanel::filter)); break; case SyncConfigPanel::filter: m_notebook->ChangeSelection(static_cast(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 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(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(SyncConfigPanel::compare), static_cast(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(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(filter.includeFilter)); m_textCtrlExclude->ChangeValue(utfTo(filter.excludeFilter)); enumTimeDescr_ .set(filter.unitTimeSpan); enumMinSizeDescr_.set(filter.unitSizeMin); enumMaxSizeDescr_.set(filter.unitSizeMax); m_spinCtrlTimespan->SetValue(static_cast(filter.timeSpan)); m_spinCtrlMinSize ->SetValue(static_cast(filter.sizeMin)); m_spinCtrlMaxSize ->SetValue(static_cast(filter.sizeMax)); updateFilterGui(); } void ConfigDialog::updateFilterGui() { const FilterConfig activeCfg = getFilterConfig(); m_notebook->SetPageImage(static_cast(SyncConfigPanel::filter), static_cast(!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(&directionsCfg_.dirs)) directionsCfg_.dirs = getChangesDirDefault(*diffDirs); else { const DirectionByChange& changeDirs = std::get(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(&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(&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(&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(&directionsCfg_.dirs)); const CompareVariant activeCmpVar = m_checkBoxUseLocalCmpOptions->GetValue() ? localCmpVar_ : globalPairCfg_.cmpCfg.compareVar; return activeCmpVar == CompareVariant::content || activeCmpVar == CompareVariant::size; } std::optional 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(&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(SyncConfigPanel::sync), static_cast(syncOptionsEnabled ? ConfigTypeImage::sync: ConfigTypeImage::syncGrey)); const bool setDirsByDifferences = std::get_if(&directionsCfg_.dirs); m_checkBoxUseDatabase->SetValue(!setDirsByDifferences); //display only relevant sync options bSizerSyncDirsDiff ->Show( setDirsByDifferences); bSizerSyncDirsChanges->Show(!setDirsByDifferences); if (const DirectionByDiff* diffDirs = std::get_if(&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(directionsCfg_.dirs); updateChangeDirButtons(changeDirs, *m_bpButtonLeftCreate, *m_bpButtonLeftUpdate, *m_bpButtonLeftDelete, *m_bpButtonRightCreate, *m_bpButtonRightUpdate, *m_bpButtonRightDelete); } const bool useDatabaseFile = std::get_if(&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(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(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(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(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(devicesForEdit_.size()) - static_cast(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 (fgSizerPerf->GetItem(i * 2 )->GetWindow()); wxStaticText* staticTextDevice = dynamic_cast(fgSizerPerf->GetItem(i * 2 + 1)->GetWindow()); spinCtrlParallelOps->SetValue(static_cast(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(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& history = m_comboBoxEmail->getHistory(); !history.empty()) defaultEmail = history[0]; m_checkBoxSendEmail->SetValue(!trimCpy(miscCfg.emailNotifyAddress).empty()); m_comboBoxEmail->setValue(m_checkBoxSendEmail->GetValue() ? utfTo(miscCfg.emailNotifyAddress) : defaultEmail); emailNotifyCondition_ = miscCfg.emailNotifyCondition; //---------------------------------------------------------------------------- m_textCtrNotes->ChangeValue(utfTo(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(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 compCfg = getCompConfig(); std::optional 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 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(SyncConfigPanel::filter)); m_textCtrlExclude->SetFocus(); return false; } if (syncCfg && syncCfg->deletionVariant == DeletionVariant::versioning) { if (AFS::isNullPath(createAbstractPath(syncCfg->versioningFolderPhrase))) { m_notebook->ChangeSelection(static_cast(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(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(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(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(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& localPairCfg, FilterConfig& defaultFilter, std::vector& versioningFolderHistory, Zstring& versioningFolderLastSelected, std::vector& logFolderHistory, Zstring& logFolderLastSelected, const Zstring& globalLogFolderPhrase, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, std::vector& emailHistory, size_t emailHistoryMax, std::vector& 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(syncDlg.ShowModal()); }