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

6498 lines
288 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 "main_dlg.h"
#include <zen/format_unit.h>
#include <zen/file_access.h>
#include <zen/file_io.h>
#include <zen/file_traverser.h>
#include <zen/thread.h>
#include <zen/process_exec.h>
#include <zen/shutdown.h>
#include <zen/resolve_path.h>
#include <zen/sys_info.h>
#include <wx/colordlg.h>
#include <wx/wupdlock.h>
#include <wx/sound.h>
#include <wx/filedlg.h>
#include <wx/textdlg.h>
#include <wx/valtext.h>
#include <wx+/context_menu.h>
#include <wx+/bitmap_button.h>
#include <wx+/app_main.h>
#include <wx+/toggle_button.h>
#include <wx+/no_flicker.h>
#include <wx+/rtl.h>
#include <wx+/window_layout.h>
#include <wx+/popup_dlg.h>
#include <wx+/window_tools.h>
#include <wx+/image_resources.h>
#include "cfg_grid.h"
#include "folder_selector.h"
#include "tree_grid.h"
#include "version_check.h"
#include "gui_status_handler.h"
#include "small_dlgs.h"
#include "rename_dlg.h"
#include "folder_pair.h"
#include "search_grid.h"
#include "batch_config.h"
#include "app_icon.h"
#include "../base_tools.h"
#include "../afs/concrete.h"
#include "../afs/native.h"
#include "../base/comparison.h"
#include "../base/algorithm.h"
#include "../base/lock_holder.h"
#include "../base/icon_loader.h"
#include "../ffs_paths.h"
#include "../localization.h"
#include "../version/version.h"
#include "../afs/gdrive.h"
using namespace zen;
using namespace fff;
namespace
{
const size_t EXT_APP_MASS_INVOKE_THRESHOLD = 10; //more is likely a user mistake (Explorer uses limit of 15)
const size_t EXT_APP_MAX_TOTAL_WAIT_TIME_MS = 1000;
const int TOP_BUTTON_OPTIMAL_WIDTH_DIP = 170;
constexpr std::chrono::milliseconds LAST_USED_CFG_EXISTENCE_CHECK_TIME_MAX(500);
constexpr std::chrono::milliseconds FILE_GRID_POST_UPDATE_DELAY(400);
const ZstringView macroNameItemPath = Zstr("%item_path%");
const ZstringView macroNameItemPath2 = Zstr("%item_path2%");
const ZstringView macroNameItemPaths = Zstr("%item_paths%");
const ZstringView macroNameLocalPath = Zstr("%local_path%");
const ZstringView macroNameLocalPath2 = Zstr("%local_path2%");
const ZstringView macroNameLocalPaths = Zstr("%local_paths%");
const ZstringView macroNameItemName = Zstr("%item_name%");
const ZstringView macroNameItemName2 = Zstr("%item_name2%");
const ZstringView macroNameItemNames = Zstr("%item_names%");
const ZstringView macroNameParentPath = Zstr("%parent_path%");
const ZstringView macroNameParentPath2 = Zstr("%parent_path2%");
const ZstringView macroNameParentPaths = Zstr("%parent_paths%");
bool containsFileItemMacro(const Zstring& commandLinePhrase)
{
return contains(commandLinePhrase, macroNameItemPath ) ||
contains(commandLinePhrase, macroNameItemPath2 ) ||
contains(commandLinePhrase, macroNameItemPaths ) ||
contains(commandLinePhrase, macroNameLocalPath ) ||
contains(commandLinePhrase, macroNameLocalPath2 ) ||
contains(commandLinePhrase, macroNameLocalPaths ) ||
contains(commandLinePhrase, macroNameItemName ) ||
contains(commandLinePhrase, macroNameItemName2 ) ||
contains(commandLinePhrase, macroNameItemNames ) ||
contains(commandLinePhrase, macroNameParentPath ) ||
contains(commandLinePhrase, macroNameParentPath2) ||
contains(commandLinePhrase, macroNameParentPaths);
}
//let's NOT create wxWidgets objects statically:
wxColor getColorHighlightCompareButton() { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0, 0x80} : wxColor{236, 236, 255}; } //dark + light blue
wxColor getColorHighlightSyncButton () { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0x40, 0} : wxColor{230, 255, 215}; } //dark + light green
wxColor getColorAuiPanelCaptionText() { return wxSystemSettings::GetAppearance().IsDark() ? 0xdadada : 0xffffff; }
wxColor getColorAuiPanelCaptionBack() { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0x4f, 0x8e} : wxColor{51, 147, 223}; } //dark + medium blue
wxColor getColorAuiPanelCaptionBackGradient() { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0, 0x3d, 0x6e} : wxColor{ 0, 120, 215}; } //dark + medium blue
wxColor getColorFlashStatusInfo()
{
return enhanceContrast({31, 57, 226} /*blue*/, wxWindow::GetClassDefaultAttributes(wxWindowVariant::wxWINDOW_VARIANT_NORMAL).colBg,
5 /*contrastRatioMin*/); //W3C recommends >= 4.5
}
IconBuffer::IconSize convert(GridIconSize isize)
{
switch (isize)
{
case GridIconSize::small:
return IconBuffer::IconSize::small;
case GridIconSize::medium:
return IconBuffer::IconSize::medium;
case GridIconSize::large:
return IconBuffer::IconSize::large;
}
return IconBuffer::IconSize::small;
}
bool acceptDialogFileDrop(const std::vector<Zstring>& shellItemPaths)
{
return std::any_of(shellItemPaths.begin(), shellItemPaths.end(), [](const Zstring& shellItemPath)
{
const Zstring ext = getFileExtension(shellItemPath);
return equalAsciiNoCase(ext, "ffs_gui") ||
equalAsciiNoCase(ext, "ffs_batch");
});
}
FfsGuiConfig getDefaultGuiConfig(const FilterConfig& defaultFilter)
{
FfsGuiConfig defaultCfg;
//set default file filter: this is only ever relevant when creating new configurations!
//a default FfsGuiConfig does not need these user-specific exclusions!
defaultCfg.mainCfg.globalFilter = defaultFilter;
return defaultCfg;
}
}
//------------------------------------------------------------------
/* class hierarchy:
template<>
FolderPairPanelBasic
/|\
|
template<>
FolderPairCallback FolderPairPanelGenerated
/|\ /|\
_________|_________ ________|
| | |
FolderPairFirst FolderPairPanel
*/
template <class GuiPanel>
class fff::FolderPairCallback : public FolderPairPanelBasic<GuiPanel> //implements callback functionality to MainDialog as imposed by FolderPairPanelBasic
{
public:
FolderPairCallback(GuiPanel& basicPanel, MainDialog& mainDlg,
wxPanel& dropWindow1L,
wxPanel& dropWindow1R,
wxButton& selectFolderButtonL,
wxButton& selectFolderButtonR,
wxButton& selectSftpButtonL,
wxButton& selectSftpButtonR,
FolderHistoryBox& dirpathL,
FolderHistoryBox& dirpathR,
Zstring& folderLastSelectedL,
Zstring& folderLastSelectedR,
Zstring& sftpKeyFileLastSelected,
wxStaticText* staticTextL,
wxStaticText* staticTextR,
wxWindow* dropWindow2L,
wxWindow* dropWindow2R) :
FolderPairPanelBasic<GuiPanel>(basicPanel), //pass FolderPairPanelGenerated part...
mainDlg_(mainDlg),
folderSelectorLeft_ (&mainDlg, dropWindow1L, selectFolderButtonL, selectSftpButtonL, dirpathL, folderLastSelectedL,
sftpKeyFileLastSelected, staticTextL, dropWindow2L, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_),
folderSelectorRight_(&mainDlg, dropWindow1R, selectFolderButtonR, selectSftpButtonR, dirpathR, folderLastSelectedR,
sftpKeyFileLastSelected, staticTextR, dropWindow2R, droppedPathsFilter_, getDeviceParallelOps_, setDeviceParallelOps_)
{
folderSelectorLeft_ .setSiblingSelector(&folderSelectorRight_);
folderSelectorRight_.setSiblingSelector(&folderSelectorLeft_);
folderSelectorLeft_ .Bind(EVENT_ON_FOLDER_SELECTED, [&mainDlg](wxCommandEvent& event) { mainDlg.onFolderSelected(event); });
folderSelectorRight_.Bind(EVENT_ON_FOLDER_SELECTED, [&mainDlg](wxCommandEvent& event) { mainDlg.onFolderSelected(event); });
}
void setValues(const LocalPairConfig& lpc)
{
this->setConfig(lpc.localCmpCfg, lpc.localSyncCfg, lpc.localFilter);
folderSelectorLeft_ .setPath(lpc.folderPathPhraseLeft);
folderSelectorRight_.setPath(lpc.folderPathPhraseRight);
}
LocalPairConfig getValues() const
{
return
{
folderSelectorLeft_ .getPath(),
folderSelectorRight_.getPath(),
this->getCompConfig(),
this->getSyncConfig(),
this->getFilterConfig()
};
}
private:
MainConfiguration getMainConfig() const override { return mainDlg_.getConfig().mainCfg; }
wxWindow* getParentWindow() override { return &mainDlg_; }
void onLocalCompCfgChange () override { mainDlg_.applyCompareConfig(false /*setDefaultViewType*/); }
void onLocalSyncCfgChange () override { mainDlg_.applySyncDirections(); }
void onLocalFilterCfgChange() override { mainDlg_.applyFilterConfig(); } //re-apply filter
const std::function<bool(const std::vector<Zstring>& shellItemPaths)> droppedPathsFilter_ = [&](const std::vector<Zstring>& shellItemPaths)
{
if (acceptDialogFileDrop(shellItemPaths))
{
assert(!shellItemPaths.empty());
mainDlg_.loadConfiguration(shellItemPaths);
return false; //don't set dropped paths
}
return true; //do set dropped paths
};
const std::function<size_t(const Zstring& folderPathPhrase)> getDeviceParallelOps_ = [&](const Zstring& folderPathPhrase)
{
return getDeviceParallelOps(mainDlg_.currentCfg_.mainCfg.deviceParallelOps, folderPathPhrase);
};
const std::function<void(const Zstring& folderPathPhrase, size_t parallelOps)> setDeviceParallelOps_ = [&](const Zstring& folderPathPhrase, size_t parallelOps)
{
setDeviceParallelOps(mainDlg_.currentCfg_.mainCfg.deviceParallelOps, folderPathPhrase, parallelOps);
mainDlg_.updateUnsavedCfgStatus();
};
MainDialog& mainDlg_;
FolderSelector folderSelectorLeft_;
FolderSelector folderSelectorRight_;
};
class fff::FolderPairPanel :
public FolderPairPanelGenerated, //FolderPairPanel "owns" FolderPairPanelGenerated!
public FolderPairCallback<FolderPairPanelGenerated>
{
public:
FolderPairPanel(wxWindow* parent,
MainDialog& mainDlg,
Zstring& folderLastSelectedL,
Zstring& folderLastSelectedR,
Zstring& sftpKeyFileLastSelected) :
FolderPairPanelGenerated(parent),
FolderPairCallback<FolderPairPanelGenerated>(static_cast<FolderPairPanelGenerated&>(*this), mainDlg,
*m_panelLeft,
*m_panelRight,
*m_buttonSelectFolderLeft,
*m_buttonSelectFolderRight,
*m_bpButtonSelectAltFolderLeft,
*m_bpButtonSelectAltFolderRight,
*m_folderPathLeft,
*m_folderPathRight,
folderLastSelectedL,
folderLastSelectedR,
sftpKeyFileLastSelected,
nullptr /*staticText*/, nullptr /*staticText*/,
nullptr /*dropWindow2*/, nullptr /*dropWindow2*/) {}
};
class fff::FolderPairFirst : public FolderPairCallback<MainDialogGenerated>
{
public:
FolderPairFirst(MainDialog& mainDlg,
Zstring& folderLastSelectedL,
Zstring& folderLastSelectedR,
Zstring& sftpKeyFileLastSelected) :
FolderPairCallback<MainDialogGenerated>(mainDlg, mainDlg,
*mainDlg.m_panelTopLeft,
*mainDlg.m_panelTopRight,
*mainDlg.m_buttonSelectFolderLeft,
*mainDlg.m_buttonSelectFolderRight,
*mainDlg.m_bpButtonSelectAltFolderLeft,
*mainDlg.m_bpButtonSelectAltFolderRight,
*mainDlg.m_folderPathLeft,
*mainDlg.m_folderPathRight,
folderLastSelectedL,
folderLastSelectedR,
sftpKeyFileLastSelected,
mainDlg.m_staticTextResolvedPathL,
mainDlg.m_staticTextResolvedPathR,
&mainDlg.m_gridMainL->getMainWin(),
&mainDlg.m_gridMainR->getMainWin()) {}
};
//---------------------------------------------------------------------------------------------
class MainDialog::UiInputDisabler
{
public:
UiInputDisabler(MainDialog& mainDlg, bool enableAbort) : mainDlg_(mainDlg)
{
disableGuiElementsImpl(enableAbort);
}
~UiInputDisabler()
{
if (!dismissed_ )
{
wxTheApp->Yield(); //GUI update before enabling buttons again: prevent strange behaviour of delayed button clicks
enableGuiElementsImpl();
}
}
void dismiss() { dismissed_ = true; }
private:
UiInputDisabler (const UiInputDisabler&) = delete;
UiInputDisabler& operator=(const UiInputDisabler&) = delete;
void disableGuiElementsImpl(bool enableAbort); //dis-/enable all elements (except abort button) that might receive unwanted user input
void enableGuiElementsImpl(); //during long-running processes: comparison, deletion
MainDialog& mainDlg_;
bool dismissed_ = false;
};
void MainDialog::UiInputDisabler::disableGuiElementsImpl(bool enableAbort)
{
//disables all elements (except abort button) that might receive user input during long-running processes:
//when changing consider: comparison, synchronization, manual deletion
//OS X: wxWidgets portability promise is again a mess: http://wxwidgets.10942.n7.nabble.com/Disable-panel-and-appropriate-children-windows-linux-macos-td35357.html
mainDlg_.EnableCloseButton(false); //closing main dialog is not allowed during synchronization! crash!
//EnableCloseButton(false) just does not work reliably!
//- Windows: dialog can still be closed by clicking the task bar preview window with the middle mouse button or by pressing ALT+F4!
//- OS X: Quit/Preferences menu items still enabled during sync,
// ([[m_macWindow standardWindowButton:NSWindowCloseButton] setEnabled:enable]) does not stick after calling Maximize() ([m_macWindow zoom:nil])
//- Linux: it just works! :)
for (size_t pos = 0; pos < mainDlg_.m_menubar->GetMenuCount(); ++pos)
mainDlg_.m_menubar->EnableTop(pos, false);
if (enableAbort)
{
mainDlg_.m_buttonCancel->Enable();
mainDlg_.m_buttonCancel->Show();
//if (m_buttonCancel->IsShownOnScreen()) -> needed?
mainDlg_.m_buttonCancel->SetFocus();
mainDlg_.m_buttonCompare->Disable();
mainDlg_.m_buttonCompare->Hide();
mainDlg_.m_panelTopButtons->Layout();
mainDlg_.m_bpButtonCmpConfig ->Disable();
mainDlg_.m_bpButtonCmpContext ->Disable();
mainDlg_.m_bpButtonFilter ->Disable();
mainDlg_.m_bpButtonFilterContext->Disable();
mainDlg_.m_bpButtonSyncConfig ->Disable();
mainDlg_.m_bpButtonSyncContext->Disable();
mainDlg_.m_buttonSync ->Disable();
}
else
mainDlg_.m_panelTopButtons->Disable();
mainDlg_.m_panelDirectoryPairs->Disable();
mainDlg_.m_gridOverview ->Disable();
mainDlg_.m_panelCenter ->Disable();
mainDlg_.m_panelSearch ->Disable();
mainDlg_.m_panelLog ->Disable();
mainDlg_.m_panelConfig ->Disable();
mainDlg_.m_panelViewFilter ->Disable();
mainDlg_.Refresh(); //wxWidgets fails to do this automatically for child items of disabled windows
}
void MainDialog::UiInputDisabler::enableGuiElementsImpl()
{
//wxGTK, yet another QOI issue: some stupid bug keeps moving main dialog to top!!
mainDlg_.EnableCloseButton(true);
for (size_t pos = 0; pos < mainDlg_.m_menubar->GetMenuCount(); ++pos)
mainDlg_.m_menubar->EnableTop(pos, true);
mainDlg_.m_buttonCancel->Disable();
mainDlg_.m_buttonCancel->Hide();
mainDlg_.m_buttonCompare->Enable();
mainDlg_.m_buttonCompare->Show();
mainDlg_.m_panelTopButtons->Layout();
mainDlg_.m_bpButtonCmpConfig ->Enable();
mainDlg_.m_bpButtonCmpContext ->Enable();
mainDlg_.m_bpButtonFilter ->Enable();
mainDlg_.m_bpButtonFilterContext->Enable();
mainDlg_.m_bpButtonSyncConfig ->Enable();
mainDlg_.m_bpButtonSyncContext->Enable();
mainDlg_.m_buttonSync ->Enable();
mainDlg_.m_panelTopButtons->Enable();
mainDlg_.m_panelDirectoryPairs->Enable();
mainDlg_.m_gridOverview ->Enable();
mainDlg_.m_panelCenter ->Enable();
mainDlg_.m_panelSearch ->Enable();
mainDlg_.m_panelLog ->Enable();
mainDlg_.m_panelConfig ->Enable();
mainDlg_.m_panelViewFilter ->Enable();
mainDlg_.Refresh();
//mainDlg_.auiMgr_.Update(); needed on macOS; 2021-02-01: apparently not anymore!
}
//---------------------------------------------------------------------------------------------
namespace
{
void updateTopButton(wxBitmapButton& btn,
const wxImage& img,
const wxString& varName, const char* varIconName /*optional*/,
const char* extraIconName /*optional*/,
const wxColor& highlightCol /*optional*/)
{
const wxColor backCol = highlightCol.IsOk() ? highlightCol : btn.GetBackgroundColour();
wxImage iconImg = highlightCol.IsOk() ? img : greyScale(img);
wxImage btnLabelImg = createImageFromText(btn.GetLabelText(), btn.GetFont(),
enhanceContrast(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT), backCol, 4.5 /*contrastRatioMin*/));
wxImage varLabelImg = createImageFromText(varName, wxNORMAL_FONT->Bold(),
enhanceContrast(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT), backCol, 4.5 /*contrastRatioMin*/));
wxImage varImg = varLabelImg;
if (varIconName)
{
wxImage varIcon = mirrorIfRtl(loadImage(varIconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize())));
varImg = btn.GetLayoutDirection() != wxLayout_RightToLeft ?
stackImages(varLabelImg, varIcon, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) :
stackImages(varIcon, varLabelImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5));
}
wxImage btnImg = stackImages(btnLabelImg, varImg, ImageStackLayout::vertical, ImageStackAlignment::center);
btnImg = btn.GetLayoutDirection() != wxLayout_RightToLeft ?
stackImages(iconImg, btnImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) :
stackImages(btnImg, iconImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5));
if (extraIconName)
{
const wxImage exImg = loadImage(extraIconName, dipToScreen(20));
btnImg = btn.GetLayoutDirection() != wxLayout_RightToLeft ?
stackImages(btnImg, exImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5)) :
stackImages(exImg, btnImg, ImageStackLayout::horizontal, ImageStackAlignment::center, dipToScreen(5));
}
wxSize btnSize = btnImg.GetSize() + wxSize(dipToScreen(5 + 5), 0) /*border space*/;
btnSize.x = std::max(btnSize.x, dipToScreen(TOP_BUTTON_OPTIMAL_WIDTH_DIP));
btnSize.y += dipToScreen(2 + 2); //border space
btnImg = resizeCanvas(btnImg, btnSize, wxALIGN_CENTER);
if (highlightCol.IsOk())
btnImg = layOver(rectangleImage(btnImg.GetSize(), highlightCol), btnImg, wxALIGN_CENTER);
setImage(btn, btnImg);
}
}
//##################################################################################################################################
void MainDialog::create(const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath)
{
std::vector<Zstring> cfgFilePaths = globalCfg.mainDlg.config.lastUsedFiles;
//------------------------------------------------------------------------------------------
//check existence of all files in parallel:
AsyncFirstResult<std::false_type> firstUnavailableFile;
for (const Zstring& filePath : cfgFilePaths)
firstUnavailableFile.addJob([filePath]() -> std::optional<std::false_type>
{
try
{
assert(!filePath.empty());
getItemType(filePath); //throw FileError
return {};
}
catch (FileError&) { return std::false_type(); }
});
//potentially slow network access: give all checks 500ms to finish
const bool allFilesAvailable = firstUnavailableFile.timedWait(LAST_USED_CFG_EXISTENCE_CHECK_TIME_MAX) && //false: time elapsed
!firstUnavailableFile.get(); //no missing
if (!allFilesAvailable)
cfgFilePaths.clear(); //we do NOT want to show an error due to last config file missing on application start!
//------------------------------------------------------------------------------------------
if (cfgFilePaths.empty())
try //3. ...to load auto-save config (should not block)
{
const Zstring lastRunConfigFilePath = getLastRunConfigPath();
getItemType(lastRunConfigFilePath); //throw FileError
cfgFilePaths.push_back(lastRunConfigFilePath);
}
catch (FileError&) {} //not-existing/access error? => user may click on [Last session] later
FfsGuiConfig guiCfg = getDefaultGuiConfig(globalCfg.defaultFilter);
if (!cfgFilePaths.empty())
try
{
std::wstring warningMsg;
std::tie(guiCfg, warningMsg) = readAnyConfig(cfgFilePaths); //throw FileError
if (!warningMsg.empty())
showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg));
//what about showing as changed config on parsing errors????
}
catch (const FileError& e)
{
showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
}
//------------------------------------------------------------------------------------------
create(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath, false /*startComparison*/);
}
void MainDialog::create(const FfsGuiConfig& guiCfg, const std::vector<Zstring>& cfgFilePaths,
const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath,
bool startComparison)
{
MainDialog* mainDlg = new MainDialog(guiCfg, cfgFilePaths, globalCfg, globalCfgFilePath);
//avoid Windows 10 white flash when showing dark mode window: https://chromium-review.googlesource.com/c/chromium/src/+/6092335
#if 0 //variant 1: works, but no fade-in animation
#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")
BOOL cloak = true; //requires Windows 8 and later
bool cloaked = SUCCEEDED(::DwmSetWindowAttribute(mainDlg->GetHWND(), DWMWA_CLOAK, &cloak, sizeof(cloak)));
mainDlg->Show();
if (cloaked)
{
/*BOOL success = */ ::UpdateWindow(mainDlg->GetHWND());
BOOL cloak = false;
/*HRESULT hr = */::DwmSetWindowAttribute(mainDlg->GetHWND(), DWMWA_CLOAK, &cloak, sizeof(cloak));
}
#endif
#if 0 //variant 2: works, but different fade-in animation
mainDlg->Iconize();
mainDlg->Show();
mainDlg->Iconize(false);
#endif
mainDlg->Show();
//------------------------------------------------------------------------------------------
//construction complete! trigger special events:
//------------------------------------------------------------------------------------------
//show welcome dialog after FreeFileSync update => show *before* any other dialogs
if (mainDlg->globalCfg_.welcomeDialogLastVersion != ffsVersion)
{
mainDlg->globalCfg_.welcomeDialogLastVersion = ffsVersion;
//showAboutDialog(mainDlg); => dialog centered incorrectly (Centos)
//mainDlg->CallAfter([mainDlg] { showAboutDialog(mainDlg); }); => dialog centered incorrectly (Windows, Centos)
mainDlg->guiQueue_.processAsync([] {}, [mainDlg]() { showAboutDialog(mainDlg); }); //apparently oh-kay?
}
//if FFS is started with a *.ffs_gui file as commandline parameter AND all directories contained exist, comparison shall be started right away
if (startComparison)
{
const MainConfiguration currMainCfg = mainDlg->getConfig().mainCfg;
//------------------------------------------------------------------------------------------
//harmonize checks with comparison.cpp:: checkForIncompleteInput()
//we're really doing two checks: 1. check directory existence 2. check config validity -> don't mix them!
bool havePartialPair = false;
bool haveFullPair = false;
std::vector<AbstractPath> folderPathsToCheck;
auto addFolderCheck = [&](const LocalPairConfig& lpc)
{
const AbstractPath folderPathL = createAbstractPath(lpc.folderPathPhraseLeft);
const AbstractPath folderPathR = createAbstractPath(lpc.folderPathPhraseRight);
if (AFS::isNullPath(folderPathL) != AFS::isNullPath(folderPathR)) //only skip check if both sides are empty!
havePartialPair = true;
else if (!AFS::isNullPath(folderPathL))
haveFullPair = true;
if (!AFS::isNullPath(folderPathL))
folderPathsToCheck.push_back(folderPathL); //noexcept
if (!AFS::isNullPath(folderPathR))
folderPathsToCheck.push_back(folderPathR); //noexcept
};
addFolderCheck(currMainCfg.firstPair);
for (const LocalPairConfig& lpc : currMainCfg.additionalPairs)
addFolderCheck(lpc);
//------------------------------------------------------------------------------------------
if (havePartialPair != haveFullPair) //either all pairs full or all half-filled -> validity check!
{
//check existence of all directories in parallel!
AsyncFirstResult<std::false_type> firstMissingDir;
for (const AbstractPath& folderPath : folderPathsToCheck)
firstMissingDir.addJob([folderPath]() -> std::optional<std::false_type>
{
try
{
if (AFS::getItemType(folderPath) != AFS::ItemType::file) //throw FileError
return {};
}
catch (FileError&) {}
return std::false_type();
});
const bool startComparisonNow = !firstMissingDir.timedWait(std::chrono::milliseconds(500)) || //= no result yet => start comparison anyway!
!firstMissingDir.get(); //= all directories exist
if (startComparisonNow) //simulate click on "compare"
{
wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED);
mainDlg->m_buttonCompare->Command(dummy2);
}
}
}
}
MainDialog::MainDialog(const FfsGuiConfig& guiCfg, const std::vector<Zstring>& cfgFilePaths,
const GlobalConfig& globalCfg, const Zstring& globalCfgFilePath) :
MainDialogGenerated(nullptr),
globalCfgFilePath_(globalCfgFilePath),
folderHistoryLeft_ (std::make_shared<HistoryList>(globalCfg.mainDlg.folderHistoryLeft, globalCfg.folderHistoryMax)),
folderHistoryRight_(std::make_shared<HistoryList>(globalCfg.mainDlg.folderHistoryRight, globalCfg.folderHistoryMax)),
imgTrashSmall_([]
{
try { return extractWxImage(fff::getTrashIcon(dipToScreen(getMenuIconDipSize()))); /*throw SysError*/ }
catch (SysError&) { assert(false); return loadImage("delete_recycler", dipToScreen(getMenuIconDipSize())); }
}
()),
imgFileManagerSmall_([]
{
try { return extractWxImage(fff::getFileManagerIcon(dipToScreen(getMenuIconDipSize()))); /*throw SysError*/ }
catch (SysError&) { assert(false); return loadImage("file_manager", dipToScreen(getMenuIconDipSize())); }
}())
{
SetSizeHints(dipToWxsize(640), dipToWxsize(400));
//setup sash: detach + reparent:
m_splitterMain->SetSizer(nullptr); //alas wxFormbuilder doesn't allow us to have child windows without a sizer, so we have to remove it here
m_splitterMain->setupWindows(m_gridMainL, m_gridMainC, m_gridMainR);
setRelativeFontSize(*m_buttonCompare, 1.4);
setRelativeFontSize(*m_buttonSync, 1.4);
setRelativeFontSize(*m_buttonCancel, 1.4);
SetIcon(getFfsIcon()); //set application icon
auto generateSaveAsImage = [](const char* layoverName)
{
const wxSize oldSize = loadImage("cfg_save").GetSize();
wxImage backImg = loadImage("cfg_save", oldSize.GetWidth() * 9 / 10);
backImg = resizeCanvas(backImg, oldSize, wxALIGN_BOTTOM | wxALIGN_LEFT);
return layOver(backImg, loadImage(layoverName, backImg.GetWidth() * 7 / 10), wxALIGN_TOP | wxALIGN_RIGHT);
};
setImage(*m_bpButtonCmpConfig, loadImage("options_compare"));
setImage(*m_bpButtonSyncConfig, loadImage("options_sync"));
setImage(*m_bpButtonCmpContext, mirrorIfRtl(loadImage("button_arrow_right")));
setImage(*m_bpButtonFilterContext, mirrorIfRtl(loadImage("button_arrow_right")));
setImage(*m_bpButtonSyncContext, mirrorIfRtl(loadImage("button_arrow_right")));
setImage(*m_bpButtonViewFilterContext, mirrorIfRtl(loadImage("button_arrow_right")));
//m_bpButtonNew ->set dynamically
setImage(*m_bpButtonOpen, loadImage("cfg_load"));
//m_bpButtonSave ->set dynamically
setImage(*m_bpButtonSaveAs, generateSaveAsImage("start_sync"));
setImage(*m_bpButtonSaveAsBatch, generateSaveAsImage("cfg_batch"));
setImage(*m_bpButtonAddPair, loadImage("item_add"));
setImage(*m_bpButtonHideSearch, loadImage("close_panel"));
//setImage(*m_bpButtonToggleLog, loadImage("log_file"));
m_bpButtonFilter ->SetMinSize({screenToWxsize(loadImage("options_filter").GetWidth()) + dipToWxsize(27), -1}); //make the filter button wider
m_textCtrlSearchTxt->SetMinSize({dipToWxsize(220), -1});
//----------------------------------------------------------------------------------------
wxImage labelImage = createImageFromText(_("Select view:"), m_bpButtonViewType->GetFont(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT));
labelImage = resizeCanvas(labelImage, labelImage.GetSize() + wxSize(dipToScreen(10), 0), wxALIGN_CENTER); //add border space
auto generateViewTypeImage = [&](const char* imgName)
{
return stackImages(labelImage, mirrorIfRtl(loadImage(imgName)), ImageStackLayout::vertical, ImageStackAlignment::center);
};
m_bpButtonViewType->init(generateViewTypeImage("viewtype_sync_action"),
generateViewTypeImage("viewtype_cmp_result"));
//tooltip is updated dynamically in setViewTypeSyncAction()
//----------------------------------------------------------------------------------------
m_bpButtonShowExcluded ->SetToolTip(_("Show filtered or temporarily excluded files"));
m_bpButtonShowEqual ->SetToolTip(_("Show files that are equal"));
m_bpButtonShowConflict ->SetToolTip(_("Show conflicts"));
m_bpButtonShowCreateLeft ->SetToolTip(_("Show files that will be created on the left side"));
m_bpButtonShowCreateRight->SetToolTip(_("Show files that will be created on the right side"));
m_bpButtonShowDeleteLeft ->SetToolTip(_("Show files that will be deleted on the left side"));
m_bpButtonShowDeleteRight->SetToolTip(_("Show files that will be deleted on the right side"));
m_bpButtonShowUpdateLeft ->SetToolTip(_("Show files that will be updated on the left side"));
m_bpButtonShowUpdateRight->SetToolTip(_("Show files that will be updated on the right side"));
m_bpButtonShowDoNothing ->SetToolTip(_("Show files that won't be copied"));
m_bpButtonShowLeftOnly ->SetToolTip(_("Show files that exist on left side only"));
m_bpButtonShowRightOnly ->SetToolTip(_("Show files that exist on right side only"));
m_bpButtonShowLeftNewer ->SetToolTip(_("Show files that are newer on left"));
m_bpButtonShowRightNewer->SetToolTip(_("Show files that are newer on right"));
m_bpButtonShowDifferent ->SetToolTip(_("Show files that are different"));
//----------------------------------------------------------------------------------------
const wxImage& imgFile = IconBuffer::genericFileIcon(IconBuffer::IconSize::small);
const wxImage& imgDir = IconBuffer::genericDirIcon (IconBuffer::IconSize::small);
//init log panel
setRelativeFontSize(*m_staticTextSyncResult, 1.5);
setImage(*m_bitmapItemStat, imgFile);
wxImage imgTime = loadImage("time", -1 /*maxWidth*/, imgFile.GetHeight());
setImage(*m_bitmapTimeStat, imgTime);
m_bitmapTimeStat->SetMinSize({-1, screenToWxsize(imgFile.GetHeight())});
logPanel_ = new LogPanel(m_panelLog); //pass ownership
bSizerLog->Add(logPanel_, 1, wxEXPAND);
setLastOperationLog(ProcessSummary(), nullptr /*errorLog*/);
//we have to use the OS X naming convention by default, because wxMac permanently populates the display menu when the wxMenuItem is created for the first time!
//=> other wx ports are not that badly programmed; therefore revert:
assert(m_menuItemOptions->GetItemLabel() == _("&Preferences") + L"\tCtrl+,"); //"Ctrl" is automatically mapped to command button!
m_menuItemOptions->SetItemLabel(_("&Options"));
//---------------- support for dockable gui style --------------------------------
bSizerPanelHolder->Detach(m_panelTopButtons);
bSizerPanelHolder->Detach(m_panelLog);
bSizerPanelHolder->Detach(m_panelDirectoryPairs);
bSizerPanelHolder->Detach(m_gridOverview);
bSizerPanelHolder->Detach(m_panelCenter);
bSizerPanelHolder->Detach(m_panelConfig);
bSizerPanelHolder->Detach(m_panelViewFilter);
auiMgr_.SetDockSizeConstraint(1 /*width_pct*/, 1 /*height_pct*/); //get rid: interferes with programmatic layout changes + doesn't limit what user can do
auiMgr_.SetManagedWindow(this);
auiMgr_.SetFlags(wxAUI_MGR_DEFAULT | wxAUI_MGR_LIVE_RESIZE);
auiMgr_.Bind(wxEVT_AUI_PANE_CLOSE, [](wxAuiManagerEvent& event)
{
//wxAuiManager::ClosePane already calls wxAuiManager::RestorePane if wxAuiPaneInfo::IsMaximized
if (wxAuiPaneInfo* pi = event.GetPane())
if (!pi->IsMaximized())
pi->best_size = pi->rect.GetSize(); //ensure current window sizes will be used when pane is shown again:
assert(event.GetPane()->rect != wxSize());
});
//daily WTF: wxAuiManager ignores old directory pane size in wxAuiPaneInfo::rect
//and calculates new window sizes based on best_size/min_size during wxEVT_AUI_PANE_RESTORE!
auiMgr_.Bind(wxEVT_AUI_PANE_MAXIMIZE, [this](wxAuiManagerEvent& event)
{
wxAuiPaneInfo& dirPane = auiMgr_.GetPane(m_panelDirectoryPairs);
wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog);
assert(event.GetPane() == &logPane);
//ensure current window sizes will be used during wxEVT_AUI_PANE_RESTORE:
dirPane.best_size = dirPane.rect.GetSize();
logPane.best_size = logPane.rect.GetSize();
assert(dirPane.rect != wxSize());
assert(logPane.rect != wxSize());
});
auiMgr_.Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& event)
{
if (event.GetPane() == &auiMgr_.GetPane(m_panelLog))
{
event.Veto();
showLogPanel(false);
}
});
compareStatus_.emplace(*this); //integrate the compare status panel (in hidden state)
//caption required for all panes that can be manipulated by the users => used by context menu
auiMgr_.AddPane(m_panelCenter,
wxAuiPaneInfo().Name(L"CenterPanel").CenterPane().PaneBorder(false));
//set comparison button label tentatively for m_panelTopButtons to receive final height:
updateTopButton(*m_buttonCompare, loadImage("compare"), getVariantName(CompareVariant::timeSize), "cmp_time", nullptr /*extraIconName*/, wxNullColour);
m_panelTopButtons->GetSizer()->SetSizeHints(m_panelTopButtons); //~=Fit() + SetMinSize()
m_buttonCancel->SetMinSize({std::max(m_buttonCancel->GetSize().x, dipToWxsize(TOP_BUTTON_OPTIMAL_WIDTH_DIP)),
std::max(m_buttonCancel->GetSize().y, m_buttonCompare->GetSize().y)
});
auiMgr_.AddPane(m_panelTopButtons,
wxAuiPaneInfo().Name(L"TopPanel").Layer(2).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false).
PaneBorder(false).Gripper().
//BestSize(-1, m_panelTopButtons->GetSize().GetHeight() + dipToWxsize(10)).
MinSize(dipToWxsize(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight()));
//note: min height is calculated incorrectly by wxAuiManager if panes with and without caption are in the same row => use smaller min-size
auiMgr_.AddPane(compareStatus_->getAsWindow(),
wxAuiPaneInfo().Name(L"ProgressPanel").Layer(2).Top().Row(2).CaptionVisible(false).PaneBorder(false).Hide().
//wxAui does not consider the progress panel's wxRAISED_BORDER and set's too small a panel height! => use correct value from wxWindow::GetSize()
MinSize(-1, compareStatus_->getAsWindow()->GetSize().GetHeight())); //bonus: minimal height isn't a bad idea anyway
m_panelDirectoryPairs->GetSizer()->SetSizeHints(m_panelDirectoryPairs); //~=Fit() + SetMinSize()
auiMgr_.AddPane(m_panelDirectoryPairs,
wxAuiPaneInfo().Name(L"FoldersPanel").Layer(2).Top().Row(3).Caption(_("Folder Pairs")).CaptionVisible(false).PaneBorder(false).Gripper().
/* yes, m_panelDirectoryPairs's min height is overwritten in updateGuiForFolderPair(), but the default height might be wrong
after increasing text size (Win10 Settings -> Accessibility -> Text size), e.g. to 150%:
auiMgr_.LoadPerspective will load a too small "dock_size", so m_panelTopLeft/m_panelTopCenter will have squashed height */
MinSize(dipToWxsize(100), m_panelDirectoryPairs->GetSize().y).CloseButton(false));
m_panelSearch->GetSizer()->SetSizeHints(m_panelSearch); //~=Fit() + SetMinSize()
auiMgr_.AddPane(m_panelSearch,
wxAuiPaneInfo().Name(L"SearchPanel").Layer(2).Bottom().Row(3).Caption(_("Find")).CaptionVisible(false).PaneBorder(false).Gripper().
MinSize(dipToWxsize(100), m_panelSearch->GetSize().y).Hide());
auiMgr_.AddPane(m_panelLog,
wxAuiPaneInfo().Name(L"LogPanel").Layer(2).Bottom().Row(2).Caption(_("Log")).MaximizeButton().Hide().
MinSize (dipToWxsize(100), dipToWxsize(100)).
BestSize(dipToWxsize(600), dipToWxsize(300)));
m_panelViewFilter->GetSizer()->SetSizeHints(m_panelViewFilter); //~=Fit() + SetMinSize()
auiMgr_.AddPane(m_panelViewFilter,
wxAuiPaneInfo().Name(L"ViewFilterPanel").Layer(2).Bottom().Row(1).Caption(_("View Settings")).CaptionVisible(false).
PaneBorder(false).Gripper().MinSize(dipToWxsize(80), m_panelViewFilter->GetSize().y));
m_panelConfig->GetSizer()->SetSizeHints(m_panelConfig); //~=Fit() + SetMinSize()
auiMgr_.AddPane(m_panelConfig,
wxAuiPaneInfo().Name(L"ConfigPanel").Layer(3).Left().Position(1).Caption(_("Configuration")).MinSize(bSizerCfgHistoryButtons->GetSize()));
auiMgr_.AddPane(m_gridOverview,
wxAuiPaneInfo().Name(L"OverviewPanel").Layer(3).Left().Position(2).Caption(_("Overview")).
MinSize (dipToWxsize(100), dipToWxsize(100)).
BestSize(dipToWxsize(300), -1));
{
wxAuiDockArt* artProvider = auiMgr_.GetArtProvider();
wxFont font = artProvider->GetFont(wxAUI_DOCKART_CAPTION_FONT);
font.SetWeight(wxFONTWEIGHT_BOLD);
font.SetPointSize(wxNORMAL_FONT->GetPointSize()); //= larger than the wxAuiDockArt default; looks better on OS X
artProvider->SetFont(wxAUI_DOCKART_CAPTION_FONT, font);
artProvider->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, font.GetPixelSize().GetHeight() + dipToWxsize(2 + 2));
//- fix wxWidgets 3.1.0 insane color scheme
artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, getColorAuiPanelCaptionText()); //accessibility: always set both foreground AND background colors!
artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, getColorAuiPanelCaptionBack());
artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_GRADIENT_COLOUR, getColorAuiPanelCaptionBackGradient());
}
//auiMgr_.Update(); -> redundant; called by setGlobalCfgOnInit() below
defaultPerspective_ = auiMgr_.SavePerspective(); //does not need wxAuiManager::Update()!
//----------------------------------------------------------------------------------
//register view layout context menu
m_panelTopButtons->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); });
m_panelConfig ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); });
m_panelViewFilter->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); });
m_panelStatusBar ->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onSetLayoutContext(event); });
//----------------------------------------------------------------------------------
//file grid: sorting
m_gridMainL->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClickRim(event, true /*leftSide*/); });
m_gridMainR->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClickRim(event, false /*leftSide*/); });
m_gridMainC->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClickC(event); });
m_gridMainL->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContextRim(event, true /*leftSide*/); });
m_gridMainR->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContextRim(event, false /*leftSide*/); });
m_gridMainC->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContextC(event); });
//file grid: context menu
m_gridMainL->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onGridContextRim(event, true /*leftSide*/); });
m_gridMainR->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onGridContextRim(event, false /*leftSide*/); });
m_gridMainL->Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this](GridClickEvent& event) { onGridGroupContextRim(event, true /*leftSide*/); });
m_gridMainR->Bind(EVENT_GRID_MOUSE_RIGHT_DOWN, [this](GridClickEvent& event) { onGridGroupContextRim(event, false /*leftSide*/); });
m_gridMainL->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onGridDoubleClickRim(event, true /*leftSide*/); });
m_gridMainR->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onGridDoubleClickRim(event, false /*leftSide*/); });
//tree grid:
m_gridOverview->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onTreeGridContext (event); });
m_gridOverview->Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onTreeGridSelection(event); });
//cfg grid:
m_gridCfgHistory->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onCfgGridKeyEvent(event); });
m_gridCfgHistory->Bind(EVENT_GRID_SELECT_RANGE, [this](GridSelectEvent& event) { onCfgGridSelection (event); });
m_gridCfgHistory->Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onCfgGridDoubleClick (event); });
m_gridCfgHistory->Bind(EVENT_GRID_CONTEXT_MENU, [this](GridContextMenuEvent& event) { onCfgGridContext (event); });
m_gridCfgHistory->Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onCfgGridLabelContext (event); });
m_gridCfgHistory->Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onCfgGridLabelLeftClick(event); });
//----------------------------------------------------------------------------------
m_panelSearch->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onSearchPanelKeyPressed(event); });
//set tool tips with (non-translated!) short cut hint
auto setCommandToolTip = [](wxButton& btn, const wxString& label, wxString shortcut)
{
wxString tooltip = wxControl::RemoveMnemonics(label);
if (!shortcut.empty())
{
tooltip += L" (" + shortcut + L')';
}
btn.SetToolTip(tooltip);
};
setCommandToolTip(*m_bpButtonNew, _("&New"), L"Ctrl+N"); //
setCommandToolTip(*m_bpButtonOpen, _("&Open..."), L"Ctrl+O"); //
setCommandToolTip(*m_bpButtonSave, _("&Save"), L"Ctrl+S"); //reuse texts from GUI builder
setCommandToolTip(*m_bpButtonSaveAs, _("Save &as..."), L""); //
setCommandToolTip(*m_bpButtonSaveAsBatch, _("Save as &batch job..."), L""); //
setCommandToolTip(*m_bpButtonToggleLog, _("Show &log"), L"F4"); //
setCommandToolTip(*m_buttonCompare, _("Start &comparison"), L"F5"); //
setCommandToolTip(*m_bpButtonCmpConfig, _("C&omparison settings"), L"F6"); //
setCommandToolTip(*m_bpButtonSyncConfig, _("S&ynchronization settings"), L"F8"); //
setCommandToolTip(*m_buttonSync, _("Start &synchronization"), L"F9"); //
setCommandToolTip(*m_bpButtonSwapSides, _("Swap sides"), L"Ctrl+Tab");
//m_bpButtonCmpContext ->SetToolTip(m_bpButtonCmpConfig ->GetToolTipText());
//m_bpButtonSyncContext->SetToolTip(m_bpButtonSyncConfig->GetToolTipText());
setImage(*m_bitmapSmallDirectoryLeft, imgDir);
setImage(*m_bitmapSmallFileLeft, imgFile);
setImage(*m_bitmapSmallDirectoryRight, imgDir);
setImage(*m_bitmapSmallFileRight, imgFile);
//---------------------- menu bar----------------------------
setImage(*m_menuItemNew, loadImage("cfg_new", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemLoad, loadImage("cfg_load", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemSave, loadImage("cfg_save", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemSaveAsBatch, loadImage("cfg_batch", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemShowLog, loadImage("log_file", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemCompare, loadImage("compare", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemCompSettings, loadImage("options_compare", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemFilter, loadImage("options_filter", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemSyncSettings, loadImage("options_sync", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemSynchronize, loadImage("start_sync", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemOptions, loadImage("settings", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemFind, loadImage("find_sicon"));
setImage(*m_menuItemResetLayout, loadImage("reset_sicon"));
setImage(*m_menuItemHelp, loadImage("help", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemAbout, loadImage("about", dipToScreen(getMenuIconDipSize())));
setImage(*m_menuItemCheckVersionNow, loadImage("update_check", dipToScreen(getMenuIconDipSize())));
fixMenuIcons(*m_menuFile);
fixMenuIcons(*m_menuActions);
fixMenuIcons(*m_menuTools);
fixMenuIcons(*m_menuHelp);
//create language selection menu
for (const TranslationInfo& ti : getAvailableTranslations())
{
wxMenuItem* newItem = new wxMenuItem(m_menuLanguages, wxID_ANY, ti.languageName);
setImage(*newItem, loadImage(ti.languageFlag)); //GTK: set *before* inserting into menu
m_menuLanguages->Bind(wxEVT_COMMAND_MENU_SELECTED, [this, langId = ti.languageID](wxCommandEvent&) { switchProgramLanguage(langId); }, newItem->GetId());
m_menuLanguages->Append(newItem); //pass ownership
}
//set up layout items to toggle showing hidden panels
m_menuItemShowMain ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Main Bar")));
m_menuItemShowFolders ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Folder Pairs")));
m_menuItemShowViewFilter->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("View Settings")));
m_menuItemShowConfig ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Configuration")));
m_menuItemShowOverview ->SetItemLabel(replaceCpy(_("Show \"%x\""), L"%x", _("Overview")));
auto setupLayoutMenuEvent = [&](wxMenuItem* menuItem, wxWindow* panelWindow)
{
m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, [this, panelWindow](wxCommandEvent&)
{
this->auiMgr_.GetPane(panelWindow).Show();
this->auiMgr_.Update();
}, menuItem->GetId());
//"hide" menu items by default
detachedMenuItems_.insert(m_menuTools->Remove(menuItem)); //pass ownership
};
setupLayoutMenuEvent(m_menuItemShowMain, m_panelTopButtons);
setupLayoutMenuEvent(m_menuItemShowFolders, m_panelDirectoryPairs);
setupLayoutMenuEvent(m_menuItemShowViewFilter, m_panelViewFilter);
setupLayoutMenuEvent(m_menuItemShowConfig, m_panelConfig);
setupLayoutMenuEvent(m_menuItemShowOverview, m_gridOverview);
m_menuTools->Bind(wxEVT_MENU_OPEN, [this](wxMenuEvent& event) { onOpenMenuTools(event); });
//notify about (logical) application main window => program won't quit, but stay on this dialog
wxTheApp->SetTopWindow(this);
wxTheApp->SetExitOnFrameDelete(true);
//init handling of first folder pair
firstFolderPair_ = std::make_unique<FolderPairFirst>(*this,
globalCfg_.mainDlg.folderLastSelectedLeft,
globalCfg_.mainDlg.folderLastSelectedRight,
globalCfg_.sftpKeyFileLastSelected);
//init grid settings
filegrid::init(*m_gridMainL, *m_gridMainC, *m_gridMainR);
treegrid::init(*m_gridOverview);
cfggrid ::init(*m_gridCfgHistory);
//initialize and load configuration
setGlobalCfgOnInit(globalCfg); //calls auiMgr_.Update()
setConfig(guiCfg, cfgFilePaths); //expects auiMgr_.Update(): e.g. recalcMaxFolderPairsVisible()
//support for CTRL + C and DEL on grids
m_gridMainL->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainL, true /*leftSide*/); });
m_gridMainC->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainC, true /*leftSide*/); });
m_gridMainR->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onGridKeyEvent(event, *m_gridMainR, false /*leftSide*/); });
m_gridOverview->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onTreeKeyEvent(event); });
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events
//drag and drop .ffs_gui and .ffs_batch on main dialog
setupFileDrop(*this);
Bind(EVENT_DROP_FILE, [this](FileDropEvent& event) { onDialogFilesDropped(event); });
//calculate witdh of folder pair manually (if scrollbars are visible)
m_panelTopLeft->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeLeftFolderWidth(event); });
m_panelTopLeft ->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onTopFolderPairKeyEvent(event); });
m_panelTopCenter->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onTopFolderPairKeyEvent(event); });
m_panelTopRight ->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onTopFolderPairKeyEvent(event); });
//dynamically change sizer direction depending on size
m_panelTopButtons->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeTopButtonPanel(event); });
m_panelConfig ->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeConfigPanel (event); });
m_panelViewFilter->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { onResizeViewPanel (event); });
wxSizeEvent dummy3;
onResizeTopButtonPanel(dummy3); //
onResizeConfigPanel (dummy3); //call once on window creation
onResizeViewPanel (dummy3); //
const int scrollDelta = m_buttonSelectFolderLeft->GetSize().y; //more approriate than GetCharHeight() here
m_scrolledWindowFolderPairs->SetScrollRate(scrollDelta, scrollDelta);
//event handler for manual (un-)checking of rows and setting of sync direction
m_gridMainC->Bind(EVENT_GRID_CHECK_ROWS, [this](CheckRowsEvent& event) { onCheckRows (event); });
m_gridMainC->Bind(EVENT_GRID_SYNC_DIRECTION, [this](SyncDirectionEvent& event) { onSetSyncDirection(event); });
//mainly to update row label sizes...
updateGui();
//register regular check for update on next idle event
Bind(wxEVT_IDLE, &MainDialog::onStartupUpdateCheck, this);
//asynchronous call to wxWindow::Dimensions(): fix superfluous frame on right and bottom when FFS is started in fullscreen mode
Bind(wxEVT_IDLE, &MainDialog::onLayoutWindowAsync, this);
wxCommandEvent evtDummy; //call once before onLayoutWindowAsync()
onResizeLeftFolderWidth(evtDummy); //
onSystemShutdownRegister(onBeforeSystemShutdownCookie_);
//show and clear "extra" log in case of startup errors:
guiQueue_.processAsync([] { std::this_thread::sleep_for(std::chrono::milliseconds(500)); }, [this] //give worker threads some time to (potentially) log extra errors
{
if (!operationInProgress_ && folderCmp_.empty()) //don't show if main dialog is otherwise busy!
{
ErrorLog extraLog = fetchExtraLog();
try //clean up remnant logs from previous FFS runs:
{
traverseFolder(getConfigDirPath(), [&](const FileInfo& fi) //"ErrorLog 2023-07-05 105207.073.xml"
{
if (startsWith(fi.itemName, Zstr("ErrorLog ")) && endsWith(fi.itemName, Zstr(".xml"))) //case-sensitive
{
append(extraLog, loadErrorLog(fi.fullPath)); //throw FileError
removeFilePlain(fi.fullPath); //throw FileError
//yeah, "read + delete" is a bit racy...
}
}, nullptr, nullptr); //throw FileError
}
catch (const FileError& e) { logMsg(extraLog, e.toString(), MessageType::MSG_TYPE_ERROR); }
std::stable_sort(extraLog.begin(), extraLog.end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; });
if (!extraLog.empty())
{
const ErrorLogStats logCount = getStats(extraLog);
const TaskResult taskResult = logCount.errors > 0 ? TaskResult::error : (logCount.warnings > 0 ? TaskResult::warning : TaskResult::success);
setLastOperationLog({.result = taskResult}, make_shared<const ErrorLog>(std::move(extraLog)));
showLogPanel(true);
}
}
});
//scroll cfg history to last used position. We cannot do this earlier e.g. in setGlobalCfgOnInit()
//1. setConfig() indirectly calls cfggrid::addAndSelect() which changes cfg history scroll position
//2. Grid::makeRowVisible() requires final window height! => do this after window resizing is complete
if (m_gridCfgHistory->getRowCount() > 0)
m_gridCfgHistory->scrollTo(std::clamp<size_t>(globalCfg.mainDlg.config.topRowPos, //must be set *after* wxAuiManager::LoadPerspective() to have any effect
0, m_gridCfgHistory->getRowCount() - 1));
//first selected item should *always* be visible:
const std::vector<size_t> selectedRows = m_gridCfgHistory->getSelectedRows();
if (!selectedRows.empty())
{
m_gridCfgHistory->setGridCursor(selectedRows[0], GridEventPolicy::deny);
//= Grid::makeRowVisible() + set grid cursor (+ select cursor row => undo:)
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
}
//start up: user most likely wants to change config, or start comparison by pressing ENTER
m_gridCfgHistory->SetFocus();
}
MainDialog::~MainDialog()
{
std::wstring errorMsg;
try //LastRun.ffs_gui
{
writeConfig(getConfig(), lastRunConfigPath_); //throw FileError
}
catch (const FileError& e) { errorMsg += e.toString() + L"\n\n"; }
try //GlobalSettings.xml
{
writeConfig(getGlobalCfgBeforeExit(), globalCfgFilePath_); //throw FileError
}
catch (const FileError& e) { errorMsg += e.toString() + L"\n\n"; }
//don't annoy users on read-only drives: it's enough to show a single error message
if (!errorMsg.empty())
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(trimCpy(errorMsg)));
//auiMgr_.UnInit(); - "since wxWidgets 3.1.4 [...] it will be called automatically when this window is destroyed, as well as when the manager itself is."
for (wxMenuItem* item : detachedMenuItems_)
delete item; //something's got to give
//no need for wxEventHandler::Unbind(): event sources are components of this window and are destroyed, too
}
//-------------------------------------------------------------------------------------------------------------------------------------
void MainDialog::onBeforeSystemShutdown()
{
try { writeConfig(getConfig(), lastRunConfigPath_); }
catch (const FileError& e) { logExtraError(e.toString()); }
try { writeConfig(getGlobalCfgBeforeExit(), globalCfgFilePath_); }
catch (const FileError& e) { logExtraError(e.toString()); }
}
void MainDialog::onClose(wxCloseEvent& event)
{
//wxEVT_END_SESSION is already handled by application.cpp::onSystemShutdown()!
//regular destruction handling
if (event.CanVeto())
{
//=> veto all attempts to close the main window while comparison or synchronization are running:
if (operationInProgress_)
{
event.Veto();
Raise(); //=what Windows does when vetoing a close (via middle mouse on taskbar preview) while showing a modal dialog
SetFocus(); //
return;
}
const bool cancelled = !saveOldConfig(); //notify user about changed settings
if (cancelled) //...or error
{
event.Veto();
return;
}
}
Destroy();
}
void MainDialog::setGlobalCfgOnInit(const GlobalConfig& globalCfg)
{
globalCfg_ = globalCfg;
DpiLayout layout;
if (auto it = globalCfg.dpiLayouts.find(getDpiScalePercent());
it != globalCfg.dpiLayouts.end())
layout = it->second;
//caveat: set/get language asymmmetry! setLanguage(globalCfg.programLanguage); //throw FileError
//we need to set language before creating this class!
WindowLayout::setInitial(*this, {layout.mainDlg.size, layout.mainDlg.pos, layout.mainDlg.isMaximized}, {dipToWxsize(900), dipToWxsize(600)} /*defaultSize*/);
//set column attributes
m_gridMainL ->setColumnConfig(convertColAttributes(layout.fileColumnAttribsLeft, getFileGridDefaultColAttribsLeft()));
m_gridMainR ->setColumnConfig(convertColAttributes(layout.fileColumnAttribsRight, getFileGridDefaultColAttribsLeft()));
m_splitterMain->setSashOffset(globalCfg.mainDlg.sashOffset);
m_gridOverview->setColumnConfig(convertColAttributes(layout.overviewColumnAttribs, getOverviewDefaultColAttribs()));
treegrid::setShowPercentage(*m_gridOverview, globalCfg.mainDlg.overview.showPercentBar);
treegrid::getDataView(*m_gridOverview).setSortDirection(globalCfg.mainDlg.overview.lastSortColumn, globalCfg.mainDlg.overview.lastSortAscending);
//--------------------------------------------------------------------------------
//load list of configuration files
cfggrid::getDataView(*m_gridCfgHistory).set(globalCfg.mainDlg.config.fileHistory);
//globalCfg.mainDlg.cfgGridTopRowPos => defer evaluation until later within MainDialog constructor
m_gridCfgHistory->setColumnConfig(convertColAttributes(layout.configColumnAttribs, getCfgGridDefaultColAttribs()));
cfggrid::getDataView(*m_gridCfgHistory).setSortDirection(globalCfg.mainDlg.config.lastSortColumn, globalCfg.mainDlg.config.lastSortAscending);
cfggrid::setSyncOverdueDays(*m_gridCfgHistory, globalCfg.mainDlg.config.syncOverdueDays);
//m_gridCfgHistory->Refresh(); <- implicit in last call
//remove non-existent items: sufficient to call once at startup
std::vector<Zstring> cfgFilePaths;
for (const ConfigFileItem& item : globalCfg.mainDlg.config.fileHistory)
cfgFilePaths.push_back(item.cfgFilePath);
cfgHistoryRemoveObsolete(cfgFilePaths);
//are we spawning too many async jobs, considering cfgHistoryRemoveObsolete()!?
cfgHistoryUpdateNotes(cfgFilePaths);
//--------------------------------------------------------------------------------
//load list of last used folders
m_folderPathLeft ->setHistory(folderHistoryLeft_);
m_folderPathRight->setHistory(folderHistoryRight_);
//show/hide file icons
filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg.mainDlg.showIcons, convert(globalCfg.mainDlg.iconSize));
filegrid::setItemPathForm(*m_gridMainL, globalCfg.mainDlg.itemPathFormatLeftGrid);
filegrid::setItemPathForm(*m_gridMainR, globalCfg.mainDlg.itemPathFormatRightGrid);
//--------------------------------------------------------------------------------
m_checkBoxMatchCase->SetValue(globalCfg_.mainDlg.textSearchRespectCase);
//work around wxAuiManager::LoadPerspective overwriting pane captions with old values (might be different language!)
std::vector<std::pair<wxAuiPaneInfo*, wxString>> paneCaptions;
for (wxAuiPaneInfo& paneInfo : auiMgr_.GetAllPanes())
paneCaptions.emplace_back(&paneInfo, paneInfo.caption);
//compare progress dialog minimum sizes are layout-dependent + can't be changed by user => don't load stale values from config
std::vector<std::tuple<wxAuiPaneInfo*, wxSize /*min size*/, wxSize /*best size*/>> paneConstraints;
auto preserveConstraint = [&paneConstraints](wxAuiPaneInfo& pane) { paneConstraints.emplace_back(&pane, pane.min_size, pane.best_size); };
wxAuiPaneInfo& progPane = auiMgr_.GetPane(compareStatus_->getAsWindow());
preserveConstraint(progPane);
preserveConstraint(auiMgr_.GetPane(m_panelTopButtons));
preserveConstraint(auiMgr_.GetPane(m_panelDirectoryPairs));
preserveConstraint(auiMgr_.GetPane(m_panelSearch));
preserveConstraint(auiMgr_.GetPane(m_panelViewFilter));
preserveConstraint(auiMgr_.GetPane(m_panelConfig));
auiMgr_.LoadPerspective(layout.panelLayout, false /*update: don't call wxAuiManager::Update() yet*/);
//restore original captions
for (const auto& [paneInfo, caption] : paneCaptions)
paneInfo->Caption(caption);
//restore pane layout constraints
for (auto& [pane, minSize, bestSize] : paneConstraints)
{
pane->min_size = minSize;
pane->best_size = bestSize;
}
//--------------------------------------------------------------------------------
//if MainDialog::onBeforeSystemShutdown() is called while comparison is active, this panel is saved and restored as "visible"
progPane.Hide();
auiMgr_.GetPane(m_panelSearch).Hide(); //no need to show it on startup
auiMgr_.GetPane(m_panelLog ).Hide(); //
auiMgr_.Update();
}
GlobalConfig MainDialog::getGlobalCfgBeforeExit()
{
Freeze(); //no need to Thaw() again!!
recalcMaxFolderPairsVisible();
//--------------------------------------------------------------------------------
GlobalConfig globalSettings = globalCfg_;
globalSettings.programLanguage = getLanguage();
//retrieve column attributes
globalSettings.dpiLayouts[getDpiScalePercent()].fileColumnAttribsLeft = convertColAttributes<ColAttributesRim>(m_gridMainL->getColumnConfig());
globalSettings.dpiLayouts[getDpiScalePercent()].fileColumnAttribsRight = convertColAttributes<ColAttributesRim>(m_gridMainR->getColumnConfig());
globalSettings.mainDlg.sashOffset = m_splitterMain->getSashOffset();
globalSettings.dpiLayouts[getDpiScalePercent()].overviewColumnAttribs = convertColAttributes<ColumnAttribOverview>(m_gridOverview->getColumnConfig());
globalSettings.mainDlg.overview.showPercentBar = treegrid::getShowPercentage(*m_gridOverview);
const auto [sortCol, ascending] = treegrid::getDataView(*m_gridOverview).getSortConfig();
globalSettings.mainDlg.overview.lastSortColumn = sortCol;
globalSettings.mainDlg.overview.lastSortAscending = ascending;
//--------------------------------------------------------------------------------
//write list of configuration files
std::vector<ConfigFileItem> cfgHistory
{
//make sure [Last session] is always part of history list
ConfigFileItem(lastRunConfigPath_, LastRunStats{}, wxSystemSettings::GetAppearance().IsDark() ? 0xb7b7b7 : 0xdddddd /*grey from onCfgGridContext()*/)
};
for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get())
if (equalNativePath(item.cfgFilePath, lastRunConfigPath_))
cfgHistory[0] = item; //preserve users's background color choice
else
cfgHistory.push_back(item);
//trim excess elements (oldest first)
if (cfgHistory.size() > globalSettings.mainDlg.config.histItemsMax)
cfgHistory.resize(globalSettings.mainDlg.config.histItemsMax);
globalSettings.mainDlg.config.fileHistory = std::move(cfgHistory);
globalSettings.mainDlg.config.topRowPos = m_gridCfgHistory->getRowAtWinPos(0);
globalSettings.dpiLayouts[getDpiScalePercent()].configColumnAttribs = convertColAttributes<ColAttributesCfg>(m_gridCfgHistory->getColumnConfig());
globalSettings.mainDlg.config.syncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory);
std::tie(globalSettings.mainDlg.config.lastSortColumn,
globalSettings.mainDlg.config.lastSortAscending) = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection();
//--------------------------------------------------------------------------------
globalSettings.mainDlg.config.lastUsedFiles = activeConfigFiles_;
//write list of last used folders
globalSettings.mainDlg.folderHistoryLeft = folderHistoryLeft_ ->getList();
globalSettings.mainDlg.folderHistoryRight = folderHistoryRight_->getList();
globalSettings.mainDlg.textSearchRespectCase = m_checkBoxMatchCase->GetValue();
wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog);
assert(m_bpButtonToggleLog->isActive() == logPane.IsShown());
if (logPane.IsShown())
{
if (logPane.IsMaximized())
auiMgr_.RestorePane(logPane); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?)
else //ensure current window sizes will be used when pane is shown again:
logPane.best_size = logPane.rect.GetSize();
}
//else: logPane.best_size already contains non-maximized value
//auiMgr_.Update(); //[!] not needed
globalSettings.dpiLayouts[getDpiScalePercent()].panelLayout = auiMgr_.SavePerspective(); //does not need wxAuiManager::Update()!
const auto& [size, pos, isMaximized] = WindowLayout::getBeforeClose(*this); //call *after* wxAuiManager::SavePerspective()!
globalSettings.dpiLayouts[getDpiScalePercent()].mainDlg = {size, pos, isMaximized};
return globalSettings;
}
namespace
{
//user expectations for partial sync:
// 1. selected folder implies also processing child items
// 2. to-be-moved item requires also processing target item
std::vector<FileSystemObject*> expandSelectionForPartialSync(const std::vector<FileSystemObject*>& selection)
{
std::vector<FileSystemObject*> output;
for (FileSystemObject* fsObj : selection)
visitFSObjectRecursively(*fsObj, [&](FolderPair& folder) { output.push_back(&folder); },
[&](FilePair& file)
{
output.push_back(&file);
switch (file.getSyncOperation()) //evaluate comparison result and sync direction
{
case SO_MOVE_LEFT_FROM:
case SO_MOVE_LEFT_TO:
case SO_MOVE_RIGHT_FROM:
case SO_MOVE_RIGHT_TO:
if (FilePair* refFile = file.getMovePair())
output.push_back(refFile);
else assert(false);
break;
case SO_CREATE_LEFT:
case SO_CREATE_RIGHT:
case SO_DELETE_LEFT:
case SO_DELETE_RIGHT:
case SO_OVERWRITE_LEFT:
case SO_OVERWRITE_RIGHT:
case SO_RENAME_LEFT:
case SO_RENAME_RIGHT:
case SO_UNRESOLVED_CONFLICT:
case SO_DO_NOTHING:
case SO_EQUAL:
break;
}
},
[&](SymlinkPair& symlink) { output.push_back(&symlink); });
removeDuplicates(output);
return output;
}
bool selectionIncludesNonEqualItem(const std::vector<FileSystemObject*>& selection)
{
struct ItemFound {};
try
{
auto onFsItem = [](FileSystemObject& fsObj) { if (fsObj.getSyncOperation() != SO_EQUAL) throw ItemFound(); };
for (FileSystemObject* fsObj : selection)
visitFSObjectRecursively(*fsObj, onFsItem, onFsItem, onFsItem);
return false;
}
catch (ItemFound&) { return true;}
}
}
void MainDialog::setSyncDirManually(const std::vector<FileSystemObject*>& selection, SyncDirection direction)
{
if (!selectionIncludesNonEqualItem(selection))
return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled!
for (FileSystemObject* fsObj : selection)
{
setSyncDirectionRec(direction, *fsObj); //set new direction (recursively)
setActiveStatus(true, *fsObj); //works recursively for directories
}
updateGui();
}
void MainDialog::setIncludedManually(const std::vector<FileSystemObject*>& selection, bool setActive)
{
//if hidefiltered is active, there should be no filtered elements on screen => current element was filtered out
assert(m_bpButtonShowExcluded->isActive() || !setActive);
if (selection.empty())
return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled!
for (FileSystemObject* fsObj : selection)
setActiveStatus(setActive, *fsObj); //works recursively for directories
updateGuiDelayedIf(!m_bpButtonShowExcluded->isActive()); //show update GUI before removing rows
}
void MainDialog::copyGridSelectionToClipboard(const zen::Grid& grid)
{
try
{
wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based:
static_assert(std::is_same_v<wxStringImpl, std::wstring>);
if (auto prov = grid.getDataProvider())
{
std::vector<Grid::ColAttributes> colAttr = grid.getColumnConfig();
std::erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; });
for (size_t row : grid.getSelectedRows())
for (auto it = colAttr.begin(); it != colAttr.end(); ++it)
{
clipBuf += prov->getValue(row, it->type);
clipBuf += it == colAttr.end() - 1 ? L'\n' : L'\t';
}
}
setClipboardText(clipBuf);
}
catch (const std::bad_alloc& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L' ' + utfTo<std::wstring>(e.what())));
}
}
void MainDialog::copyPathsToClipboard(const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR)
{
try
{
wxString clipBuf; //perf: old wxString didn't model exponential growth, but now it's std::string-based:
static_assert(std::is_same_v<wxStringImpl, std::wstring>);
auto appendPath = [&](const AbstractPath& itemPath)
{
if (!clipBuf.empty())
clipBuf += L'\n';
clipBuf += AFS::getDisplayPath(itemPath);
};
for (const FileSystemObject* fsObj : selectionL)
//if (!fsObj->isEmpty<SelectSide::left>())
appendPath(fsObj->getAbstractPath<SelectSide::left>());
for (const FileSystemObject* fsObj : selectionR)
//if (!fsObj->isEmpty<SelectSide::right>())
appendPath(fsObj->getAbstractPath<SelectSide::right>());
setClipboardText(clipBuf);
}
catch (const std::bad_alloc& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L' ' + utfTo<std::wstring>(e.what())));
}
}
std::vector<FileSystemObject*> MainDialog::getGridSelection(bool fromLeft, bool fromRight) const
{
std::vector<size_t> selectedRows;
if (fromLeft)
append(selectedRows, m_gridMainL->getSelectedRows());
if (fromRight)
append(selectedRows, m_gridMainR->getSelectedRows());
removeDuplicates(selectedRows);
assert(std::is_sorted(selectedRows.begin(), selectedRows.end()));
return filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows);
}
std::vector<FileSystemObject*> MainDialog::getTreeSelection() const
{
std::vector<FileSystemObject*> output;
for (size_t row : m_gridOverview->getSelectedRows())
if (std::unique_ptr<TreeView::Node> node = treegrid::getDataView(*m_gridOverview).getLine(row))
{
if (auto root = dynamic_cast<const TreeView::RootNode*>(node.get()))
{
//selecting root means "select everything", *ignoring* current view filter!
for (FileSystemObject& fsObj : root->baseFolder.subfolders()) //no need to explicitly add child elements!
output.push_back(&fsObj);
for (FileSystemObject& fsObj : root->baseFolder.files())
output.push_back(&fsObj);
for (FileSystemObject& fsObj : root->baseFolder.symlinks())
output.push_back(&fsObj);
}
else if (auto dir = dynamic_cast<const TreeView::DirNode*>(node.get()))
output.push_back(&(dir->folder));
else if (auto file = dynamic_cast<const TreeView::FilesNode*>(node.get()))
append(output, file->filesAndLinks);
else assert(false);
}
return output;
}
void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR)
{
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
std::vector<const FileSystemObject*> copyLeft;
std::vector<const FileSystemObject*> copyRight;
for (const FileSystemObject* fsObj : selectionL)
if (!fsObj->isEmpty<SelectSide::left>())
copyLeft.push_back(fsObj);
for (const FileSystemObject* fsObj : selectionR)
if (!fsObj->isEmpty<SelectSide::right>())
copyRight.push_back(fsObj);
if (copyLeft.empty() && copyRight.empty())
return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled!
const int itemCount = static_cast<int>(copyLeft.size() + copyRight.size());
std::wstring itemList;
for (const FileSystemObject* fsObj : copyLeft)
itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::left>()) + L'\n';
for (const FileSystemObject* fsObj : copyRight)
itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::right>()) + L'\n';
//------------------------------------------------------------------
FocusPreserver fp;
if (showCopyToDialog(this,
itemList, itemCount,
globalCfg_.mainDlg.copyToCfg.targetFolderPath,
globalCfg_.mainDlg.copyToCfg.targetFolderLastSelected,
globalCfg_.mainDlg.copyToCfg.folderHistory, globalCfg_.folderHistoryMax,
globalCfg_.sftpKeyFileLastSelected,
globalCfg_.mainDlg.copyToCfg.keepRelPaths,
globalCfg_.mainDlg.copyToCfg.overwriteIfExists) != ConfirmationButton::accept)
return;
const auto& guiCfg = getConfig();
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileAlertPending);
try
{
fff::copyToAlternateFolder(copyLeft, copyRight,
globalCfg_.mainDlg.copyToCfg.targetFolderPath,
globalCfg_.mainDlg.copyToCfg.keepRelPaths,
globalCfg_.mainDlg.copyToCfg.overwriteIfExists,
globalCfg_.warnDlgs,
statusHandler); //throw CancelProcess
//"clearSelection" not needed/desired
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
//updateGui(); -> not needed
}
void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR, bool moveToRecycler)
{
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
std::vector<FileSystemObject*> deleteLeft = selectionL;
std::vector<FileSystemObject*> deleteRight = selectionR;
std::erase_if(deleteLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); });
std::erase_if(deleteRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); });
if (deleteLeft.empty() && deleteRight.empty())
return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled!
const int itemCount = static_cast<int>(deleteLeft.size() + deleteRight.size());
std::wstring itemList;
for (const FileSystemObject* fsObj : deleteLeft)
itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::left>()) + L'\n';
for (const FileSystemObject* fsObj : deleteRight)
itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::right>()) + L'\n';
//------------------------------------------------------------------
FocusPreserver fp;
if (showDeleteDialog(this, itemList, itemCount,
moveToRecycler) != ConfirmationButton::accept)
return;
//wxBusyCursor dummy; -> redundant: progress already shown in status bar!
const auto& guiCfg = getConfig();
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileAlertPending);
try
{
deleteFiles(deleteLeft, deleteRight,
extractDirectionCfg(folderCmp_, getConfig().mainCfg),
moveToRecycler,
globalCfg_.warnDlgs.warnRecyclerMissing,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
append(fullSyncLog_->log, r.errorLog.ref());
fullSyncLog_->totalTime += r.summary.totalTime;
//remove rows that are empty: just a beautification, invalid rows shouldn't cause issues
filegrid::getDataView(*m_gridMainC).removeInvalidRows();
updateGui();
}
void MainDialog::renameSelectedFiles(const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR)
{
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
std::vector<FileSystemObject*> renameLeft = selectionL;
std::vector<FileSystemObject*> renameRight = selectionR;
std::erase_if(renameLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); });
std::erase_if(renameRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); });
if (renameLeft.empty() && renameRight.empty())
return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled!
//------------------------------------------------------------------
std::vector<Zstring> fileNamesOld;
for (const FileSystemObject* fsObj : renameLeft)
fileNamesOld.push_back(fsObj->getItemName<SelectSide::left>());
for (const FileSystemObject* fsObj : renameRight)
fileNamesOld.push_back(fsObj->getItemName<SelectSide::right>());
FocusPreserver fp;
std::vector<Zstring> fileNamesNew;
if (showRenameDialog(this, fileNamesOld, fileNamesNew) != ConfirmationButton::accept)
return;
//wxBusyCursor dummy; -> redundant: progress already shown in status bar!
const auto& guiCfg = getConfig();
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileAlertPending);
try
{
renameItems(renameLeft, {fileNamesNew.data(), renameLeft.size()},
renameRight, {fileNamesNew.data() + renameLeft.size(), fileNamesNew.size() - renameLeft.size()},
extractDirectionCfg(folderCmp_, getConfig().mainCfg),
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
append(fullSyncLog_->log, r.errorLog.ref());
fullSyncLog_->totalTime += r.summary.totalTime;
updateGui();
}
namespace
{
template <SelectSide side>
AbstractPath getExistingParentFolder(const FileSystemObject& fsObj)
{
auto folder = dynamic_cast<const FolderPair*>(&fsObj);
if (!folder)
folder = dynamic_cast<const FolderPair*>(&fsObj.parent());
while (folder)
{
if (!folder->isEmpty<side>())
return folder->getAbstractPath<side>();
folder = dynamic_cast<const FolderPair*>(&folder->parent());
}
return fsObj.base().getAbstractPath<side>();
}
template <SelectSide side, class Function>
void extractFileDescriptor(const FileSystemObject& fsObj, Function onDescriptor)
{
if (!fsObj.isEmpty<side>())
visitFSObject(fsObj, [](const FolderPair& folder) {},
[&](const FilePair& file)
{
onDescriptor(FileDescriptor{file.getAbstractPath<side>(), file.getAttributes<side>()});
},
[](const SymlinkPair& symlink) {});
}
template <SelectSide side>
void collectNonNativeFiles(const std::vector<FileSystemObject*>& selectedRows, const TempFileBuffer& tempFileBuf,
std::set<FileDescriptor>& workLoad)
{
for (const FileSystemObject* fsObj : selectedRows)
extractFileDescriptor<side>(*fsObj, [&](const FileDescriptor& descr)
{
if (getNativeItemPath(descr.path).empty() &&
tempFileBuf.getTempPath(descr).empty()) //TempFileBuffer::createTempFiles() contract!
workLoad.insert(descr);
});
}
struct ItemPathInfo
{
Zstring itemPath;
Zstring itemPath2;
Zstring itemName;
Zstring itemName2;
Zstring parentPath;
Zstring parentPath2;
Zstring localPath;
Zstring localPath2;
};
template <SelectSide side>
std::vector<ItemPathInfo> getItemPathInfo(const std::vector<FileSystemObject*>& selection, const TempFileBuffer& tempFileBuf)
{
constexpr SelectSide side2 = getOtherSide<side>;
std::vector<ItemPathInfo> pathInfos;
for (const FileSystemObject* fsObj : selection) //context menu calls this function only if selection is not empty!
{
const AbstractPath basePath = fsObj->base().getAbstractPath<side >();
const AbstractPath basePath2 = fsObj->base().getAbstractPath<side2>();
//return paths, even if item is not (yet) existing:
const Zstring itemPath = AFS::isNullPath(basePath ) ? Zstr("") : utfTo<Zstring>(AFS::getDisplayPath(fsObj-> getAbstractPath<side >()));
const Zstring itemPath2 = AFS::isNullPath(basePath2) ? Zstr("") : utfTo<Zstring>(AFS::getDisplayPath(fsObj-> getAbstractPath<side2>()));
const Zstring itemName = AFS::isNullPath(basePath ) ? Zstr("") : AFS::getItemName (fsObj-> getAbstractPath<side >());
const Zstring itemName2 = AFS::isNullPath(basePath2) ? Zstr("") : AFS::getItemName (fsObj-> getAbstractPath<side2>());
const Zstring parentPath = AFS::isNullPath(basePath ) ? Zstr("") : utfTo<Zstring>(AFS::getDisplayPath(fsObj->parent().getAbstractPath<side >()));
const Zstring parentPath2 = AFS::isNullPath(basePath2) ? Zstr("") : utfTo<Zstring>(AFS::getDisplayPath(fsObj->parent().getAbstractPath<side2>()));
Zstring localPath;
Zstring localPath2;
if (const Zstring& nativePath = getNativeItemPath(fsObj->getAbstractPath<side>());
!nativePath.empty())
localPath = nativePath; //no matter if item exists or not
else //returns empty if not available (item not existing, error during copy):
extractFileDescriptor<side>(*fsObj, [&](const FileDescriptor& descr) { localPath = tempFileBuf.getTempPath(descr); });
if (const Zstring& nativePath = getNativeItemPath(fsObj->getAbstractPath<side2>());
!nativePath.empty())
localPath2 = nativePath;
else
extractFileDescriptor<side2>(*fsObj, [&](const FileDescriptor& descr) { localPath2 = tempFileBuf.getTempPath(descr); });
if (localPath .empty()) localPath = replaceCpy(utfTo<Zstring>(L"<" + _("Local path not available for %x.") + L">"), Zstr("%x"), itemPath );
if (localPath2.empty()) localPath2 = replaceCpy(utfTo<Zstring>(L"<" + _("Local path not available for %x.") + L">"), Zstr("%x"), itemPath2);
pathInfos.push_back(
{
itemPath,
itemPath2,
itemName,
itemName2,
parentPath,
parentPath2,
localPath,
localPath2,
});
}
return pathInfos;
}
}
void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool leftSide,
const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR)
{
//do not open more than one Explorer instance!
if (commandLinePhrase == extCommandFileManager.cmdLine)
if (selectionL.size() + selectionR.size() > 1)
{
if (( leftSide && !selectionL.empty()) ||
(!leftSide && selectionR.empty()))
return openExternalApplication(commandLinePhrase, leftSide, {selectionL[0]}, {});
else
return openExternalApplication(commandLinePhrase, leftSide, {}, {selectionR[0]});
}
//----------------------------------------------------------------
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
try
{
//support fallback instead of an error in this special case
if (commandLinePhrase == extCommandFileManager.cmdLine)
{
//either left or right selection is filled with exactly one item (or no selection at all)
AbstractPath itemPath = getNullPath();
if (!selectionL.empty())
{
if (selectionL[0]->isEmpty<SelectSide::left>())
return openFolderInFileBrowser(getExistingParentFolder<SelectSide::left>(*selectionL[0])); //throw FileError
itemPath = selectionL[0]->getAbstractPath<SelectSide::left>();
}
else if (!selectionR.empty())
{
if (selectionR[0]->isEmpty<SelectSide::right>())
return openFolderInFileBrowser(getExistingParentFolder<SelectSide::right>(*selectionR[0])); //throw FileError
itemPath = selectionR[0]->getAbstractPath<SelectSide::right>();
}
else
return openFolderInFileBrowser(leftSide ? //throw FileError
createAbstractPath(firstFolderPair_->getValues().folderPathPhraseLeft) :
createAbstractPath(firstFolderPair_->getValues().folderPathPhraseRight));
//itemPath != base folder in this context
if (const Zstring& gdriveUrl = getGoogleDriveFolderUrl(*AFS::getParentPath(itemPath)); //throw FileError
!gdriveUrl.empty())
return openWithDefaultApp(gdriveUrl); //throw FileError
}
std::vector<Zstring> cmdLines;
if (containsFileItemMacro(commandLinePhrase))
{
//regular command evaluation:
const size_t invokeCount = selectionL.size() + selectionR.size();
assert(invokeCount > 0);
if (invokeCount > EXT_APP_MASS_INVOKE_THRESHOLD)
if (globalCfg_.confirmDlgs.confirmCommandMassInvoke)
{
bool dontAskAgain = false;
switch (showConfirmationDialog(this, DialogInfoType::warning, PopupDialogCfg().setTitle(_("Confirm")).
setMainInstructions(replaceCpy(_P("Do you really want to execute the command %y for one item?",
"Do you really want to execute the command %y for %x items?", invokeCount),
L"%y", fmtPath(commandLinePhrase))).
setCheckBox(dontAskAgain, _("&Don't show this warning again")),
_("&Execute")))
{
case ConfirmationButton::accept:
globalCfg_.confirmDlgs.confirmCommandMassInvoke = !dontAskAgain;
break;
case ConfirmationButton::cancel:
return;
}
}
std::set<FileDescriptor> nonNativeFiles;
if (contains(commandLinePhrase, macroNameLocalPath) ||
contains(commandLinePhrase, macroNameLocalPaths))
{
collectNonNativeFiles<SelectSide::left >(selectionL, tempFileBuf_, nonNativeFiles);
collectNonNativeFiles<SelectSide::right>(selectionR, tempFileBuf_, nonNativeFiles);
}
if (contains(commandLinePhrase, macroNameLocalPath2))
{
collectNonNativeFiles<SelectSide::right>(selectionL, tempFileBuf_, nonNativeFiles);
collectNonNativeFiles<SelectSide::left >(selectionR, tempFileBuf_, nonNativeFiles);
}
//##################### create temporary files for non-native paths ######################
if (!nonNativeFiles.empty())
{
const auto& guiCfg = getConfig();
FocusPreserver fp;
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileAlertPending);
try
{
tempFileBuf_.createTempFiles(nonNativeFiles, statusHandler); //throw CancelProcess
//"clearSelection" not needed/desired
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
if (r.summary.result == TaskResult::cancelled)
return;
//updateGui(); -> not needed
}
//########################################################################################
std::vector<ItemPathInfo> pathInfos;
append(pathInfos, getItemPathInfo<SelectSide::left >(selectionL, tempFileBuf_));
append(pathInfos, getItemPathInfo<SelectSide::right>(selectionR, tempFileBuf_));
Zstring cmdLineTmp = expandMacros(commandLinePhrase);
//support path lists for a single command line: https://freefilesync.org/forum/viewtopic.php?t=10328#p39305
auto replaceListMacro = [&](const ZstringView macroName, const Zstring ItemPathInfo::*itemPath)
{
replace(cmdLineTmp, Zstring() + Zstr('"') + macroName + Zstr('"'), macroName); //get rid of quotes if existing
if (contains(cmdLineTmp, macroName))
{
Zstring pathList;
for (const ItemPathInfo& pathInfo : pathInfos)
{
if (!pathList.empty())
pathList += Zstr(' ');
pathList += escapeCommandArg(pathInfo.*itemPath);
}
replace(cmdLineTmp, macroName, pathList);
}
};
replaceListMacro(macroNameItemPaths, &ItemPathInfo::itemPath);
replaceListMacro(macroNameLocalPaths, &ItemPathInfo::localPath);
replaceListMacro(macroNameItemNames, &ItemPathInfo::itemName);
replaceListMacro(macroNameParentPaths, &ItemPathInfo::parentPath);
//generate multiple command lines per each selected item
for (const ItemPathInfo& pathInfo : pathInfos)
if (commandLinePhrase == extCommandOpenDefault.cmdLine)
//not strictly needed, but: 1. better error reporting (Windows) 2. not async => avoid zombies (Linux/macOS)
openWithDefaultApp(pathInfo.localPath); //throw FileError
else
{
Zstring cmdLineItem = cmdLineTmp;
auto replaceMacro = [&](const ZstringView macroName, const Zstring& value)
{
replace(cmdLineItem, Zstring() + Zstr('"') + macroName + Zstr('"'), macroName); //get rid of quotes if existing
replace(cmdLineItem, macroName, escapeCommandArg(value));
};
replaceMacro(macroNameItemPath, pathInfo.itemPath);
replaceMacro(macroNameItemPath2, pathInfo.itemPath2);
replaceMacro(macroNameLocalPath, pathInfo.localPath);
replaceMacro(macroNameLocalPath2, pathInfo.localPath2);
replaceMacro(macroNameItemName, pathInfo.itemName);
replaceMacro(macroNameItemName2, pathInfo.itemName2);
replaceMacro(macroNameParentPath, pathInfo.parentPath);
replaceMacro(macroNameParentPath2, pathInfo.parentPath2);
cmdLines.push_back(std::move(cmdLineItem));
}
removeDuplicatesStable(cmdLines);
}
else
cmdLines.push_back(expandMacros(commandLinePhrase)); //add single entry (even if selection is empty!)
for (const Zstring& cmdLine : cmdLines)
try
{
std::optional<int> timeoutMs;
if (cmdLines.size() <= EXT_APP_MASS_INVOKE_THRESHOLD)
timeoutMs = EXT_APP_MAX_TOTAL_WAIT_TIME_MS / cmdLines.size(); //run async, but give consoleExecute() some "time to fail"
//else: run synchronously
if (const auto& [exitCode, output] = consoleExecute(cmdLine, timeoutMs); //throw SysError, SysErrorTimeOut
exitCode != 0)
throw SysError(formatSystemError(utfTo<std::string>(commandLinePhrase),
replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), utfTo<std::wstring>(output)));
}
catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :>
catch (const SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)), e.toString()); }
}
catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); }
}
void MainDialog::setStatusInfo(const wxString& text, bool highlight)
{
if (statusTxts_.empty())
{
m_staticTextStatusCenter->SetFont((m_staticTextStatusCenter->GetFont().*(highlight ? &wxFont::Bold : &wxFont::GetBaseFont))());
m_staticTextStatusCenter->SetForegroundColour(highlight ? getColorFlashStatusInfo() : wxNullColour);
setText(*m_staticTextStatusCenter, text);
m_panelStatusBar->Layout();
}
else
statusTxts_.front() = text;
statusTxtHighlightFirst_ = highlight;
}
void MainDialog::flashStatusInfo(const wxString& text)
{
if (statusTxts_.empty())
{
statusTxts_.push_back(m_staticTextStatusCenter->GetLabelText());
statusTxts_.push_back(text);
m_staticTextStatusCenter->SetForegroundColour(getColorFlashStatusInfo());
m_staticTextStatusCenter->SetFont(m_staticTextStatusCenter->GetFont().Bold());
popStatusInfo();
}
else
statusTxts_.insert(statusTxts_.begin() + 1, text);
}
void MainDialog::popStatusInfo()
{
assert(!statusTxts_.empty());
if (!statusTxts_.empty())
{
const wxString statusTxt = std::move(statusTxts_.back());
statusTxts_.pop_back();
if (statusTxts_.empty())
setStatusInfo(statusTxt, statusTxtHighlightFirst_);
else
{
guiQueue_.processAsync([] { std::this_thread::sleep_for(std::chrono::seconds(3)); }, [this] { popStatusInfo(); });
setText(*m_staticTextStatusCenter, statusTxt);
m_panelStatusBar->Layout();
}
}
}
void MainDialog::onResizeTopButtonPanel(wxEvent& event)
{
const double horizontalWeight = 0.3;
const int newOrientation = m_panelTopButtons->GetSize().GetWidth() * horizontalWeight >
m_panelTopButtons->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window, NOT sizer width!
assert(m_buttonCompare->GetContainingSizer()->GetItem(static_cast<size_t>(0))->IsSpacer());
if (bSizerTopButtons->GetOrientation() != newOrientation)
{
bSizerTopButtons->SetOrientation(newOrientation);
m_buttonCompare->GetContainingSizer()->GetItem(static_cast<size_t>(0))->SetProportion(newOrientation == wxHORIZONTAL ? 1 : 0);
m_buttonCancel ->GetContainingSizer()->GetItem(m_buttonCancel) ->SetProportion(newOrientation == wxHORIZONTAL ? 0 : 1);
m_buttonCompare->GetContainingSizer()->GetItem(m_buttonCompare) ->SetProportion(newOrientation == wxHORIZONTAL ? 0 : 1);
m_buttonSync ->GetContainingSizer()->GetItem(m_buttonSync) ->SetProportion(newOrientation == wxHORIZONTAL ? 0 : 1);
m_panelTopButtons->Layout();
}
event.Skip();
}
void MainDialog::onResizeConfigPanel(wxEvent& event)
{
const double horizontalWeight = 0.75;
const int newOrientation = m_panelConfig->GetSize().GetWidth() * horizontalWeight >
m_panelConfig->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window, NOT sizer width!
if (bSizerConfig->GetOrientation() != newOrientation)
{
//hide button labels for horizontal layout
for (wxSizerItem* szItem : bSizerCfgHistoryButtons->GetChildren())
if (auto sizerChild = dynamic_cast<wxBoxSizer*>(szItem->GetSizer()))
for (wxSizerItem* szItem2 : sizerChild->GetChildren())
if (auto btnLabel = dynamic_cast<wxStaticText*>(szItem2->GetWindow()))
btnLabel->Show(newOrientation == wxVERTICAL);
bSizerConfig->SetOrientation(newOrientation);
bSizerCfgHistoryButtons->SetOrientation(newOrientation == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL);
bSizerSaveAs ->SetOrientation(newOrientation == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL);
m_panelConfig->Layout();
}
event.Skip();
}
void MainDialog::onResizeViewPanel(wxEvent& event)
{
const int newOrientation = m_panelViewFilter->GetSize().GetWidth() >
m_panelViewFilter->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window, NOT sizer width!
if (bSizerViewFilter->GetOrientation() != newOrientation)
{
bSizerStatistics ->SetOrientation(newOrientation);
bSizerViewButtons->SetOrientation(newOrientation);
bSizerViewFilter ->SetOrientation(newOrientation);
//apply opposite orientation for child sizers
const int childOrient = newOrientation == wxHORIZONTAL ? wxVERTICAL : wxHORIZONTAL;
for (wxSizerItem* szItem : bSizerStatistics->GetChildren())
if (auto sizerChild = dynamic_cast<wxBoxSizer*>(szItem->GetSizer()))
if (sizerChild->GetOrientation() != childOrient)
sizerChild->SetOrientation(childOrient);
m_panelViewFilter->Layout();
m_panelStatistics->Layout();
}
event.Skip();
}
void MainDialog::onResizeLeftFolderWidth(wxEvent& event)
{
//adapt left-shift display distortion caused by scrollbars for multiple folder pairs
const int width = m_panelTopLeft->GetSize().GetWidth();
for (FolderPairPanel* panel : additionalFolderPairs_)
panel->m_panelLeft->SetMinSize({width, -1});
event.Skip();
}
void MainDialog::onTreeKeyEvent(wxKeyEvent& event)
{
const std::vector<FileSystemObject*> selection = getTreeSelection();
int keyCode = event.GetKeyCode();
if (m_gridOverview->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;
}
if (event.ControlDown())
switch (keyCode)
{
case 'C':
case WXK_INSERT: //CTRL + C || CTRL + INS
case WXK_NUMPAD_INSERT:
copyGridSelectionToClipboard(*m_gridOverview);
return;
}
else if (event.AltDown())
switch (keyCode)
{
case WXK_NUMPAD_LEFT:
case WXK_LEFT: //ALT + <left>
setSyncDirManually(selection, SyncDirection::left);
return;
case WXK_NUMPAD_RIGHT:
case WXK_RIGHT: //ALT + <right>
setSyncDirManually(selection, SyncDirection::right);
return;
case WXK_NUMPAD_UP:
case WXK_NUMPAD_DOWN:
case WXK_UP: //ALT + <up>
case WXK_DOWN: //ALT + <down>
setSyncDirManually(selection, SyncDirection::none);
return;
}
else
switch (keyCode)
{
case WXK_F2:
case WXK_NUMPAD_F2:
renameSelectedFiles(selection, selection);
return;
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
startSyncForSelecction(selection);
return;
case WXK_SPACE:
case WXK_NUMPAD_SPACE:
if (!selection.empty())
setIncludedManually(selection, m_bpButtonShowExcluded->isActive() && !selection[0]->isActive());
//always exclude items if "m_bpButtonShowExcluded is unchecked" => yes, it's possible to have already unchecked items in selection, so we need to overwrite:
//e.g. select root node while the first item returned is not shown on grid!
return;
case WXK_DELETE:
case WXK_NUMPAD_DELETE:
deleteSelectedFiles(selection, selection, !event.ShiftDown() /*moveToRecycler*/);
return;
}
event.Skip(); //unknown keypress: propagate
}
void MainDialog::onGridKeyEvent(wxKeyEvent& event, Grid& grid, bool leftSide)
{
const std::vector<FileSystemObject*> selection = getGridSelection();
const std::vector<FileSystemObject*> selectionL = getGridSelection(true, false);
const std::vector<FileSystemObject*> selectionR = getGridSelection(false, true);
int keyCode = event.GetKeyCode();
if (grid.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;
}
if (event.ControlDown())
switch (keyCode)
{
case 'C':
case WXK_INSERT: //CTRL + C || CTRL + INS
case WXK_NUMPAD_INSERT:
copyPathsToClipboard(selectionL, selectionR);
return; // -> swallow event! don't allow default grid commands!
case 'T': //CTRL + T
copyToAlternateFolder(selectionL, selectionR);
return;
}
else if (event.AltDown())
switch (keyCode)
{
case WXK_NUMPAD_LEFT:
case WXK_LEFT: //ALT + <left>
setSyncDirManually(selection, SyncDirection::left);
return;
case WXK_NUMPAD_RIGHT:
case WXK_RIGHT: //ALT + <right>
setSyncDirManually(selection, SyncDirection::right);
return;
case WXK_NUMPAD_UP:
case WXK_NUMPAD_DOWN:
case WXK_UP: //ALT + <up>
case WXK_DOWN: //ALT + <down>
setSyncDirManually(selection, SyncDirection::none);
return;
}
else
{
//0 ... 9
const size_t extAppPos = [&]() -> size_t
{
if ('0' <= keyCode && keyCode <= '9')
return keyCode - '0';
if (WXK_NUMPAD0 <= keyCode && keyCode <= WXK_NUMPAD9)
return keyCode - WXK_NUMPAD0;
return static_cast<size_t>(-1);
}();
if (extAppPos < globalCfg_.externalApps.size())
{
openExternalApplication(globalCfg_.externalApps[extAppPos].cmdLine, leftSide, selectionL, selectionR);
return;
}
switch (keyCode)
{
case WXK_F2:
case WXK_NUMPAD_F2:
renameSelectedFiles(selectionL, selectionR);
return;
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
startSyncForSelecction(selection);
return;
case WXK_SPACE:
case WXK_NUMPAD_SPACE:
if (!selection.empty())
setIncludedManually(selection, m_bpButtonShowExcluded->isActive() && !selection[0]->isActive());
return;
case WXK_DELETE:
case WXK_NUMPAD_DELETE:
deleteSelectedFiles(selectionL, selectionR, !event.ShiftDown() /*moveToRecycler*/);
return;
}
}
event.Skip(); //unknown keypress: propagate
}
void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
{
if (localKeyEventsEnabled_) //avoid recursion
{
localKeyEventsEnabled_ = false;
ZEN_ON_SCOPE_EXIT(localKeyEventsEnabled_ = true);
const int keyCode = event.GetKeyCode();
//CTRL + X
/* if (event.ControlDown())
switch (keyCode)
{
case 'F': //CTRL + F
showFindPanel();
return; //-> swallow event!
} */
if (event.ControlDown())
switch (keyCode)
{
case WXK_TAB: //CTRL + TAB
case WXK_NUMPAD_TAB: //don't use F10: avoid accidental clicks: https://freefilesync.org/forum/viewtopic.php?t=1663
swapSides();
return; //-> swallow event!
}
switch (keyCode)
{
case WXK_F3:
case WXK_NUMPAD_F3:
startFindNext(!event.ShiftDown() /*searchAscending*/);
return; //-> swallow event!
//case WXK_F6:
//{
// wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED);
// m_bpButtonCmpConfig->Command(dummy2); //simulate click
//}
//return; //-> swallow event!
//case WXK_F7:
//{
// wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED);
// m_bpButtonFilter->Command(dummy2); //simulate click
//}
//return; //-> swallow event!
//case WXK_F8:
//{
// wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED);
// m_bpButtonSyncConfig->Command(dummy2); //simulate click
//}
//return; //-> swallow event!
case WXK_F11:
setGridViewType(m_bpButtonViewType->isActive() ? GridViewType::difference : GridViewType::action);
return; //-> swallow event!
//redirect certain (unhandled) keys directly to grid!
case WXK_UP:
case WXK_DOWN:
case WXK_LEFT:
case WXK_RIGHT:
case WXK_PAGEUP:
case WXK_PAGEDOWN:
case WXK_HOME:
case WXK_END:
case WXK_NUMPAD_UP:
case WXK_NUMPAD_DOWN:
case WXK_NUMPAD_LEFT:
case WXK_NUMPAD_RIGHT:
case WXK_NUMPAD_PAGEUP:
case WXK_NUMPAD_PAGEDOWN:
case WXK_NUMPAD_HOME:
case WXK_NUMPAD_END:
{
const wxWindow* focus = wxWindow::FindFocus();
if (!isComponentOf(focus, m_gridMainL ) && //
!isComponentOf(focus, m_gridMainC ) && //don't propagate keyboard commands if grid is already in focus
!isComponentOf(focus, m_gridMainR ) && //
!isComponentOf(focus, m_gridOverview ) &&
!isComponentOf(focus, m_gridCfgHistory) && //don't propagate if selecting config
!isComponentOf(focus, m_panelSearch ) &&
!isComponentOf(focus, m_panelLog ) &&
!isComponentOf(focus, m_panelDirectoryPairs) && //don't propagate if changing directory fields
m_gridMainL->IsEnabled())
{
m_gridMainL->SetFocus();
event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
m_gridMainL->getMainWin().GetEventHandler()->ProcessEvent(event); //propagating event to child lead to recursion with old key_event.h handling => still an issue?
event.Skip(false); //definitively handled now!
return;
}
}
break;
case WXK_ESCAPE: //let's do something useful and hide the log panel
if (!isComponentOf(wxWindow::FindFocus(), m_panelSearch) && //search panel also handles ESC!
m_panelLog->IsEnabled())
{
if (auiMgr_.GetPane(m_panelLog).IsShown()) //else: let it "ding"
return showLogPanel(false /*show*/);
}
break;
}
}
event.Skip();
}
void MainDialog::onTreeGridSelection(GridSelectEvent& event)
{
//scroll m_gridMain to user's new selection on m_gridOverview
ptrdiff_t leadRow = -1;
if (event.positive_ && event.rowFirst_ != event.rowLast_)
if (std::unique_ptr<TreeView::Node> node = treegrid::getDataView(*m_gridOverview).getLine(event.rowFirst_))
{
if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get()))
leadRow = filegrid::getDataView(*m_gridMainC).findRowFirstChild(&(root->baseFolder));
else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get()))
{
leadRow = filegrid::getDataView(*m_gridMainC).findRowDirect(&(dir->folder));
if (leadRow < 0) //directory was filtered out! still on tree view (but NOT on grid view)
leadRow = filegrid::getDataView(*m_gridMainC).findRowFirstChild(&(dir->folder));
}
else if (const TreeView::FilesNode* files = dynamic_cast<const TreeView::FilesNode*>(node.get()))
{
assert(!files->filesAndLinks.empty());
if (!files->filesAndLinks.empty())
leadRow = filegrid::getDataView(*m_gridMainC).findRowDirect(files->filesAndLinks[0]);
}
}
if (leadRow >= 0)
{
leadRow = std::max<ptrdiff_t>(0, leadRow - 1); //scroll one more row
m_gridMainL->scrollTo(leadRow); //
m_gridMainC->scrollTo(leadRow); //scroll all of them (including "scroll master")
m_gridMainR->scrollTo(leadRow); //
m_gridOverview->getMainWin().Update(); //draw cursor immediately rather than on next idle event (required for slow CPUs, netbook)
}
//get selection on overview panel and set corresponding markers on main grid
std::unordered_set<const FileSystemObject*> markedFilesAndLinks; //mark files/symlinks directly
std::unordered_set<const ContainerObject*> markedContainer; //mark full container including child-objects
for (size_t row : m_gridOverview->getSelectedRows())
if (std::unique_ptr<TreeView::Node> node = treegrid::getDataView(*m_gridOverview).getLine(row))
{
if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get()))
markedContainer.insert(&(root->baseFolder));
else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get()))
markedContainer.insert(&(dir->folder));
else if (const TreeView::FilesNode* files = dynamic_cast<const TreeView::FilesNode*>(node.get()))
markedFilesAndLinks.insert(files->filesAndLinks.begin(), files->filesAndLinks.end());
}
filegrid::setNavigationMarker(*m_gridMainL, *m_gridMainR,
std::move(markedFilesAndLinks), std::move(markedContainer));
//selecting overview should clear main grid selection (if any) but not the other way around:
m_gridMainL->clearSelection(GridEventPolicy::deny);
m_gridMainC->clearSelection(GridEventPolicy::deny);
m_gridMainR->clearSelection(GridEventPolicy::deny);
event.Skip();
}
namespace
{
template <SelectSide side>
std::vector<Zstring> getFilterPhrasesRel(const std::vector<FileSystemObject*>& selection)
{
std::vector<Zstring> output;
for (const FileSystemObject* fsObj : selection)
{
//#pragma warning(suppress: 6011) -> fsObj bound in this context!
Zstring phrase = FILE_NAME_SEPARATOR + fsObj->getRelativePath<side>();
const bool isFolder = dynamic_cast<const FolderPair*>(fsObj) != nullptr;
if (isFolder)
phrase += FILE_NAME_SEPARATOR;
output.push_back(std::move(phrase));
}
return output;
}
Zstring getFilterPhraseRel(const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR)
{
std::vector<Zstring> phrases;
append(phrases, getFilterPhrasesRel<SelectSide::left >(selectionL));
append(phrases, getFilterPhrasesRel<SelectSide::right>(selectionR));
removeDuplicatesStable(phrases, [](const Zstring& lhs, const Zstring& rhs) { return compareNoCase(lhs, rhs) < 0; });
//ignore case, just like path filter
Zstring relPathPhrase;
for (const Zstring& phrase : phrases)
{
relPathPhrase += phrase;
relPathPhrase += Zstr('\n');
}
return trimCpy(relPathPhrase);
}
}
void MainDialog::onTreeGridContext(GridContextMenuEvent& event)
{
const std::vector<FileSystemObject*>& selection = getTreeSelection(); //referenced by lambdas!
ContextMenu menu;
//----------------------------------------------------------------------------------------------------
auto getImage = [&](SyncDirection dir, SyncOperation soDefault)
{
return mirrorIfRtl(getSyncOpImage(!selection.empty() && selection[0]->getSyncOperation() != SO_EQUAL ?
selection[0]->testSyncOperation(dir) : soDefault));
};
const wxImage opRight = getImage(SyncDirection::right, SO_OVERWRITE_RIGHT);
const wxImage opNone = getImage(SyncDirection::none, SO_DO_NOTHING );
const wxImage opLeft = getImage(SyncDirection::left, SO_OVERWRITE_LEFT );
wxString shortcutLeft = L"\tAlt+Left";
wxString shortcutRight = L"\tAlt+Right";
if (m_gridOverview->GetLayoutDirection() == wxLayout_RightToLeft)
std::swap(shortcutLeft, shortcutRight);
const bool nonEqualSelected = selectionIncludesNonEqualItem(selection);
menu.addItem(_("Set direction:") + L" ->" + shortcutRight, [this, &selection] { setSyncDirManually(selection, SyncDirection::right); }, opRight, nonEqualSelected);
menu.addItem(_("Set direction:") + L" -" L"\tAlt+Down", [this, &selection] { setSyncDirManually(selection, SyncDirection::none); }, opNone, nonEqualSelected);
menu.addItem(_("Set direction:") + L" <-" + shortcutLeft, [this, &selection] { setSyncDirManually(selection, SyncDirection::left); }, opLeft, nonEqualSelected);
//Gtk needs a direction, "<-", because it has no context menu icons!
//Gtk requires "no spaces" for shortcut identifiers!
menu.addSeparator();
//----------------------------------------------------------------------------------------------------
auto addFilterMenu = [&](const std::wstring& label, const wxImage& img, bool include)
{
if (selection.empty())
menu.addItem(label, nullptr, img, false /*enabled*/);
else if (selection.size() == 1)
{
ContextMenu submenu;
const bool isFolder = dynamic_cast<const FolderPair*>(selection[0]) != nullptr;
const Zstring& relPathL = selection[0]->getRelativePath<SelectSide::left >();
const Zstring& relPathR = selection[0]->getRelativePath<SelectSide::right>();
//by extension
const Zstring extensionL = getFileExtension(relPathL);
const Zstring extensionR = getFileExtension(relPathR);
if (!extensionL.empty())
submenu.addItem(L"*." + utfTo<wxString>(extensionL),
[this, extensionL, include] { addFilterPhrase(Zstr("*.") + extensionL, include, false /*requireNewLine*/); });
if (!extensionR.empty() && !equalNoCase(extensionL, extensionR)) //rare, but possible (e.g. after manual rename)
submenu.addItem(L"*." + utfTo<wxString>(extensionR),
[this, extensionR, include] { addFilterPhrase(Zstr("*.") + extensionR, include, false /*requireNewLine*/); });
//by file name
Zstring filterPhraseNameL = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPathL);
Zstring filterPhraseNameR = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPathR);
if (isFolder)
{
filterPhraseNameL += FILE_NAME_SEPARATOR;
filterPhraseNameR += FILE_NAME_SEPARATOR;
}
submenu.addItem(utfTo<wxString>(filterPhraseNameL),
[this, filterPhraseNameL, include] { addFilterPhrase(filterPhraseNameL, include, true /*requireNewLine*/); });
if (!equalNoCase(filterPhraseNameL, filterPhraseNameR)) //rare, but possible (ignore case, just like path filter)
submenu.addItem(utfTo<wxString>(filterPhraseNameR),
[this, filterPhraseNameR, include] { addFilterPhrase(filterPhraseNameR, include, true /*requireNewLine*/); });
//by relative path
Zstring filterPhraseRelL = FILE_NAME_SEPARATOR + relPathL;
Zstring filterPhraseRelR = FILE_NAME_SEPARATOR + relPathR;
if (isFolder)
{
filterPhraseRelL += FILE_NAME_SEPARATOR;
filterPhraseRelR += FILE_NAME_SEPARATOR;
}
submenu.addItem(utfTo<wxString>(filterPhraseRelL), [this, filterPhraseRelL, include] { addFilterPhrase(filterPhraseRelL, include, true /*requireNewLine*/); });
if (!equalNoCase(filterPhraseRelL, filterPhraseRelR)) //rare, but possible
submenu.addItem(utfTo<wxString>(filterPhraseRelR), [this, filterPhraseRelR, include] { addFilterPhrase(filterPhraseRelR, include, true /*requireNewLine*/); });
menu.addSubmenu(label, submenu, img);
}
else //by relative path
menu.addItem(label + L" <" + _("multiple selection") + L">",
[this, &selection, include] { addFilterPhrase(getFilterPhraseRel(selection, selection), include, true /*requireNewLine*/); }, img);
};
addFilterMenu(_("&Include via filter:"), loadImage("filter_include", dipToScreen(getMenuIconDipSize())), true);
addFilterMenu(_("&Exclude via filter:"), loadImage("filter_exclude", dipToScreen(getMenuIconDipSize())), false);
//----------------------------------------------------------------------------------------------------
if (m_bpButtonShowExcluded->isActive() && !selection.empty() && !selection[0]->isActive())
menu.addItem(_("Include temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, true); }, loadImage("checkbox_true"));
else
menu.addItem(_("Exclude temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, false); }, loadImage("checkbox_false"), !selection.empty());
//----------------------------------------------------------------------------------------------------
const bool selectionContainsItemsToSync = [&]
{
for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection))
if (getEffectiveSyncDir(fsObj->getSyncOperation()) != SyncDirection::none)
return true;
return false;
}();
menu.addSeparator();
menu.addItem(_("&Synchronize selection") + L"\tEnter", [&] { startSyncForSelecction(selection); },
loadImage("start_sync_selection", dipToScreen(getMenuIconDipSize())), selectionContainsItemsToSync);
//----------------------------------------------------------------------------------------------------
const ptrdiff_t itemsSelected =
std::count_if(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); }) +
std::count_if(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); });
//menu.addSeparator();
//menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selection, selection); }, wxNullImage, itemsSelected > 0);
//----------------------------------------------------------------------------------------------------
menu.addSeparator();
menu.addItem((itemsSelected > 1 ? _("Multi-&Rename") : _("&Rename")) + L"\tF2",
[&] { renameSelectedFiles(selection, selection); }, loadImage("rename", dipToScreen(getMenuIconDipSize())), itemsSelected > 0);
menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selection, selection, true /*moveToRecycler*/); }, imgTrashSmall_, itemsSelected > 0);
menu.popup(*m_gridOverview, event.mousePos_);
}
void MainDialog::onGridContextRim(GridContextMenuEvent& event, bool leftSide)
{
const std::vector<FileSystemObject*> selection = getGridSelection(); //referenced by lambdas!
const std::vector<FileSystemObject*> selectionL = getGridSelection(true, false);
const std::vector<FileSystemObject*> selectionR = getGridSelection(false, true);
onGridContextRim(getGridSelection(),
getGridSelection(true, false),
getGridSelection(false, true), leftSide, event.mousePos_);
}
void MainDialog::onGridGroupContextRim(GridClickEvent& event, bool leftSide)
{
if (static_cast<HoverAreaGroup>(event.hoverArea_) == HoverAreaGroup::groupName)
if (const FileView::PathDrawInfo pdi = filegrid::getDataView(*m_gridMainC).getDrawInfo(event.row_);
pdi.folderGroupObj)
{
m_gridMainL->clearSelection(GridEventPolicy::deny);
m_gridMainC->clearSelection(GridEventPolicy::deny);
m_gridMainR->clearSelection(GridEventPolicy::deny);
std::vector<FileSystemObject*> selectionL;
std::vector<FileSystemObject*> selectionR;
(leftSide ? selectionL : selectionR).push_back(pdi.folderGroupObj);
onGridContextRim({pdi.folderGroupObj},
selectionL, selectionR, leftSide, event.mousePos_);
return; //"swallow" event => suppress default context menu handling
}
assert(static_cast<HoverAreaGroup>(event.hoverArea_) != HoverAreaGroup::groupName);
event.Skip();
}
void MainDialog::onGridContextRim(const std::vector<FileSystemObject*>& selection,
const std::vector<FileSystemObject*>& selectionL,
const std::vector<FileSystemObject*>& selectionR, bool leftSide, wxPoint mousePos)
{
ContextMenu menu;
auto getImage = [&](SyncDirection dir, SyncOperation soDefault)
{
return mirrorIfRtl(getSyncOpImage(!selection.empty() && selection[0]->getSyncOperation() != SO_EQUAL ?
selection[0]->testSyncOperation(dir) : soDefault));
};
const wxImage opLeft = getImage(SyncDirection::left, SO_OVERWRITE_LEFT );
const wxImage opRight = getImage(SyncDirection::right, SO_OVERWRITE_RIGHT);
const wxImage opNone = getImage(SyncDirection::none, SO_DO_NOTHING );
wxString shortcutLeft = L"\tAlt+Left";
wxString shortcutRight = L"\tAlt+Right";
if (m_gridMainL->GetLayoutDirection() == wxLayout_RightToLeft)
std::swap(shortcutLeft, shortcutRight);
const bool nonEqualSelected = selectionIncludesNonEqualItem(selection);
menu.addItem(_("Set direction:") + L" ->" + shortcutRight, [this, &selection] { setSyncDirManually(selection, SyncDirection::right); }, opRight, nonEqualSelected);
menu.addItem(_("Set direction:") + L" -" L"\tAlt+Down", [this, &selection] { setSyncDirManually(selection, SyncDirection::none); }, opNone, nonEqualSelected);
menu.addItem(_("Set direction:") + L" <-" + shortcutLeft, [this, &selection] { setSyncDirManually(selection, SyncDirection::left); }, opLeft, nonEqualSelected);
//GTK needs a direction, "<-", because it has no context menu icons!
//GTK does not allow spaces in shortcut identifiers!
menu.addSeparator();
//----------------------------------------------------------------------------------------------------
auto addFilterMenu = [&](const wxString& label, const wxImage& img, bool include)
{
if (selectionL.empty() && selectionR.empty())
menu.addItem(label, nullptr, img, false /*enabled*/);
else if (selectionL.size() + selectionR.size() == 1)
{
ContextMenu submenu;
const bool isFolder = dynamic_cast<const FolderPair*>((!selectionL.empty() ? selectionL : selectionR)[0]) != nullptr;
const Zstring& relPath = !selectionL.empty() ?
selectionL[0]->getRelativePath<SelectSide::left >() :
selectionR[0]->getRelativePath<SelectSide::right>();
//by extension
if (const Zstring extension = getFileExtension(relPath);
!extension.empty())
submenu.addItem(L"*." + utfTo<wxString>(extension), [this, extension, include]
{
addFilterPhrase(Zstr("*.") + extension, include, false /*requireNewLine*/);
});
//by file name
Zstring filterPhraseName = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPath);
if (isFolder)
filterPhraseName += FILE_NAME_SEPARATOR;
submenu.addItem(utfTo<wxString>(filterPhraseName), [this, filterPhraseName, include]
{
addFilterPhrase(filterPhraseName, include, true /*requireNewLine*/);
});
//by relative path
Zstring filterPhraseRel = FILE_NAME_SEPARATOR + relPath;
if (isFolder)
filterPhraseRel += FILE_NAME_SEPARATOR;
submenu.addItem(utfTo<wxString>(filterPhraseRel), [this, filterPhraseRel, include] { addFilterPhrase(filterPhraseRel, include, true /*requireNewLine*/); });
menu.addSubmenu(label, submenu, img);
}
else //by relative path
menu.addItem(label + L" <" + _("multiple selection") + L">",
[this, &selectionL, &selectionR, include] { addFilterPhrase(getFilterPhraseRel(selectionL, selectionR), include, true /*requireNewLine*/); }, img);
};
addFilterMenu(_("&Include via filter:"), loadImage("filter_include", dipToScreen(getMenuIconDipSize())), true);
addFilterMenu(_("&Exclude via filter:"), loadImage("filter_exclude", dipToScreen(getMenuIconDipSize())), false);
//----------------------------------------------------------------------------------------------------
if (m_bpButtonShowExcluded->isActive() && !selection.empty() && !selection[0]->isActive())
menu.addItem(_("Include temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, true); }, loadImage("checkbox_true"));
else
menu.addItem(_("Exclude temporarily") + L"\tSpace", [this, &selection] { setIncludedManually(selection, false); }, loadImage("checkbox_false"), !selection.empty());
//----------------------------------------------------------------------------------------------------
const bool selectionContainsItemsToSync = [&]
{
for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection))
if (getEffectiveSyncDir(fsObj->getSyncOperation()) != SyncDirection::none)
return true;
return false;
}();
menu.addSeparator();
menu.addItem(_("&Synchronize selection") + L"\tEnter", [&] { startSyncForSelecction(selection); },
loadImage("start_sync_selection", dipToScreen(getMenuIconDipSize())), selectionContainsItemsToSync);
//----------------------------------------------------------------------------------------------------
if (!globalCfg_.externalApps.empty())
{
menu.addSeparator();
for (auto it = globalCfg_.externalApps.begin();
it != globalCfg_.externalApps.end();
++it)
{
//translate default external apps on the fly: 1. "Show in Explorer" 2. "Open with default application"
wxString description = translate(it->description);
if (const size_t pos = it - globalCfg_.externalApps.begin();
pos == 0)
description += L"\tD-Click, 0";
else if (pos < 9)
description += L"\t" + numberTo<std::wstring>(pos);
auto openApp = [this, command = it->cmdLine, leftSide, &selectionL, &selectionR] { openExternalApplication(command, leftSide, selectionL, selectionR); };
menu.addItem(description, openApp, it->cmdLine == extCommandFileManager.cmdLine ? imgFileManagerSmall_ : wxNullImage,
it->cmdLine == extCommandFileManager.cmdLine ||
!containsFileItemMacro(it->cmdLine) ||
!selectionL.empty() || !selectionR.empty());
}
}
//----------------------------------------------------------------------------------------------------
const ptrdiff_t itemsSelected =
std::count_if(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); }) +
std::count_if(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); });
menu.addSeparator();
menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selectionL, selectionR); }, wxNullImage, itemsSelected > 0);
//----------------------------------------------------------------------------------------------------
menu.addSeparator();
menu.addItem((itemsSelected > 1 ? _("Multi-&Rename") : _("&Rename")) + L"\tF2",
[&] { renameSelectedFiles(selectionL, selectionR); }, loadImage("rename", dipToScreen(getMenuIconDipSize())), itemsSelected > 0);
menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selectionL, selectionR, true /*moveToRecycler*/); }, imgTrashSmall_, itemsSelected > 0);
menu.popup(leftSide ? *m_gridMainL : *m_gridMainR, mousePos);
}
void MainDialog::addFilterPhrase(const Zstring& phrase, bool include, bool requireNewLine)
{
Zstring& filterString = [&]() -> Zstring&
{
if (include)
{
Zstring& includeFilter = currentCfg_.mainCfg.globalFilter.includeFilter;
if (NameFilter::isNull(includeFilter, Zstring())) //fancy way of checking for "*" include
includeFilter.clear();
return includeFilter;
}
else
return currentCfg_.mainCfg.globalFilter.excludeFilter;
}();
if (requireNewLine)
{
trim(filterString, TrimSide::right, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n') || c == Zstr(' '); });
if (!filterString.empty())
filterString += Zstr('\n');
filterString += phrase;
}
else
{
trim(filterString, TrimSide::right, [](Zchar c) { return c == Zstr('\n') || c == Zstr(' '); });
if (contains(afterLast(filterString, Zstr('\n'), IfNotFoundReturn::all), FILTER_ITEM_SEPARATOR))
{
if (!endsWith(filterString, FILTER_ITEM_SEPARATOR))
filterString += Zstring() + Zstr(' ') + FILTER_ITEM_SEPARATOR;
filterString += Zstr(' ') + phrase;
}
else
{
if (!filterString.empty())
filterString += Zstr('\n');
filterString += phrase + Zstr(' ') + FILTER_ITEM_SEPARATOR; //append FILTER_ITEM_SEPARATOR to 'mark' that next extension exclude should write to same line
}
}
updateGlobalFilterButton();
if (include)
applyFilterConfig(); //user's temporary exclusions lost!
else //do not fully apply filter, just exclude new items: preserve user's temporary exclusions
{
for (BaseFolderPair& baseFolder : asRange(folderCmp_))
addHardFiltering(baseFolder, phrase);
updateGui();
}
}
void MainDialog::onGridLabelContextC(GridLabelClickEvent& event)
{
ContextMenu menu;
const GridViewType viewType = m_bpButtonViewType->isActive() ? GridViewType::action : GridViewType::difference;
menu.addItem(_("Difference") + (viewType != GridViewType::difference ? L"\tF11" : L""),
[&] { setGridViewType(GridViewType::difference); }, greyScaleIfDisabled(loadImage("compare", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::difference));
menu.addItem(_("Action") + (viewType != GridViewType::action ? L"\tF11" : L""),
[&] { setGridViewType(GridViewType::action); }, greyScaleIfDisabled(loadImage("start_sync", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::action));
menu.popup(*m_gridMainC, {event.mousePos_.x, m_gridMainC->getColumnLabelHeight()});
}
void MainDialog::onGridLabelContextRim(GridLabelClickEvent& event, bool leftSide)
{
ContextMenu menu;
//--------------------------------------------------------------------------------------------------------
Grid& grid = leftSide ? *m_gridMainL : *m_gridMainR;
//const ColumnTypeRim colType = static_cast<ColumnTypeRim>(event.colType_);
auto toggleColumn = [&](ColumnType ct)
{
auto colAttr = grid.getColumnConfig();
Grid::ColAttributes* caItemPath = nullptr;
Grid::ColAttributes* caToggle = nullptr;
for (Grid::ColAttributes& ca : colAttr)
if (ca.type == static_cast<ColumnType>(ColumnTypeRim::path))
caItemPath = &ca;
else if (ca.type == ct)
caToggle = &ca;
assert(caItemPath && caItemPath->stretch > 0 && caItemPath->visible);
assert(caToggle && caToggle ->stretch == 0);
if (caItemPath && caToggle)
{
caToggle->visible = !caToggle->visible;
//take width of newly visible column from stretched item path column
caItemPath->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset;
grid.setColumnConfig(colAttr);
}
};
if (const GridData* prov = grid.getDataProvider())
for (const Grid::ColAttributes& ca : grid.getColumnConfig())
menu.addCheckBox(prov->getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); },
ca.visible, ca.type != static_cast<ColumnType>(ColumnTypeRim::path)); //do not allow user to hide this column!
//----------------------------------------------------------------------------------------------
menu.addSeparator();
auto& itemPathFormat = leftSide ? globalCfg_.mainDlg.itemPathFormatLeftGrid : globalCfg_.mainDlg.itemPathFormatRightGrid;
auto setItemPathFormat = [&](ItemPathFormat fmt)
{
itemPathFormat = fmt;
filegrid::setItemPathForm(grid, fmt);
};
auto addFormatEntry = [&](const wxString& label, ItemPathFormat fmt)
{
menu.addRadio(label, [fmt, &setItemPathFormat] { setItemPathFormat(fmt); }, itemPathFormat == fmt);
};
addFormatEntry(_("Item name" ), ItemPathFormat::name);
addFormatEntry(_("Relative path"), ItemPathFormat::relative);
addFormatEntry(_("Full path" ), ItemPathFormat::full);
//----------------------------------------------------------------------------------------------
auto setIconSize = [&](GridIconSize sz, bool showIcons)
{
globalCfg_.mainDlg.iconSize = sz;
globalCfg_.mainDlg.showIcons = showIcons;
filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.mainDlg.showIcons, convert(globalCfg_.mainDlg.iconSize));
};
menu.addSeparator();
menu.addCheckBox(_("Show icons:"), [&] { setIconSize(globalCfg_.mainDlg.iconSize, !globalCfg_.mainDlg.showIcons); }, globalCfg_.mainDlg.showIcons);
auto addSizeEntry = [&](const wxString& label, GridIconSize sz)
{
menu.addRadio(label, [sz, &setIconSize] { setIconSize(sz, true /*showIcons*/); }, globalCfg_.mainDlg.iconSize == sz, globalCfg_.mainDlg.showIcons);
};
addSizeEntry(TAB_SPACE + _("Small" ), GridIconSize::small );
addSizeEntry(TAB_SPACE + _("Medium"), GridIconSize::medium);
addSizeEntry(TAB_SPACE + _("Large" ), GridIconSize::large );
//----------------------------------------------------------------------------------------------
auto setDefault = [&]
{
grid.setColumnConfig(convertColAttributes(leftSide ? getFileGridDefaultColAttribsLeft() : getFileGridDefaultColAttribsRight(), getFileGridDefaultColAttribsLeft()));
const GlobalConfig defaultCfg;
setItemPathFormat(leftSide ? defaultCfg.mainDlg.itemPathFormatLeftGrid : defaultCfg.mainDlg.itemPathFormatRightGrid);
setIconSize(defaultCfg.mainDlg.iconSize, defaultCfg.mainDlg.showIcons);
};
menu.addSeparator();
menu.addItem(_("&Default"), setDefault, loadImage("reset_sicon"));
// if (type == ColumnTypeRim::date)
{
auto selectTimeSpan = [&]
{
if (showSelectTimespanDlg(this, manualTimeSpanFrom_, manualTimeSpanTo_) == ConfirmationButton::accept)
{
applyTimeSpanFilter(folderCmp_, manualTimeSpanFrom_, manualTimeSpanTo_); //overwrite current active/inactive settings
//updateGuiDelayedIf(!m_bpButtonShowExcluded->isActive()); //show update GUI before removing rows
updateGui();
}
};
menu.addSeparator();
menu.addItem(_("Select time span..."), selectTimeSpan);
}
//--------------------------------------------------------------------------------------------------------
menu.popup(grid, {event.mousePos_.x, grid.getColumnLabelHeight()});
//event.Skip();
}
void MainDialog::onOpenMenuTools(wxMenuEvent& event)
{
//each layout menu item is either shown and owned by m_menuTools OR detached from m_menuTools and owned by detachedMenuItems_:
auto filterLayoutItems = [&](wxMenuItem* menuItem, wxWindow* panelWindow)
{
wxAuiPaneInfo& paneInfo = this->auiMgr_.GetPane(panelWindow);
if (paneInfo.IsShown())
{
if (!detachedMenuItems_.contains(menuItem))
detachedMenuItems_.insert(m_menuTools->Remove(menuItem)); //pass ownership
}
else if (detachedMenuItems_.contains(menuItem))
{
detachedMenuItems_.erase(menuItem); //pass ownership
m_menuTools->Append(menuItem); //
}
};
filterLayoutItems(m_menuItemShowMain, m_panelTopButtons);
filterLayoutItems(m_menuItemShowFolders, m_panelDirectoryPairs);
filterLayoutItems(m_menuItemShowViewFilter, m_panelViewFilter);
filterLayoutItems(m_menuItemShowConfig, m_panelConfig);
filterLayoutItems(m_menuItemShowOverview, m_gridOverview);
event.Skip();
}
void MainDialog::resetLayout()
{
m_splitterMain->setSashOffset(0);
auiMgr_.LoadPerspective(defaultPerspective_, false /*don't call wxAuiManager::Update() => already done in updateGuiForFolderPair() */);
updateGuiForFolderPair();
//progress dialog size:
globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size = std::nullopt;
globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = false;
}
void MainDialog::onSetLayoutContext(wxMouseEvent& event)
{
ContextMenu menu;
menu.addItem(_("&Reset layout"), [&] { resetLayout(); }, loadImage("reset_sicon"));
//----------------------------------------------------------------------------------------
bool addedSeparator = false;
for (wxAuiPaneInfo& paneInfo : auiMgr_.GetAllPanes())
if (!paneInfo.IsShown() &&
paneInfo.window != compareStatus_->getAsWindow() &&
paneInfo.window != m_panelLog &&
paneInfo.window != m_panelSearch)
{
if (!addedSeparator)
{
menu.addSeparator();
addedSeparator = true;
}
menu.addItem(replaceCpy(_("Show \"%x\""), L"%x", paneInfo.caption), [this, &paneInfo]
{
paneInfo.Show();
this->auiMgr_.Update();
});
}
menu.popup(*this);
}
void MainDialog::onCompSettingsContext(wxEvent& event)
{
ContextMenu menu;
auto setVariant = [&](CompareVariant var)
{
currentCfg_.mainCfg.cmpCfg.compareVar = var;
applyCompareConfig(true /*setDefaultViewType*/);
};
const CompareVariant activeCmpVar = getConfig().mainCfg.cmpCfg.compareVar;
auto addVariantItem = [&](CompareVariant cmpVar, const char* iconName)
{
const wxImage imgSel = loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize()));
menu.addItem(getVariantName(cmpVar), [&setVariant, cmpVar] { setVariant(cmpVar); }, greyScaleIfDisabled(imgSel, activeCmpVar == cmpVar));
};
addVariantItem(CompareVariant::timeSize, "cmp_time");
addVariantItem(CompareVariant::content, "cmp_content");
addVariantItem(CompareVariant::size, "cmp_size");
menu.popup(*m_bpButtonCmpContext, {m_bpButtonCmpContext->GetSize().x, 0});
}
void MainDialog::onSyncSettingsContext(wxEvent& event)
{
ContextMenu menu;
auto setVariant = [&](SyncVariant var)
{
currentCfg_.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(var);
applySyncDirections();
};
const SyncVariant activeSyncVar = getSyncVariant(getConfig().mainCfg.syncCfg.directionCfg);
auto addVariantItem = [&](SyncVariant syncVar, const char* iconName)
{
const wxImage imgSel = mirrorIfRtl(loadImage(iconName, -1 /*maxWidth*/, dipToScreen(getMenuIconDipSize())));
menu.addItem(getVariantName(syncVar), [&setVariant, syncVar] { setVariant(syncVar); }, greyScaleIfDisabled(imgSel, activeSyncVar == syncVar));
};
addVariantItem(SyncVariant::twoWay, "sync_twoway");
addVariantItem(SyncVariant::mirror, "sync_mirror");
addVariantItem(SyncVariant::update, "sync_update");
//addVariantItem(SyncVariant::custom, "sync_custom"); -> doesn't make sense, does it?
menu.popup(*m_bpButtonSyncContext, {m_bpButtonSyncContext->GetSize().x, 0});
}
void MainDialog::onDialogFilesDropped(FileDropEvent& event)
{
assert(!event.itemPaths_.empty());
loadConfiguration(event.itemPaths_);
//event.Skip();
}
void MainDialog::onFolderSelected(wxCommandEvent& event)
{
if (!folderCmp_.empty())
clearGrid(); //+ update GUI!
else
updateUnsavedCfgStatus();
event.Skip();
}
void MainDialog::cfgHistoryRemoveObsolete(const std::vector<Zstring>& filePaths)
{
auto getUnavailableCfgFilesAsync = [filePaths] //don't use wxString: NOT thread-safe! (e.g. non-atomic ref-count)
{
std::vector<std::future<bool>> keepFile; //check all config files in parallel!
for (const Zstring& filePath : filePaths)
keepFile.push_back(runAsync([=]
{
try
{
getItemType(filePath); //throw FileError
return true;
}
catch (FileError&) { return false; } //not-existing/access error? e.g. not accessible network share or USB stick => remove cfg
}));
//potentially slow network access => limit maximum wait time!
waitForAllTimed(keepFile.begin(), keepFile.end(), std::chrono::seconds(2));
std::vector<Zstring> pathsToRemove;
auto itFut = keepFile.begin();
for (auto it = filePaths.begin(); it != filePaths.end(); ++it, ++itFut)
if (isReady(*itFut) && !itFut->get()) //not ready? maybe HDD that is just spinning up => better keep it
pathsToRemove.push_back(*it);
return pathsToRemove;
};
guiQueue_.processAsync(getUnavailableCfgFilesAsync, [this](const std::vector<Zstring>& filePaths2)
{
if (!filePaths2.empty())
{
cfggrid::getDataView(*m_gridCfgHistory).removeItems(filePaths2);
//restore grid selection (after rows were removed)
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
}
});
}
void MainDialog::cfgHistoryUpdateNotes(const std::vector<Zstring>& filePaths)
{
//load per-config user notes (let's not keep stale copy in GlobalSettings.xml)
for (const Zstring& filePath : filePaths)
{
auto getCfgNotes = [filePath]
{
try
{
const auto& [newGuiCfg, warningMsg] = readAnyConfig({filePath}); //throw FileError
return newGuiCfg.notes;
}
catch (FileError&) { return std::wstring(); }
};
guiQueue_.processAsync(getCfgNotes, [this, filePath](const std::wstring& notes)
{
if (const auto& [item, row] = cfggrid::getDataView(*m_gridCfgHistory).getItem(filePath);
item)
if (item->notes != notes)
{
cfggrid::getDataView(*m_gridCfgHistory).setNotes(filePath, notes);
m_gridCfgHistory->Refresh();
}
});
}
}
std::vector<std::wstring> MainDialog::getJobNames() const
{
std::vector<std::wstring> jobNames;
for (const Zstring& cfgFilePath : activeConfigFiles_)
jobNames.push_back(equalNativePath(cfgFilePath, lastRunConfigPath_) ?
L'[' + _("Last session") + L']' :
extractJobName(cfgFilePath));
return jobNames;
}
void MainDialog::updateUnsavedCfgStatus()
{
const FfsGuiConfig guiCfg = getConfig();
auto makeBrightGrey = [](wxImage img)
{
img = img.ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally!
brighten(img, 80);
return img;
};
//update new config button
const bool allowNew = guiCfg != getDefaultGuiConfig(globalCfg_.defaultFilter);
if (m_bpButtonNew->IsEnabled() != allowNew || !m_bpButtonNew->GetBitmap().IsOk()) //support polling
{
setImage(*m_bpButtonNew, allowNew ? loadImage("cfg_new") : makeBrightGrey(loadImage("cfg_new")));
m_bpButtonNew->Enable(allowNew);
m_menuItemNew->Enable(allowNew);
}
//update save config button
const bool haveUnsavedCfg = lastSavedCfg_ != guiCfg;
const bool allowSave = haveUnsavedCfg ||
activeConfigFiles_.size() > 1;
const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
if (m_bpButtonSave->IsEnabled() != allowSave || !m_bpButtonSave->GetBitmap().IsOk()) //support polling
{
setImage(*m_bpButtonSave, allowSave ? loadImage("cfg_save") : makeBrightGrey(loadImage("cfg_save")));
m_bpButtonSave->Enable(allowSave);
m_menuItemSave->Enable(allowSave); //bitmap is automatically greyscaled on Win7 (introducing a crappy looking shift), but not on XP
}
//set main dialog title
wxString title;
if (haveUnsavedCfg)
title += L'*';
bool showingConfigName = true;
if (!activeCfgFilePath.empty())
{
title += extractJobName(activeCfgFilePath);
if (const std::optional<Zstring>& parentPath = getParentFolderPath(activeCfgFilePath))
title += L" [" + utfTo<wxString>(*parentPath) + L']';
}
else if (activeConfigFiles_.size() > 1)
{
for (const std::wstring& jobName : getJobNames())
title += jobName + L" + ";
if (endsWith(title, L" + "))
title.resize(title.size() - 3);
}
else
showingConfigName = false;
if (showingConfigName)
title += SPACED_DASH;
title += L"FreeFileSync " + utfTo<std::wstring>(ffsVersion);
try
{
if (runningElevated()) //throw FileError
title += L" (root)";
}
catch (FileError&) { assert(false); }
if (!showingConfigName)
title += SPACED_DASH + _("Folder Comparison and Synchronization");
SetTitle(title);
//macOS-only:
OSXSetModified(haveUnsavedCfg);
SetRepresentedFilename(utfTo<wxString>(activeCfgFilePath));
}
void MainDialog::onConfigSave(wxCommandEvent& event)
{
const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
//if we work on a single named configuration document: save directly if changed
//else: always show file dialog
if (activeCfgFilePath.empty())
trySaveConfig(nullptr);
else
{
if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_gui")))
trySaveConfig(&activeCfgFilePath);
else if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_batch")))
trySaveBatchConfig(&activeCfgFilePath);
else
showNotificationDialog(this, DialogInfoType::error,
PopupDialogCfg().setDetailInstructions(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(activeCfgFilePath)) +
L"\n\n" + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(activeCfgFilePath)) + L'\n' +
_("Expected:") + L" ffs_gui, ffs_batch"));
}
}
bool MainDialog::trySaveConfig(const Zstring* guiCfgPath) //"false": error/cancel
{
Zstring cfgFilePath;
if (guiCfgPath)
{
cfgFilePath = *guiCfgPath;
assert(endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_gui")));
}
else
{
const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
const std::optional<Zstring> defaultFolderPath = !activeCfgFilePath.empty() ?
getParentFolderPath(activeCfgFilePath) :
getParentFolderPath(globalCfg_.mainDlg.config.lastSelectedFile);
Zstring defaultFileName = !activeCfgFilePath.empty() ?
getItemName(activeCfgFilePath) :
Zstr("SyncSettings.ffs_gui");
//attention: activeConfigFiles_ may be an imported ffs_batch file! We don't want to overwrite it with a GUI config!
defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_gui");
wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo<wxString>(defaultFileName),
wxString(L"FreeFileSync (*.ffs_gui)|*.ffs_gui") + L"|" +_("All files") + L" (*.*)|*",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (fileSelector.ShowModal() != wxID_OK)
return false;
cfgFilePath = utfTo<Zstring>(fileSelector.GetPath());
if (!endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_gui"))) //no weird shit!
cfgFilePath += Zstr(".ffs_gui"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724
globalCfg_.mainDlg.config.lastSelectedFile = cfgFilePath;
}
const FfsGuiConfig guiCfg = getConfig();
try
{
writeConfig(guiCfg, cfgFilePath); //throw FileError
setLastUsedConfig(guiCfg, {cfgFilePath});
flashStatusInfo(_("Configuration saved"));
return true;
}
catch (const FileError& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
return false;
}
}
bool MainDialog::trySaveBatchConfig(const Zstring* batchCfgPath) //"false": error/cancel
{
//essentially behave like trySaveConfig(): the collateral damage of not saving GUI-only settings "m_bpButtonViewType" is negligible
const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
//prepare batch config: reuse existing batch-specific settings from file if available
BatchExclusiveConfig batchExCfg;
try
{
Zstring referenceBatchFile;
if (batchCfgPath)
referenceBatchFile = *batchCfgPath;
else if (!activeCfgFilePath.empty() && endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_batch")))
referenceBatchFile = activeCfgFilePath;
if (!referenceBatchFile.empty())
batchExCfg = readBatchConfig(referenceBatchFile).first.batchExCfg; //throw FileError
//=> ignore warnings altogether: user has seen them already when loading the config file!
}
catch (const FileError& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
return false;
}
Zstring cfgFilePath;
if (batchCfgPath)
{
cfgFilePath = *batchCfgPath;
assert(endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_batch")));
}
else
{
//let user update batch config: this should change batch-exclusive settings only, else the "setLastUsedConfig" below would be somewhat of a lie
if (showBatchConfigDialog(this,
batchExCfg,
currentCfg_.mainCfg.ignoreErrors) != ConfirmationButton::accept)
return false;
updateUnsavedCfgStatus(); //nothing else to update on GUI!
const std::optional<Zstring> defaultFolderPath = !activeCfgFilePath.empty() ?
getParentFolderPath(activeCfgFilePath) :
getParentFolderPath(globalCfg_.mainDlg.config.lastSelectedFile);
Zstring defaultFileName = !activeCfgFilePath.empty() ?
getItemName(activeCfgFilePath) :
Zstr("BatchRun.ffs_batch");
//attention: activeConfigFiles_ may be an ffs_gui file! We don't want to overwrite it with a BATCH config!
defaultFileName = beforeLast(defaultFileName, Zstr('.'), IfNotFoundReturn::all) + Zstr(".ffs_batch");
wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), utfTo<wxString>(defaultFileName),
_("FreeFileSync batch") + L" (*.ffs_batch)|*.ffs_batch" + L"|" +_("All files") + L" (*.*)|*",
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (fileSelector.ShowModal() != wxID_OK)
return false;
cfgFilePath = utfTo<Zstring>(fileSelector.GetPath());
if (!endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_batch"))) //no weird shit!
cfgFilePath += Zstr(".ffs_batch"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724
globalCfg_.mainDlg.config.lastSelectedFile = cfgFilePath;
}
const FfsGuiConfig guiCfg = getConfig();
try
{
writeConfig({guiCfg, batchExCfg}, cfgFilePath); //throw FileError
setLastUsedConfig(guiCfg, {cfgFilePath}); //[!] behave as if we had saved guiCfg
flashStatusInfo(_("Configuration saved"));
return true;
}
catch (const FileError& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
return false;
}
}
bool MainDialog::saveOldConfig() //"false": error/cancel
{
const FfsGuiConfig guiCfg = getConfig();
if (lastSavedCfg_ != guiCfg)
{
const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
//notify user about changed settings
if (globalCfg_.confirmDlgs.confirmSaveConfig)
if (!activeCfgFilePath.empty())
//only if check is active and non-default config file loaded
{
bool neverSaveChanges = false;
switch (showQuestionDialog(this, DialogInfoType::info, PopupDialogCfg().setTitle(utfTo<wxString>(activeCfgFilePath)).
setMainInstructions(replaceCpy(_("Do you want to save changes to %x?"), L"%x", fmtPath(getItemName(activeCfgFilePath)))).
setCheckBox(neverSaveChanges, _("Never save &changes"), static_cast<ConfirmationButton3>(QuestionButton2::yes)),
_("&Save"), _("Do&n't save")))
{
case QuestionButton2::yes: //save
if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_gui")))
return trySaveConfig(&activeCfgFilePath); //"false": error/cancel
else if (endsWithAsciiNoCase(activeCfgFilePath, Zstr(".ffs_batch")))
return trySaveBatchConfig(&activeCfgFilePath); //"false": error/cancel
else
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().
setDetailInstructions(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(activeCfgFilePath)) +
L"\n\n" + _("Unexpected file extension:") + L' ' + fmtPath(getFileExtension(activeCfgFilePath)) + L'\n' +
_("Expected:") + L" ffs_gui, ffs_batch"));
return false;
}
break;
case QuestionButton2::no: //don't save
globalCfg_.confirmDlgs.confirmSaveConfig = !neverSaveChanges;
break;
case QuestionButton2::cancel:
return false;
}
}
//user doesn't save changes =>
//discard current reference file(s), this ensures next app start will load [Last session] instead of the original non-modified config selection
setLastUsedConfig(guiCfg, {} /*cfgFilePaths*/);
//this seems to make theoretical sense also: the job of this function is to make sure, current (volatile) config and reference file name are in sync
// => if user does not save cfg, it is not attached to a physical file anymore!
}
return true;
}
void MainDialog::onConfigLoad(wxCommandEvent& event)
{
std::optional<Zstring> defaultFolderPath = getParentFolderPath(globalCfg_.mainDlg.config.lastSelectedFile);
wxFileDialog fileSelector(this, wxString() /*message*/, utfTo<wxString>(defaultFolderPath ? *defaultFolderPath : Zstr("")), wxString() /*default file name*/,
wxString(L"FreeFileSync (*.ffs_gui; *.ffs_batch)|*.ffs_gui;*.ffs_batch") + L"|" +_("All files") + L" (*.*)|*",
wxFD_OPEN | wxFD_MULTIPLE);
if (fileSelector.ShowModal() != wxID_OK)
return;
wxArrayString tmp;
fileSelector.GetPaths(tmp);
std::vector<Zstring> filePaths;
for (const wxString& path : tmp)
filePaths.push_back(utfTo<Zstring>(path));
if (!filePaths.empty())
globalCfg_.mainDlg.config.lastSelectedFile = filePaths[0];
assert(!filePaths.empty());
loadConfiguration(filePaths);
}
void MainDialog::onCfgGridSelection(GridSelectEvent& event)
{
std::vector<Zstring> filePaths;
for (size_t row : m_gridCfgHistory->getSelectedRows())
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row))
filePaths.push_back(cfg->cfgItem.cfgFilePath);
else
assert(false);
//clicking on already selected config should not clear comparison results:
const bool skipSelection = [&] //what about multi-selection? a second selection probably *should* clear results
{
return filePaths.size() == 1 && activeConfigFiles_.size() == 1 &&
filePaths[0] == activeConfigFiles_[0];
}();
if (!skipSelection)
if (filePaths.empty() || //ignore accidental clicks in empty space of configuration panel
!loadConfiguration(filePaths, true /*ignoreBrokenConfig*/)) //=> allow user to delete broken config entry!
//user changed m_gridCfgHistory selection so it's this method's responsibility to synchronize with activeConfigFiles:
//- if user cancelled saving old config
//- there's an error loading new config
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
event.Skip();
}
void MainDialog::onCfgGridDoubleClick(GridClickEvent& event)
{
if (!activeConfigFiles_.empty())
{
wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED);
m_buttonCompare->Command(dummy); //simulate click
}
}
void MainDialog::onConfigNew(wxCommandEvent& event)
{
loadConfiguration({});
}
bool MainDialog::loadConfiguration(const std::vector<Zstring>& filePaths, bool ignoreBrokenConfig) //"false": error/cancel
{
FfsGuiConfig newGuiCfg = getDefaultGuiConfig(globalCfg_.defaultFilter);
std::wstring warningMsg;
if (!filePaths.empty()) //empty cfg file list means "use default"
try
{
std::tie(newGuiCfg, warningMsg) = readAnyConfig(filePaths); //throw FileError
//allow reading batch configurations, too
}
catch (const FileError& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
if (!ignoreBrokenConfig)
return false;
}
if (!saveOldConfig()) //=> error/cancel
return false;
setConfig(newGuiCfg, filePaths);
if (!warningMsg.empty())
{
showNotificationDialog(this, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg));
setLastUsedConfig(FfsGuiConfig(), filePaths); //simulate changed config due to parsing errors
}
//flashStatusInfo("Configuration loaded"); -> irrelevant!?
return true;
}
void MainDialog::removeSelectedCfgHistoryItems(bool deleteFromDisk)
{
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
const std::vector<size_t> selectedRows = m_gridCfgHistory->getSelectedRows();
if (!selectedRows.empty())
{
std::vector<Zstring> filePaths;
for (size_t row : selectedRows)
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row))
filePaths.push_back(cfg->cfgItem.cfgFilePath);
else
assert(false);
if (deleteFromDisk)
{
//===========================================================================
std::wstring fileList;
for (const Zstring& filePath : filePaths)
fileList += utfTo<std::wstring>(filePath) + L'\n';
FocusPreserver fp;
bool moveToRecycler = true;
if (showDeleteDialog(this, fileList, static_cast<int>(filePaths.size()),
moveToRecycler) != ConfirmationButton::accept)
return;
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
0 /*autoRetryCount*/,
std::chrono::seconds(0) /*autoRetryDelay*/,
globalCfg_.soundFileAlertPending);
std::vector<Zstring> deletedPaths;
try
{
deleteListOfFiles(filePaths, deletedPaths, moveToRecycler, globalCfg_.warnDlgs.warnRecyclerMissing, statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
filePaths = deletedPaths;
//===========================================================================
}
cfggrid::getDataView(*m_gridCfgHistory).removeItems(filePaths);
m_gridCfgHistory->Refresh(); //grid size changed => clears selection!
//discard unsaved changes => no point in saving before loading next config, right?
//- bonus: clear activeConfigFiles_ if loadConfiguration() fails so that old configs don't reappear after restart
setLastUsedConfig(getConfig(), {} /*cfgFilePaths*/);
//set active selection on next item to allow "batch-deletion" by holding down DEL key
//user expects that selected config is also loaded: https://freefilesync.org/forum/viewtopic.php?t=5723
// => deleteFromDisk failed? still select selectedRows.front()!
std::vector<Zstring> nextCfgPaths;
if (m_gridCfgHistory->getRowCount() > 0)
{
const size_t nextRow = std::min(selectedRows.front(), m_gridCfgHistory->getRowCount() - 1);
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(nextRow))
{
nextCfgPaths.push_back(cfg->cfgItem.cfgFilePath);
m_gridCfgHistory->setGridCursor(nextRow, GridEventPolicy::deny);
//= Grid::makeRowVisible(redundant) + set grid cursor + select cursor row(redundant)
}
}
loadConfiguration(nextCfgPaths); //=> error/(cancel)
}
}
void MainDialog::renameSelectedCfgHistoryItem()
{
const std::vector<size_t> selectedRows = m_gridCfgHistory->getSelectedRows();
if (!selectedRows.empty())
{
const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0]);
assert(cfg);
if (!cfg)
return;
if (cfg->isLastRunCfg)
return showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(
replaceCpy(_("%x cannot be renamed."), L"%x", fmtPath(cfg->name))));
const Zstring cfgPathOld = cfg->cfgItem.cfgFilePath;
//FIRST: 1. consolidate unsaved changes using the *old* config file name, if any!
//2. get rid of multiple-selection if exists 3. load cfg to allow non-failing(!) setLastUsedConfig() below
if (!loadConfiguration({cfgPathOld})) //=> error/cancel
return;
const Zstring fileName = getItemName(cfgPathOld);
/**/ Zstring folderPathPf = beforeLast(cfgPathOld, FILE_NAME_SEPARATOR, IfNotFoundReturn::none);
if (!folderPathPf.empty())
folderPathPf += FILE_NAME_SEPARATOR;
const Zstring cfgNameOld = beforeLast(fileName, Zstr('.'), IfNotFoundReturn::all);
/**/ Zstring cfgDotExt = afterLast(fileName, Zstr('.'), IfNotFoundReturn::none);
if (!cfgDotExt.empty())
cfgDotExt = Zstr('.') + cfgDotExt;
wxString cfgNameTmp = utfTo<wxString>(cfgNameOld);
for (;;)
{
wxTextEntryDialog cfgRenameDlg(this, _("New name:"), _("Rename Configuration"), cfgNameTmp);
wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST);
inputValidator.SetCharExcludes(L"/\\"); //let's not silently forbid "fileNameForbiddenChars", but let it fail explicitly!
cfgRenameDlg.SetTextValidator(inputValidator);
if (cfgRenameDlg.ShowModal() != wxID_OK)
return;
cfgNameTmp = cfgRenameDlg.GetValue();
const Zstring cfgNameNew = utfTo<Zstring>(trimCpy(cfgNameTmp));
if (cfgNameNew == cfgNameOld)
return;
const Zstring cfgPathNew = folderPathPf + cfgNameNew + cfgDotExt;
try
{
if (cfgNameNew.empty()) //better error message + check than wxFILTER_EMPTY, e.g. trimCpy()!
throw FileError(_("Configuration name must not be empty."));
moveAndRenameItem(cfgPathOld, cfgPathNew, false /*replaceExisting*/); //throw FileError, (ErrorMoveUnsupported), ErrorTargetExisting
}
catch (const FileError& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
continue;
}
cfggrid::getDataView(*m_gridCfgHistory).renameItem(cfgPathOld, cfgPathNew);
m_gridCfgHistory->Refresh(); //grid size changed => clears selection!
const auto& [item, row] = cfggrid::getDataView(*m_gridCfgHistory).getItem(cfgPathNew);
assert(item);
m_gridCfgHistory->setGridCursor(row, GridEventPolicy::deny);
//= Grid::makeRowVisible(redundant) + set grid cursor + select cursor row(redundant)
//
//keep current cfg and just swap the file name: see previous "loadConfiguration({cfgPathOld}"!
setLastUsedConfig(lastSavedCfg_, {cfgPathNew});
return;
}
}
}
void MainDialog::onCfgGridKeyEvent(wxKeyEvent& event)
{
const int keyCode = event.GetKeyCode();
switch (keyCode)
{
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
if (!activeConfigFiles_.empty())
{
wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED);
(folderCmp_.empty() ? m_buttonCompare : m_buttonSync)->Command(dummy); //simulate click
}
break;
case WXK_DELETE:
case WXK_NUMPAD_DELETE:
removeSelectedCfgHistoryItems(event.ShiftDown() /*deleteFromDisk*/);
return; //"swallow" event
case WXK_F2:
case WXK_NUMPAD_F2:
renameSelectedCfgHistoryItem();
return; //"swallow" event
}
event.Skip();
}
void MainDialog::onCfgGridContext(GridContextMenuEvent& event)
{
ContextMenu menu;
const std::vector<size_t> selectedRows = m_gridCfgHistory->getSelectedRows();
std::vector<Zstring> cfgFilePaths;
for (size_t row : selectedRows)
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row))
cfgFilePaths.push_back(cfg->cfgItem.cfgFilePath);
else
assert(false);
//--------------------------------------------------------------------------------------------------------
ContextMenu submenu;
auto applyBackColor = [this, &cfgFilePaths](const wxColor& col)
{
cfggrid::getDataView(*m_gridCfgHistory).setBackColor(cfgFilePaths, col);
//re-apply selection (after sorting by color tags):
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
//m_gridCfgHistory->Refresh(); <- implicit in last call
};
const wxSize colSize{this->GetCharHeight(), this->GetCharHeight()};
auto addColorOption = [&](const wxColor& col, const wxString& name)
{
submenu.addItem(name, [&, col] { applyBackColor(col); },
rectangleImage({wxsizeToScreen(colSize.x),
wxsizeToScreen(colSize.y)},
col.Ok() ? col : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW),
{0xdd, 0xdd, 0xdd} /*light grey*/, dipToScreen(1)),
!selectedRows.empty());
};
const auto defaultColors = []() -> std::vector<std::pair<wxColor, wxString>>
{
if (wxSystemSettings::GetAppearance().IsDark()) //=> offer darker colors
return {{wxNullColour /*=> !wxColor::IsOk()*/, L'(' + _("&Default") + L')'}, //meta options should be enclosed in parentheses
{{0xfe, 0x59, 0x48}, _("Red")},
{{0xfe, 0xff, 0x31}, _("Yellow")},
{{0x5a, 0xff, 0x00}, _("Green")},
{{0x5a, 0xff, 0xff}, _("Cyan")},
{{0x48, 0x47, 0xff}, _("Blue")},
{{0xc1, 0x7e, 0xfe}, _("Purple")},
{{0xb7, 0xb7, 0xb7}, _("Gray")},
};
else //=> offer lighter colors
return {{wxNullColour /*=> !wxColor::IsOk()*/, L'(' + _("&Default") + L')'}, //meta options should be enclosed in parentheses
{{0xff, 0xd8, 0xcb}, _("Red")},
{{0xff, 0xf9, 0x99}, _("Yellow")},
{{0xcc, 0xff, 0x99}, _("Green")},
{{0xcc, 0xff, 0xff}, _("Cyan")},
{{0xcc, 0xcc, 0xff}, _("Blue")},
{{0xf2, 0xcb, 0xff}, _("Purple")},
{{0xdd, 0xdd, 0xdd}, _("Gray")},
};
}();
std::unordered_set<wxUint32> addedColorCodes;
//add default colors
for (const auto& [color, name] : defaultColors)
{
addColorOption(color, name);
if (color.IsOk())
addedColorCodes.insert(color.GetRGBA());
}
//add user-defined colors
for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get())
if (item.backColor.IsOk())
if (const auto [it, inserted] = addedColorCodes.insert(item.backColor.GetRGBA());
inserted)
addColorOption(item.backColor, item.backColor.GetAsString(wxC2S_HTML_SYNTAX)); //#RRGGBB
//show color picker
wxBitmap bmpColorPicker(wxsizeToScreen(colSize.x),
wxsizeToScreen(colSize.y)); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes
bmpColorPicker.SetScaleFactor(getScreenDpiScale());
{
wxMemoryDC dc(bmpColorPicker);
const wxColor borderCol(0xdd, 0xdd, 0xdd); //light grey
drawFilledRectangle(dc, wxRect(colSize), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), borderCol, dipToWxsize(1));
dc.SetFont(dc.GetFont().Bold());
dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
dc.DrawText(L"?", wxPoint() + (colSize - dc.GetTextExtent(L"?")) / 2);
}
submenu.addItem(_("Different color..."), [&]
{
wxColourData colCfg;
colCfg.SetChooseFull(true);
colCfg.SetChooseAlpha(false);
colCfg.SetColour(defaultColors[1].first); //tentative
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0]))
if (cfg->cfgItem.backColor.IsOk())
colCfg.SetColour(cfg->cfgItem.backColor);
int i = 0;
for (const auto& [color, name] : defaultColors)
if (color.IsOk() && i < static_cast<int>(wxColourData::NUM_CUSTOM))
colCfg.SetCustomColour(i++, color);
auto fixColorPickerColor = [](const wxColor& col)
{
assert(col.Alpha() == 255);
return col;
};
wxColourDialog dlg(this, &colCfg);
dlg.Center();
dlg.Bind(wxEVT_COLOUR_CHANGED, [&](wxColourDialogEvent& event2)
{
//show preview during color selection (Windows-only atm)
cfggrid::getDataView(*m_gridCfgHistory).setBackColor(cfgFilePaths, fixColorPickerColor(event2.GetColour()), true /*previewOnly*/);
m_gridCfgHistory->Refresh();
});
if (dlg.ShowModal() == wxID_OK)
applyBackColor(fixColorPickerColor(dlg.GetColourData().GetColour()));
else //shut off color preview
{
cfggrid::getDataView(*m_gridCfgHistory).setBackColor(cfgFilePaths, wxNullColour, true /*previewOnly*/);
m_gridCfgHistory->Refresh();
}
}, bmpColorPicker.ConvertToImage());
menu.addSubmenu(_("Background color"), submenu, loadImage("color", dipToScreen(getMenuIconDipSize())), !selectedRows.empty());
menu.addSeparator();
//--------------------------------------------------------------------------------------------------------
auto showInFileManager = [&]
{
if (!selectedRows.empty())
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0]))
{
const Zstring cmdLine = replaceCpy(expandMacros(extCommandFileManager.cmdLine), Zstr("%local_path%"), escapeCommandArg(cfg->cfgItem.cfgFilePath));
try
{
if (const auto& [exitCode, output] = consoleExecute(cmdLine, EXT_APP_MAX_TOTAL_WAIT_TIME_MS); //throw SysError, SysErrorTimeOut
exitCode != 0)
throw SysError(formatSystemError(utfTo<std::string>(extCommandFileManager.cmdLine),
replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), utfTo<std::wstring>(output)));
}
catch (SysErrorTimeOut&) {} //child process not failed yet => probably fine :>
catch (const SysError& e)
{
const std::wstring errorMsg = replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString();
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(errorMsg));
}
return;
}
assert(false);
};
menu.addItem(translate(extCommandFileManager.description), //translate default external apps on the fly: "Show in Explorer"
showInFileManager, imgFileManagerSmall_, !selectedRows.empty());
menu.addSeparator();
//--------------------------------------------------------------------------------------------------------
const bool renameEnabled = [&]
{
if (!selectedRows.empty())
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0]))
return !cfg->isLastRunCfg;
return false;
}();
menu.addItem(_("&Rename") + L"\tF2", [this] { renameSelectedCfgHistoryItem (); }, loadImage("rename", dipToScreen(getMenuIconDipSize())), renameEnabled);
//--------------------------------------------------------------------------------------------------------
menu.addItem(_("&Hide") + L"\tDel", [this] { removeSelectedCfgHistoryItems(false /*deleteFromDisk*/); }, wxNullImage, !selectedRows.empty());
menu.addItem(_("&Delete") + L"\tShift+Del", [this] { removeSelectedCfgHistoryItems(true /*deleteFromDisk*/); }, imgTrashSmall_, !selectedRows.empty());
//--------------------------------------------------------------------------------------------------------
menu.popup(*m_gridCfgHistory, event.mousePos_);
//event.Skip();
}
void MainDialog::onCfgGridLabelContext(GridLabelClickEvent& event)
{
ContextMenu menu;
//--------------------------------------------------------------------------------------------------------
auto toggleColumn = [&](ColumnType ct)
{
auto colAttr = m_gridCfgHistory->getColumnConfig();
Grid::ColAttributes* caName = nullptr;
Grid::ColAttributes* caToggle = nullptr;
for (Grid::ColAttributes& ca : colAttr)
if (ca.type == static_cast<ColumnType>(ColumnTypeCfg::name))
caName = &ca;
else if (ca.type == ct)
caToggle = &ca;
assert(caName && caName->stretch > 0 && caName->visible);
assert(caToggle && caToggle->stretch == 0);
if (caName && caToggle)
{
caToggle->visible = !caToggle->visible;
//take width of newly visible column from stretched folder name column
caName->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset;
m_gridCfgHistory->setColumnConfig(colAttr);
}
};
if (auto prov = m_gridCfgHistory->getDataProvider())
for (const Grid::ColAttributes& ca : m_gridCfgHistory->getColumnConfig())
menu.addCheckBox(prov->getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); },
ca.visible, ca.type != static_cast<ColumnType>(ColumnTypeCfg::name)); //do not allow user to hide name column!
else assert(false);
//--------------------------------------------------------------------------------------------------------
menu.addSeparator();
auto setDefault = [&]
{
const DpiLayout defaultLayout;
m_gridCfgHistory->setColumnConfig(convertColAttributes(defaultLayout.configColumnAttribs, getCfgGridDefaultColAttribs()));
};
menu.addItem(_("&Default"), setDefault, loadImage("reset_sicon")); //'&' -> reuse text from "default" buttons elsewhere
//--------------------------------------------------------------------------------------------------------
menu.addSeparator();
auto setCfgHighlight = [&]
{
int cfgGridSyncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory);
if (showCfgHighlightDlg(this, cfgGridSyncOverdueDays) == ConfirmationButton::accept)
cfggrid::setSyncOverdueDays(*m_gridCfgHistory, cfgGridSyncOverdueDays);
};
menu.addItem(_("Highlight..."), setCfgHighlight);
//--------------------------------------------------------------------------------------------------------
menu.popup(*m_gridCfgHistory, {event.mousePos_.x, m_gridCfgHistory->getColumnLabelHeight()});
//event.Skip();
}
void MainDialog::onCfgGridLabelLeftClick(GridLabelClickEvent& event)
{
const auto colType = static_cast<ColumnTypeCfg>(event.colType_);
bool sortAscending = getDefaultSortDirection(colType);
const auto [sortCol, ascending] = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection();
if (sortCol == colType)
sortAscending = !ascending;
cfggrid::getDataView(*m_gridCfgHistory).setSortDirection(colType, sortAscending);
m_gridCfgHistory->Refresh();
//re-apply selection:
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
}
void MainDialog::onCheckRows(CheckRowsEvent& event)
{
std::vector<size_t> selectedRows;
const size_t rowLast = std::min(event.rowLast_, filegrid::getDataView(*m_gridMainC).rowsOnView()); //consider dummy rows
for (size_t i = event.rowFirst_; i < rowLast; ++i)
selectedRows.push_back(i);
if (!selectedRows.empty())
{
std::vector<FileSystemObject*> objects = filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows);
setIncludedManually(objects, event.setActive_);
}
}
void MainDialog::onSetSyncDirection(SyncDirectionEvent& event)
{
std::vector<size_t> selectedRows;
const size_t rowLast = std::min(event.rowLast_, filegrid::getDataView(*m_gridMainC).rowsOnView()); //consider dummy rows
for (size_t i = event.rowFirst_; i < rowLast; ++i)
selectedRows.push_back(i);
if (!selectedRows.empty())
{
std::vector<FileSystemObject*> objects = filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows);
setSyncDirManually(objects, event.direction_);
}
}
void MainDialog::setLastUsedConfig(const FfsGuiConfig& guiConfig, const std::vector<Zstring>& cfgFilePaths)
{
activeConfigFiles_ = cfgFilePaths;
lastSavedCfg_ = guiConfig;
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, true /*scrollToSelection*/); //put file paths on list of last used config files
//update notes after save + for newly loaded files => BUT: superfluous when loading already known config files!
cfgHistoryUpdateNotes(cfgFilePaths);
updateUnsavedCfgStatus();
}
void MainDialog::setConfig(const FfsGuiConfig& newGuiCfg, const std::vector<Zstring>& cfgFilePaths)
{
currentCfg_ = newGuiCfg;
//(re-)set view filter buttons
setViewFilterDefault();
updateGlobalFilterButton();
//set first folder pair
firstFolderPair_->setValues(currentCfg_.mainCfg.firstPair);
setAddFolderPairs(currentCfg_.mainCfg.additionalPairs);
setGridViewType(currentCfg_.gridViewType);
//clearGrid(); //+ update GUI! -> already called by setAddFolderPairs()
setLastUsedConfig(newGuiCfg, cfgFilePaths);
}
FfsGuiConfig MainDialog::getConfig() const
{
FfsGuiConfig guiCfg = currentCfg_;
//load settings whose ownership lies not in currentCfg:
//first folder pair
guiCfg.mainCfg.firstPair = firstFolderPair_->getValues();
//add additional pairs
guiCfg.mainCfg.additionalPairs.clear();
for (const FolderPairPanel* panel : additionalFolderPairs_)
guiCfg.mainCfg.additionalPairs.push_back(panel->getValues());
//sync preview
guiCfg.gridViewType = m_bpButtonViewType->isActive() ? GridViewType::action : GridViewType::difference;
return guiCfg;
}
void MainDialog::updateGuiDelayedIf(bool condition)
{
if (condition)
{
filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR);
m_gridMainL->Update();
m_gridMainC->Update();
m_gridMainR->Update();
//some delay to show the changed GUI before removing rows from sight
std::this_thread::sleep_for(FILE_GRID_POST_UPDATE_DELAY);
}
updateGui();
}
void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairIndexToShow)
{
GlobalPairConfig globalPairCfg;
globalPairCfg.cmpCfg = currentCfg_.mainCfg.cmpCfg;
globalPairCfg.syncCfg = currentCfg_.mainCfg.syncCfg;
globalPairCfg.filter = currentCfg_.mainCfg.globalFilter;
globalPairCfg.miscCfg.deviceParallelOps = currentCfg_.mainCfg.deviceParallelOps;
globalPairCfg.miscCfg.ignoreErrors = currentCfg_.mainCfg.ignoreErrors;
globalPairCfg.miscCfg.autoRetryCount = currentCfg_.mainCfg.autoRetryCount;
globalPairCfg.miscCfg.autoRetryDelay = currentCfg_.mainCfg.autoRetryDelay;
globalPairCfg.miscCfg.postSyncCommand = currentCfg_.mainCfg.postSyncCommand;
globalPairCfg.miscCfg.postSyncCondition = currentCfg_.mainCfg.postSyncCondition;
globalPairCfg.miscCfg.altLogFolderPathPhrase = currentCfg_.mainCfg.altLogFolderPathPhrase;
globalPairCfg.miscCfg.emailNotifyAddress = currentCfg_.mainCfg.emailNotifyAddress;
globalPairCfg.miscCfg.emailNotifyCondition = currentCfg_.mainCfg.emailNotifyCondition;
globalPairCfg.miscCfg.notes = currentCfg_.notes;
//don't recalculate value but consider current screen status!!!
//e.g. it's possible that the first folder pair local config is shown with all config initial if user just removed local config via mouse context menu!
const bool showMultipleCfgs = m_bpButtonLocalCompCfg->IsShown();
//harmonize with MainDialog::updateGuiForFolderPair()!
assert(showMultipleCfgs || localPairIndexToShow == -1);
assert(m_bpButtonLocalCompCfg->IsShown() == m_bpButtonLocalSyncCfg->IsShown() &&
m_bpButtonLocalCompCfg->IsShown() == m_bpButtonLocalFilter ->IsShown());
std::vector<LocalPairConfig> localCfgs; //showSyncConfigDlg() needs *all* folder pairs for deviceParallelOps update
localCfgs.push_back(firstFolderPair_->getValues());
for (const FolderPairPanel* panel : additionalFolderPairs_)
localCfgs.push_back(panel->getValues());
//------------------------------------------------------------------------------------
const GlobalPairConfig globalPairCfgOld = globalPairCfg;
const std::vector<LocalPairConfig> localPairCfgOld = localCfgs;
if (showSyncConfigDlg(this,
panelToShow,
showMultipleCfgs ? localPairIndexToShow : -1,
showMultipleCfgs,
globalPairCfg,
localCfgs,
globalCfg_.defaultFilter,
globalCfg_.versioningFolderHistory, globalCfg_.versioningFolderLastSelected,
globalCfg_.logFolderHistory, globalCfg_.logFolderLastSelected, globalCfg_.logFolderPhrase,
globalCfg_.folderHistoryMax,
globalCfg_.sftpKeyFileLastSelected,
globalCfg_.emailHistory, globalCfg_.emailHistoryMax,
globalCfg_.commandHistory, globalCfg_.commandHistoryMax) == ConfirmationButton::accept)
{
assert(localCfgs.size() == localPairCfgOld.size());
currentCfg_.mainCfg.cmpCfg = globalPairCfg.cmpCfg;
currentCfg_.mainCfg.syncCfg = globalPairCfg.syncCfg;
currentCfg_.mainCfg.globalFilter = globalPairCfg.filter;
currentCfg_.mainCfg.deviceParallelOps = globalPairCfg.miscCfg.deviceParallelOps;
currentCfg_.mainCfg.ignoreErrors = globalPairCfg.miscCfg.ignoreErrors;
currentCfg_.mainCfg.autoRetryCount = globalPairCfg.miscCfg.autoRetryCount;
currentCfg_.mainCfg.autoRetryDelay = globalPairCfg.miscCfg.autoRetryDelay;
currentCfg_.mainCfg.postSyncCommand = globalPairCfg.miscCfg.postSyncCommand;
currentCfg_.mainCfg.postSyncCondition = globalPairCfg.miscCfg.postSyncCondition;
currentCfg_.mainCfg.altLogFolderPathPhrase = globalPairCfg.miscCfg.altLogFolderPathPhrase;
currentCfg_.mainCfg.emailNotifyAddress = globalPairCfg.miscCfg.emailNotifyAddress;
currentCfg_.mainCfg.emailNotifyCondition = globalPairCfg.miscCfg.emailNotifyCondition;
currentCfg_.notes = globalPairCfg.miscCfg.notes;
firstFolderPair_->setValues(localCfgs[0]);
for (size_t i = 1; i < localCfgs.size(); ++i)
additionalFolderPairs_[i - 1]->setValues(localCfgs[i]);
//------------------------------------------------------------------------------------
const bool cmpConfigChanged = globalPairCfg.cmpCfg != globalPairCfgOld.cmpCfg || [&]
{
for (size_t i = 0; i < localCfgs.size(); ++i)
if (localCfgs[i].localCmpCfg != localPairCfgOld[i].localCmpCfg)
return true;
return false;
}();
//[!] don't redetermine sync directions if only options for deletion handling or versioning are changed!!!
const bool syncDirectionsChanged = globalPairCfg.syncCfg.directionCfg != globalPairCfgOld.syncCfg.directionCfg || [&]
{
for (size_t i = 0; i < localCfgs.size(); ++i)
if (static_cast<bool>(localCfgs[i].localSyncCfg) != static_cast<bool>(localPairCfgOld[i].localSyncCfg) ||
(localCfgs[i].localSyncCfg && localCfgs[i].localSyncCfg->directionCfg != localPairCfgOld[i].localSyncCfg->directionCfg))
return true;
return false;
}();
const bool filterConfigChanged = globalPairCfg.filter != globalPairCfgOld.filter || [&]
{
for (size_t i = 0; i < localCfgs.size(); ++i)
if (localCfgs[i].localFilter != localPairCfgOld[i].localFilter)
return true;
return false;
}();
//const bool miscConfigChanged = globalPairCfg.miscCfg.deviceParallelOps != globalPairCfgOld.miscCfg.deviceParallelOps ||
// globalPairCfg.miscCfg.ignoreErrors != globalPairCfgOld.miscCfg.ignoreErrors ||
// globalPairCfg.miscCfg.autoRetryCount != globalPairCfgOld.miscCfg.autoRetryCount ||
// globalPairCfg.miscCfg.autoRetryDelay != globalPairCfgOld.miscCfg.autoRetryDelay ||
// globalPairCfg.miscCfg.postSyncCommand != globalPairCfgOld.miscCfg.postSyncCommand ||
// globalPairCfg.miscCfg.postSyncCondition != globalPairCfgOld.miscCfg.postSyncCondition ||
// globalPairCfg.miscCfg.altLogFolderPathPhrase != globalPairCfgOld.miscCfg.altLogFolderPathPhrase ||
// globalPairCfg.miscCfg.emailNotifyAddress != globalPairCfgOld.miscCfg.emailNotifyAddress ||
// globalPairCfg.miscCfg.emailNotifyCondition != globalPairCfgOld.miscCfg.emailNotifyCondition;
// globalPairCfg.miscCfg.notes != globalPairCfgOld.miscCfg.notes;
if (cmpConfigChanged)
applyCompareConfig(globalPairCfg.cmpCfg.compareVar != globalPairCfgOld.cmpCfg.compareVar /*setDefaultViewType*/);
if (syncDirectionsChanged)
applySyncDirections();
if (filterConfigChanged)
{
updateGlobalFilterButton(); //refresh global filter icon
applyFilterConfig(); //re-apply filter
}
}
//else: possible but obscure: default filter changed => impact on "New config" enabled/disabled!
updateUnsavedCfgStatus(); //also included by updateGui();
}
void MainDialog::onGlobalFilterContext(wxEvent& event)
{
std::optional<FilterConfig> filterCfgOnClipboard;
if (std::optional<wxString> clipTxt = getClipboardText())
filterCfgOnClipboard = parseFilterBuf(utfTo<std::string>(*clipTxt));
auto cutFilter = [&]
{
setClipboardText(utfTo<wxString>(serializeFilter(currentCfg_.mainCfg.globalFilter)));
currentCfg_.mainCfg.globalFilter = FilterConfig();
updateGlobalFilterButton();
applyFilterConfig();
};
auto copyFilter = [&] { setClipboardText(utfTo<wxString>(serializeFilter(currentCfg_.mainCfg.globalFilter))); };
auto pasteFilter = [&]
{
currentCfg_.mainCfg.globalFilter = *filterCfgOnClipboard;
updateGlobalFilterButton();
applyFilterConfig();
};
ContextMenu menu;
menu.addItem( _("&Copy"), copyFilter, loadImage("item_copy_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter));
menu.addItem( _("&Paste"), pasteFilter, loadImage("item_paste_sicon"), filterCfgOnClipboard.has_value());
menu.addSeparator();
menu.addItem( _("Cu&t"), cutFilter, loadImage("item_cut_sicon"), !isNullFilter(currentCfg_.mainCfg.globalFilter));
menu.popup(*m_bpButtonFilterContext, {m_bpButtonFilterContext->GetSize().x, 0});
}
void MainDialog::onToggleViewType(wxCommandEvent& event)
{
setGridViewType(m_bpButtonViewType->isActive() ? GridViewType::difference : GridViewType::action);
}
void MainDialog::onToggleViewButton(wxCommandEvent& event)
{
if (auto button = dynamic_cast<ToggleButton*>(event.GetEventObject()))
{
button->toggle();
updateGui();
//consistency: toggling view buttons should *always* clear selections, not only implicitly when row count changes:
//
//m_gridMainL->clearSelection(GridEventPolicy::deny);
//m_gridMainC->clearSelection(GridEventPolicy::deny); -> implicitly called by onTreeGridSelection()
//m_gridMainR->clearSelection(GridEventPolicy::deny);
m_gridOverview->clearSelection(GridEventPolicy::allow);
}
else
assert(false);
}
void MainDialog::setViewFilterDefault()
{
auto setButton = [](ToggleButton& tb, bool value) { tb.setActive(value); };
const auto& def = globalCfg_.mainDlg.viewFilterDefault;
setButton(*m_bpButtonShowExcluded, def.excluded);
setButton(*m_bpButtonShowEqual, def.equal);
setButton(*m_bpButtonShowConflict, def.conflict);
setButton(*m_bpButtonShowLeftOnly, def.leftOnly);
setButton(*m_bpButtonShowRightOnly, def.rightOnly);
setButton(*m_bpButtonShowLeftNewer, def.leftNewer);
setButton(*m_bpButtonShowRightNewer, def.rightNewer);
setButton(*m_bpButtonShowDifferent, def.different);
setButton(*m_bpButtonShowCreateLeft, def.createLeft);
setButton(*m_bpButtonShowCreateRight, def.createRight);
setButton(*m_bpButtonShowUpdateLeft, def.updateLeft);
setButton(*m_bpButtonShowUpdateRight, def.updateRight);
setButton(*m_bpButtonShowDeleteLeft, def.deleteLeft);
setButton(*m_bpButtonShowDeleteRight, def.deleteRight);
setButton(*m_bpButtonShowDoNothing, def.doNothing);
}
void MainDialog::onViewTypeContextMouse(wxMouseEvent& event)
{
ContextMenu menu;
const GridViewType viewType = m_bpButtonViewType->isActive() ? GridViewType::action : GridViewType::difference;
menu.addItem(_("Difference") + (viewType != GridViewType::difference ? L"\tF11" : L""),
[&] { setGridViewType(GridViewType::difference); }, greyScaleIfDisabled(loadImage("compare", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::difference));
menu.addItem(_("Action") + (viewType != GridViewType::action ? L"\tF11" : L""),
[&] { setGridViewType(GridViewType::action); }, greyScaleIfDisabled(loadImage("start_sync", dipToScreen(getMenuIconDipSize())), viewType == GridViewType::action));
menu.popup(*m_bpButtonViewType, {m_bpButtonViewType->GetSize().x, 0});
}
void MainDialog::onViewFilterContext(wxEvent& event)
{
ContextMenu menu;
auto saveButtonDefault = [](const ToggleButton& tb, bool& defaultValue)
{
if (tb.IsShown())
defaultValue = tb.isActive();
};
auto saveDefault = [&]
{
auto& def = globalCfg_.mainDlg.viewFilterDefault;
saveButtonDefault(*m_bpButtonShowExcluded, def.excluded);
saveButtonDefault(*m_bpButtonShowEqual, def.equal);
saveButtonDefault(*m_bpButtonShowConflict, def.conflict);
saveButtonDefault(*m_bpButtonShowLeftOnly, def.leftOnly);
saveButtonDefault(*m_bpButtonShowRightOnly, def.rightOnly);
saveButtonDefault(*m_bpButtonShowLeftNewer, def.leftNewer);
saveButtonDefault(*m_bpButtonShowRightNewer, def.rightNewer);
saveButtonDefault(*m_bpButtonShowDifferent, def.different);
saveButtonDefault(*m_bpButtonShowCreateLeft, def.createLeft);
saveButtonDefault(*m_bpButtonShowCreateRight, def.createRight);
saveButtonDefault(*m_bpButtonShowDeleteLeft, def.deleteLeft);
saveButtonDefault(*m_bpButtonShowDeleteRight, def.deleteRight);
saveButtonDefault(*m_bpButtonShowUpdateLeft, def.updateLeft);
saveButtonDefault(*m_bpButtonShowUpdateRight, def.updateRight);
saveButtonDefault(*m_bpButtonShowDoNothing, def.doNothing);
flashStatusInfo(_("View settings saved"));
};
menu.addItem(_("&Save as default"), saveDefault, loadImage("cfg_save", dipToScreen(getMenuIconDipSize())));
menu.popup(*m_bpButtonViewFilterContext, {m_bpButtonViewFilterContext->GetSize().x, 0});
}
void MainDialog::updateGlobalFilterButton()
{
//global filter: test for Null-filter
setImage(*m_bpButtonFilter, greyScaleIfDisabled(loadImage("options_filter"), !isNullFilter(currentCfg_.mainCfg.globalFilter)));
m_bpButtonFilter->SetToolTip(_("Filter") + L" (F7)" + getFilterSummaryForTooltip(currentCfg_.mainCfg.globalFilter));
//m_bpButtonFilterContext->SetToolTip(m_bpButtonFilter->GetToolTipText());
}
void MainDialog::onCompare(wxCommandEvent& event)
{
/* mitigate unwanted reentrancy caused by wxApp::Yield():
disabling GUI elements is NOT enough! e.g. reentrancy when there's a second click event *already* in the Windows message queue
CAVEAT: This doesn't block all theoretically possible Window events that were queued *before* disableGuiElementsImpl() takes effect,
but at least the 90% case of (rare!) crashes caused by a duplicate click event on comparison or sync button. */
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
//wxBusyCursor dummy; -> redundant: progress already shown in progress dialog!
FocusPreserver fp; //e.g. keep focus on config panel after pressing F5
//give nice hint on what's next to do if user manually clicked on compare
assert(m_buttonCompare->GetId() != wxID_ANY);
if (fp.getFocusId() == m_buttonCompare->GetId())
fp.setFocus(m_buttonSync);
int scrollPosX = 0;
int scrollPosY = 0;
m_gridMainL->GetViewStart(&scrollPosX, &scrollPosY); //preserve current scroll position
ZEN_ON_SCOPE_EXIT(m_gridMainL->Scroll(scrollPosX, scrollPosY); //
m_gridMainR->Scroll(scrollPosX, scrollPosY); //restore
m_gridMainC->Scroll(-1, scrollPosY); ); //
clearGrid(); //avoid memory peak by clearing old data first
const auto& guiCfg = getConfig();
const std::vector<FolderPairCfg>& fpCfgList = extractCompareCfg(guiCfg.mainCfg);
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
//handle status display and error messages
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now(),
guiCfg.mainCfg.ignoreErrors,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileAlertPending);
auto requestPassword = [&, password = Zstring()](const std::wstring& msg, const std::wstring& lastErrorMsg) mutable //throw CancelProcess
{
assert(runningOnMainThread());
if (showPasswordPrompt(this, msg, lastErrorMsg, password) != ConfirmationButton::accept)
statusHandler.cancelProcessNow(CancelReason::user); //throw CancelProcess
return password;
};
try
{
//GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization
std::unique_ptr<LockHolder> dirLocks;
folderCmp_ = compare(globalCfg_.warnDlgs,
globalCfg_.fileTimeTolerance,
requestPassword,
globalCfg_.runWithBackgroundPriority,
globalCfg_.createLockFile,
dirLocks,
fpCfgList,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
//---------------------------------------------------------------------------
setLastOperationLog(r.summary, r.errorLog.ptr());
fullSyncLog_ = {r.errorLog.ref(), r.summary.startTime, r.summary.totalTime};
if (r.summary.result == TaskResult::cancelled)
return updateGui(); //refresh grid in ANY case! (also on abort)
filegrid::setData(*m_gridMainC, folderCmp_); //
treegrid::setData(*m_gridOverview, folderCmp_); //update view on data
updateGui(); //
//play (optional) sound notification
if (!globalCfg_.soundFileCompareFinished.empty())
{
//wxWidgets shows modal error dialog by default => "no, wxWidgets, NO!"
wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership!
ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget));
wxSound::Play(utfTo<wxString>(globalCfg_.soundFileCompareFinished), wxSOUND_ASYNC);
}
if (!IsActive())
RequestUserAttention(); //this == toplevel win, so we also get the taskbar flash!
//remember folder history (except when cancelled by user)
for (const FolderPairCfg& fpCfg : fpCfgList)
{
folderHistoryLeft_ ->addItem(fpCfg.folderPathPhraseLeft_);
folderHistoryRight_->addItem(fpCfg.folderPathPhraseRight_);
}
//mark selected cfg files as "in sync" when there is nothing to do: https://freefilesync.org/forum/viewtopic.php?t=4991
if (r.summary.result == TaskResult::success)
if (getCUD(SyncStatistics(folderCmp_)) == 0)
{
setStatusInfo(_("No files to synchronize"), true /*highlight*/); //user might be AFK: don't flashStatusInfo()
//overwrites status info already set in updateGui() above
cfggrid::getDataView(*m_gridCfgHistory).setLastInSyncTime(activeConfigFiles_, std::chrono::system_clock::to_time_t(r.summary.startTime));
//re-apply selection: sort order changed if sorted by last sync time, or log
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
//m_gridCfgHistory->Refresh(); <- implicit in last call
}
//reset icon cache (IconBuffer) after *each* comparison!
filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.mainDlg.showIcons, convert(globalCfg_.mainDlg.iconSize));
}
void MainDialog::updateGui()
{
updateGridViewData(); //update gridDataView and write status information
const SyncStatistics st(folderCmp_);
updateStatistics(st);
updateUnsavedCfgStatus();
const auto& mainCfg = getConfig().mainCfg;
const std::optional<CompareVariant> cmpVar = getCommonCompVariant(mainCfg);
const std::optional<SyncVariant> syncVar = getCommonSyncVariant(mainCfg);
const char* cmpVarIconName = nullptr;
if (cmpVar)
switch (*cmpVar)
{
case CompareVariant::timeSize: cmpVarIconName = "cmp_time"; break;
case CompareVariant::content: cmpVarIconName = "cmp_content"; break;
case CompareVariant::size: cmpVarIconName = "cmp_size"; break;
}
const char* syncVarIconName = nullptr;
if (syncVar)
switch (*syncVar)
{
case SyncVariant::twoWay: syncVarIconName = "sync_twoway"; break;
case SyncVariant::mirror: syncVarIconName = "sync_mirror"; break;
case SyncVariant::update: syncVarIconName = "sync_update"; break;
case SyncVariant::custom: syncVarIconName = "sync_custom"; break;
}
const bool useDbFile = [&]
{
for (const FolderPairCfg& fpCfg : extractCompareCfg(mainCfg))
if (std::get_if<DirectionByChange>(&fpCfg.directionCfg.dirs))
return true;
return false;
}();
updateTopButton(*m_buttonCompare, loadImage("compare"),
getVariantName(cmpVar), cmpVarIconName,
nullptr /*extraIconName*/,
folderCmp_.empty() ? getColorHighlightCompareButton() : wxNullColour);
updateTopButton(*m_buttonSync, loadImage("start_sync"),
getVariantName(syncVar), syncVarIconName,
useDbFile ? "database" : nullptr,
getCUD(st) != 0 ? getColorHighlightSyncButton() : wxNullColour);
m_panelTopButtons->Layout();
m_menuItemExportList->Enable(!folderCmp_.empty()); //empty CSV confuses users: https://freefilesync.org/forum/viewtopic.php?t=4787
//auiMgr_.Update(); -> doesn't seem to be needed
}
void MainDialog::clearGrid(ptrdiff_t pos)
{
if (!folderCmp_.empty())
{
assert(pos < makeSigned(folderCmp_.size()));
if (pos < 0)
folderCmp_.clear();
else
folderCmp_.erase(folderCmp_.begin() + pos);
}
if (folderCmp_.empty())
fullSyncLog_.reset();
filegrid::setData(*m_gridMainC, folderCmp_);
treegrid::setData(*m_gridOverview, folderCmp_);
updateGui();
}
void MainDialog::updateStatistics(const SyncStatistics& st)
{
auto setValue = [](wxStaticText& txtControl, bool isZeroValue, const wxString& valueAsString, wxStaticBitmap& bmpControl, const char* imageName)
{
if (txtControl.GetLabel() != valueAsString)
{
wxFont fnt = txtControl.GetFont();
fnt.SetWeight(isZeroValue ? wxFONTWEIGHT_NORMAL : wxFONTWEIGHT_BOLD);
txtControl.SetFont(fnt);
txtControl.SetLabelText(valueAsString);
setImage(bmpControl, greyScaleIfDisabled(mirrorIfRtl(loadImage(imageName)), !isZeroValue));
}
};
auto setIntValue = [&setValue](wxStaticText& txtControl, int value, wxStaticBitmap& bmpControl, const char* imageName)
{
setValue(txtControl, value == 0, formatNumber(value), bmpControl, imageName);
};
//update preview of item count and bytes to be transferred:
setValue(*m_staticTextData, st.getBytesToProcess() == 0, formatFilesizeShort(st.getBytesToProcess()), *m_bitmapData, "data");
setIntValue(*m_staticTextCreateLeft, st.createCount<SelectSide::left >(), *m_bitmapCreateLeft, "so_create_left_sicon");
setIntValue(*m_staticTextUpdateLeft, st.updateCount<SelectSide::left >(), *m_bitmapUpdateLeft, "so_update_left_sicon");
setIntValue(*m_staticTextDeleteLeft, st.deleteCount<SelectSide::left >(), *m_bitmapDeleteLeft, "so_delete_left_sicon");
setIntValue(*m_staticTextCreateRight, st.createCount<SelectSide::right>(), *m_bitmapCreateRight, "so_create_right_sicon");
setIntValue(*m_staticTextUpdateRight, st.updateCount<SelectSide::right>(), *m_bitmapUpdateRight, "so_update_right_sicon");
setIntValue(*m_staticTextDeleteRight, st.deleteCount<SelectSide::right>(), *m_bitmapDeleteRight, "so_delete_right_sicon");
m_panelViewFilter->Layout(); //[!] statistics panel size changed, so this is needed
m_panelStatistics->Layout();
m_panelStatistics->Refresh(); //fix small mess up on RTL layout
}
void MainDialog::applyCompareConfig(bool setDefaultViewType)
{
clearGrid(); //+ GUI update
//convenience: change sync view
if (setDefaultViewType)
switch (currentCfg_.mainCfg.cmpCfg.compareVar)
{
case CompareVariant::timeSize:
case CompareVariant::size:
setGridViewType(GridViewType::action);
break;
case CompareVariant::content:
setGridViewType(GridViewType::difference);
break;
}
}
void MainDialog::onStartSync(wxCommandEvent& event)
{
FocusPreserver fp; //e.g. keep focus on config panel after pressing F9
if (folderCmp_.empty())
{
//quick sync: simulate button click on "compare"
wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED);
m_buttonCompare->Command(dummy); //simulate click
if (folderCmp_.empty()) //check if user aborted or error occurred, etc...
return;
}
if (std::exchange(operationInProgress_, true)) //*after* simluated comparison button click!
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
//------------------------------------------------------------------
const auto& guiCfg = getConfig();
//show sync preview/confirmation dialog
if (globalCfg_.confirmDlgs.confirmSyncStart)
{
bool dontShowAgain = false;
if (showSyncConfirmationDlg(this, false /*syncSelection*/,
getCommonSyncVariant(guiCfg.mainCfg),
SyncStatistics(folderCmp_),
dontShowAgain) != ConfirmationButton::accept)
return;
globalCfg_.confirmDlgs.confirmSyncStart = !dontShowAgain;
}
const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
const WindowLayout::Dimensions progressDim
{
globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size,
std::nullopt /*pos*/,
globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized
};
UiInputDisabler uiBlock(*this, false /*enableAbort*/); //StatusHandlerFloatingDialog will internally process Window messages, so avoid unexpected callbacks!
//class handling status updates and error messages
StatusHandlerFloatingDialog statusHandler(this, getJobNames(), syncStartTime,
guiCfg.mainCfg.ignoreErrors,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileSyncFinished,
globalCfg_.soundFileAlertPending,
progressDim,
globalCfg_.progressDlgAutoClose);
try
{
//PERF_START;
//let's report here rather than before comparison (user might have changed global settings in the meantime!)
logNonDefaultSettings(globalCfg_, statusHandler); //throw CancelProcess
//wxBusyCursor dummy; -> redundant: progress already shown in progress dialog!
//GUI mode: end directory lock lifetime after comparion and start new locking right before sync
std::unique_ptr<LockHolder> dirLocks;
if (globalCfg_.createLockFile)
{
std::set<Zstring> folderPathsToLock;
for (const BaseFolderPair& baseFolder : asRange(folderCmp_))
{
if (baseFolder.getFolderStatus<SelectSide::left>() == BaseFolderStatus::existing) //do NOT check directory existence again!
if (const Zstring& nativePath = getNativeItemPath(baseFolder.getAbstractPath<SelectSide::left>()); //restrict directory locking to native paths until further
!nativePath.empty())
folderPathsToLock.insert(nativePath);
if (baseFolder.getFolderStatus<SelectSide::right>() == BaseFolderStatus::existing)
if (const Zstring& nativePath = getNativeItemPath(baseFolder.getAbstractPath<SelectSide::right>());
!nativePath.empty())
folderPathsToLock.insert(nativePath);
}
dirLocks = std::make_unique<LockHolder>(folderPathsToLock, globalCfg_.warnDlgs.warnDirectoryLockFailed, statusHandler); //throw CancelProcess
}
synchronize(syncStartTime,
globalCfg_.verifyFileCopy,
globalCfg_.copyLockedFiles,
globalCfg_.copyFilePermissions,
globalCfg_.failSafeFileCopy,
globalCfg_.runWithBackgroundPriority,
extractSyncCfg(guiCfg.mainCfg),
folderCmp_,
globalCfg_.warnDlgs,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) { assert(statusHandler.taskCancelled() == CancelReason::user); }
//-------------------------------------------------------------------
StatusHandlerFloatingDialog::Result r = statusHandler.prepareResult();
//merge logs of comparison, manual operations, sync
append(fullSyncLog_->log, r.errorLog.ref());
fullSyncLog_->totalTime += r.summary.totalTime;
//"consume" fullSyncLog_, but don't reset: there may be items remaining for manual operations or re-sync!
ProcessSummary fullSummary = r.summary;
fullSummary.startTime = std::exchange(fullSyncLog_->startTime, std::chrono::system_clock::now());
fullSummary.totalTime = std::exchange(fullSyncLog_->totalTime, {});
//let's *not* redetermine "ProcessSummary::result", even if errors occured during manual operations!
ErrorLog fullLog = std::exchange(fullSyncLog_->log, {});
auto logMsg2 =[&](const std::wstring& msg, MessageType type)
{
logMsg(fullLog, msg, type);
logMsg(r.errorLog.ref(), msg, type);
};
AbstractPath logFolderPath = createAbstractPath(guiCfg.mainCfg.altLogFolderPathPhrase); //optional
if (AFS::isNullPath(logFolderPath))
logFolderPath = createAbstractPath(globalCfg_.logFolderPhrase);
assert(!AFS::isNullPath(logFolderPath)); //mandatory! but still: let's include fall back
if (AFS::isNullPath(logFolderPath))
logFolderPath = createAbstractPath(getLogFolderDefaultPath());
AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, generateLogFileName(globalCfg_.logFormat, fullSummary));
//e.g. %AppData%\FreeFileSync\Logs\Backup FreeFileSync 2013-09-15 015052.123 [Error].log
auto notifyStatusNoThrow = [&](std::wstring&& msg) { try { statusHandler.updateStatus(std::move(msg)); /*throw CancelProcess*/ } catch (CancelProcess&) {} };
if (statusHandler.taskCancelled())
/* user cancelled => don't run post sync command
=> don't run post sync action
=> don't send email notification
=> don't play sound notification
(=> DO save log file: sync attempt is more than just a "manual operation")
(=> DO update last sync stats for the selected cfg files) */
assert(statusHandler.taskCancelled() == CancelReason::user); //"stop on first error" is only for ffs_batch
else
{
//--------------------- post sync command ----------------------
if (const Zstring cmdLine = trimCpy(expandMacros(guiCfg.mainCfg.postSyncCommand));
!cmdLine.empty())
if (guiCfg.mainCfg.postSyncCondition == PostSyncCondition::completion ||
(guiCfg.mainCfg.postSyncCondition == PostSyncCondition::errors) == (r.summary.result == TaskResult::cancelled ||
r.summary.result == TaskResult::error))
try
{
//give consoleExecute() some "time to fail", but not too long to hang our process
const int DEFAULT_APP_TIMEOUT_MS = 100;
if (const auto& [exitCode, output] = consoleExecute(cmdLine, DEFAULT_APP_TIMEOUT_MS); //throw SysError, SysErrorTimeOut
exitCode != 0)
throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), utfTo<std::wstring>(output)));
logMsg2(_("Executing command:") + L' ' + utfTo<std::wstring>(cmdLine) + L" [" + replaceCpy(_("Exit code %x"), L"%x", L"0") + L']', MSG_TYPE_INFO);
}
catch (SysErrorTimeOut&) //child process not failed yet => probably fine :>
{
logMsg2(_("Executing command:") + L' ' + utfTo<std::wstring>(cmdLine), MSG_TYPE_INFO);
}
catch (const SysError& e)
{
logMsg2(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)) + L"\n\n" + e.toString(), MSG_TYPE_ERROR);
}
//--------------------- email notification ----------------------
if (const std::string notifyEmail = trimCpy(guiCfg.mainCfg.emailNotifyAddress);
!notifyEmail.empty())
if (guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::always ||
(guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorWarning && (fullSummary.result == TaskResult::cancelled ||
fullSummary.result == TaskResult::error ||
fullSummary.result == TaskResult::warning)) ||
(guiCfg.mainCfg.emailNotifyCondition == ResultsNotification::errorOnly && (fullSummary.result == TaskResult::cancelled ||
fullSummary.result == TaskResult::error)))
try
{
logMsg2(replaceCpy(_("Sending email notification to %x"), L"%x", utfTo<std::wstring>(notifyEmail)), MSG_TYPE_INFO);
sendLogAsEmail(notifyEmail, fullSummary, fullLog, logFilePath, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e) { logMsg2(e.toString(), MSG_TYPE_ERROR); }
}
//--------------------- save log file ----------------------
std::set<AbstractPath> logsToKeepPaths;
{
const std::set<Zstring /*cfg file path*/, LessNativePath> activeCfgSorted(activeConfigFiles_.begin(), activeConfigFiles_.end());
for (const ConfigFileItem& cfi : cfggrid::getDataView(*m_gridCfgHistory).get())
if (!activeCfgSorted.contains(cfi.cfgFilePath)) //exception: don't keep old logs for the selected cfg files!
logsToKeepPaths.insert(cfi.lastRunStats.logFilePath);
}
try //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. include status in log file name without extra rename
{
//do NOT use tryReportingError()! saving log files should not be cancellable!
saveLogFile(logFilePath, fullSummary, fullLog, globalCfg_.logfilesMaxAgeDays, globalCfg_.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e)
{
try //fallback: log file *must* be saved no matter what!
{
const AbstractPath logFileDefaultPath = AFS::appendRelPath(createAbstractPath(getLogFolderDefaultPath()), generateLogFileName(globalCfg_.logFormat, fullSummary));
if (logFilePath == logFileDefaultPath)
throw;
logMsg2(e.toString(), MSG_TYPE_ERROR);
logFilePath = logFileDefaultPath;
saveLogFile(logFileDefaultPath, fullSummary, fullLog, globalCfg_.logfilesMaxAgeDays, globalCfg_.logFormat, logsToKeepPaths, notifyStatusNoThrow); //throw FileError
}
catch (const FileError& e2) { logMsg2(e2.toString(), MSG_TYPE_ERROR); logExtraError(e2.toString()); } //should never happen!!!
}
//--------- update last sync stats for the selected cfg files ---------
const ErrorLogStats& fullLogStats = getStats(fullLog);
cfggrid::getDataView(*m_gridCfgHistory).setLastRunStats(activeConfigFiles_,
{
std::chrono::system_clock::to_time_t(fullSummary.startTime),
logFilePath,
fullSummary.result,
fullSummary.statsProcessed.items,
fullSummary.statsProcessed.bytes,
fullSummary.totalTime,
fullLogStats.errors,
fullLogStats.warnings,
});
//re-apply selection: sort order changed if sorted by last sync time
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
//m_gridCfgHistory->Refresh(); <- implicit in last call
//---------------------------------------------------------------------------
setLastOperationLog(r.summary, r.errorLog.ptr());
//remove empty rows: just a beautification, invalid rows shouldn't cause issues
filegrid::getDataView(*m_gridMainC).removeInvalidRows();
//---------------------------------------------------------------------------
const StatusHandlerFloatingDialog::DlgOptions dlgOpt = statusHandler.showResult();
globalCfg_.progressDlgAutoClose = dlgOpt.autoCloseSelected;
globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.size = dlgOpt.dim.size; //=> ignore dim.pos
globalCfg_.dpiLayouts[getDpiScalePercent()].progressDlg.isMaximized = dlgOpt.dim.isMaximized;
updateGui(); //let's update *after* showResult(): some users are interested in seeing the old statistics dialog even after sync
//---------------------------------------------------------------------------
//run shutdown *after* last sync stats were updated! they will be saved via onBeforeSystemShutdownCookie_: https://freefilesync.org/forum/viewtopic.php?t=5761
using FinalRequest = StatusHandlerFloatingDialog::FinalRequest;
switch (dlgOpt.finalRequest)
{
case FinalRequest::none:
break;
case FinalRequest::exit:
//don't Close() which prompts to save current config in onClose()
Destroy(); //for top-level windows this employs delayed destruction (wxPendingDelete)
uiBlock.dismiss(); //...or else: crash when ~UiInputDisabler() calls Yield() + enableGuiElementsImpl()!
fp .dismiss();
break;
case FinalRequest::shutdown:
try
{
shutdownSystem(); //throw FileError
terminateProcess(static_cast<int>(FfsExitCode::success));
//no point in continuing and saving cfg again in ~MainDialog()/onBeforeSystemShutdown() while the OS will kill us any time!
}
catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); }
//[!] ignores current error handling setting, BUT this is not a sync error!
break;
}
}
namespace
{
void appendInactive(ContainerObject& conObj, std::vector<FileSystemObject*>& inactiveItems)
{
for (FilePair& file : conObj.files())
if (!file.isActive())
inactiveItems.push_back(&file);
for (SymlinkPair& symlink : conObj.symlinks())
if (!symlink.isActive())
inactiveItems.push_back(&symlink);
for (FolderPair& folder : conObj.subfolders())
{
if (!folder.isActive())
inactiveItems.push_back(&folder);
appendInactive(folder, inactiveItems); //recurse
}
}
}
void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& selection)
{
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
//------------------ analyze selection ------------------
std::unordered_set<const BaseFolderPair*> basePairsSelect;
std::vector<FileSystemObject*> selectedActive;
for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection))
{
switch (fsObj->getSyncOperation())
{
case SO_CREATE_LEFT:
case SO_CREATE_RIGHT:
case SO_DELETE_LEFT:
case SO_DELETE_RIGHT:
case SO_MOVE_LEFT_FROM:
case SO_MOVE_LEFT_TO:
case SO_MOVE_RIGHT_FROM:
case SO_MOVE_RIGHT_TO:
case SO_OVERWRITE_LEFT:
case SO_OVERWRITE_RIGHT:
case SO_RENAME_LEFT:
case SO_RENAME_RIGHT:
basePairsSelect.insert(&fsObj->base());
break;
case SO_UNRESOLVED_CONFLICT:
case SO_DO_NOTHING:
case SO_EQUAL:
break;
}
if (fsObj->isActive())
selectedActive.push_back(fsObj);
}
if (basePairsSelect.empty())
return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled!
FocusPreserver fp;
{
//---------------------------------------------------------------
//simulate partial sync by temporarily excluding all other items:
std::vector<FileSystemObject*> inactiveItems; //remember inactive (assuming a smaller number than active items)
for (BaseFolderPair& baseFolder : asRange(folderCmp_))
appendInactive(baseFolder, inactiveItems);
setActiveStatus(false, folderCmp_); //limit to folderCmpSelect? => no, let's also activate non-participating folder pairs, if only to visually match user selection
for (FileSystemObject* fsObj : selectedActive)
fsObj->setActive(true);
//don't run a full updateGui() (which would remove excluded rows) since we're only temporarily excluding:
filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR);
m_gridOverview->Refresh();
ZEN_ON_SCOPE_EXIT(
setActiveStatus(true, folderCmp_);
//inactive items are expected to still exist after sync! => no need for FileSystemObject::ObjectId
for (FileSystemObject* fsObj : inactiveItems)
fsObj->setActive(false);
filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); //e.g. if user cancels confirmation popup
m_gridOverview->Refresh();
);
//---------------------------------------------------------------
const auto& guiCfg = getConfig();
const std::vector<FolderPairSyncCfg> fpCfg = extractSyncCfg(guiCfg.mainCfg);
//only apply partial sync to base pairs that contain at least one item to sync (e.g. avoid needless sync.ffs_db updates)
std::vector<SharedRef<BaseFolderPair>> folderCmpSelect;
std::vector<FolderPairSyncCfg> fpCfgSelect;
for (size_t i = 0; i < folderCmp_.size(); ++i)
if (basePairsSelect.contains(&folderCmp_[i].ref()))
{
folderCmpSelect.push_back(folderCmp_[i]);
fpCfgSelect .push_back( fpCfg[i]);
}
//show sync preview/confirmation dialog
if (globalCfg_.confirmDlgs.confirmSyncStart)
{
bool dontShowAgain = false;
if (showSyncConfirmationDlg(this,
true /*syncSelection*/,
getCommonSyncVariant(guiCfg.mainCfg),
SyncStatistics(folderCmpSelect),
dontShowAgain) != ConfirmationButton::accept)
return;
globalCfg_.confirmDlgs.confirmSyncStart = !dontShowAgain;
}
const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
//last sync log file? => let's go without; same behavior as manual deletion
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, syncStartTime,
guiCfg.mainCfg.ignoreErrors,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
globalCfg_.soundFileAlertPending);
try
{
//let's report here rather than before comparison (user might have changed global settings in the meantime!)
logNonDefaultSettings(globalCfg_, statusHandler); //throw CancelProcess
//LockHolder? => let's go without; same behavior as manual deletion
synchronize(syncStartTime,
globalCfg_.verifyFileCopy,
globalCfg_.copyLockedFiles,
globalCfg_.copyFilePermissions,
globalCfg_.failSafeFileCopy,
globalCfg_.runWithBackgroundPriority,
fpCfgSelect,
folderCmpSelect,
globalCfg_.warnDlgs,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
append(fullSyncLog_->log, r.errorLog.ref());
fullSyncLog_->totalTime += r.summary.totalTime;
} //run updateGui() *after* reverting our temporary exclusions
//remove empty rows: just a beautification, invalid rows shouldn't cause issues
filegrid::getDataView(*m_gridMainC).removeInvalidRows();
updateGui();
}
void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::shared_ptr<const ErrorLog>& errorLog)
{
const wxImage syncResultImage = [&]
{
switch (summary.result)
{
case TaskResult::success:
return loadImage("result_success");
case TaskResult::warning:
return loadImage("result_warning");
case TaskResult::error:
case TaskResult::cancelled:
return loadImage("result_error");
}
assert(false);
return wxNullImage;
}();
const wxImage logOverlayImage = [&]
{
//don't use "syncResult": There may be errors after sync, e.g. failure to save log file/send email!
if (errorLog)
{
const ErrorLogStats logCount = getStats(*errorLog);
if (logCount.errors > 0)
return loadImage("msg_error", dipToScreen(getMenuIconDipSize()));
if (logCount.warnings > 0)
return loadImage("msg_warning", dipToScreen(getMenuIconDipSize()));
//return loadImage("msg_success", dipToScreen(getMenuIconDipSize())); -> too noisy?
}
return wxNullImage;
}();
setImage(*m_bitmapSyncResult, syncResultImage);
m_staticTextSyncResult->SetLabelText(getSyncResultLabel(summary.result));
m_staticTextItemsProcessed->SetLabelText(formatNumber(summary.statsProcessed.items));
m_staticTextBytesProcessed->SetLabelText(L'(' + formatFilesizeShort(summary.statsProcessed.bytes) + L')');
const bool hideRemainingStats = (summary.statsTotal.items < 0 && summary.statsTotal.bytes < 0) || //no total items/bytes: e.g. for pure folder comparison
summary.statsProcessed == summary.statsTotal; //...if everything was processed successfully
m_staticTextProcessed ->Show(!hideRemainingStats);
m_staticTextRemaining ->Show(!hideRemainingStats);
m_staticTextItemsRemaining->Show(!hideRemainingStats);
m_staticTextBytesRemaining->Show(!hideRemainingStats);
if (!hideRemainingStats)
{
m_staticTextItemsRemaining->SetLabelText( formatNumber(summary.statsTotal.items - summary.statsProcessed.items));
m_staticTextBytesRemaining->SetLabelText(L'(' + formatFilesizeShort(summary.statsTotal.bytes - summary.statsProcessed.bytes) + L')');
}
const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(summary.totalTime).count();
m_staticTextTimeElapsed->SetLabelText(utfTo<wxString>(formatTimeSpan(totalTimeSec, true /*hourRequired*/)));
//include "hour" => let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308
logPanel_->setLog(errorLog);
m_panelLog->Layout();
//m_panelItemStats->Dimensions(); //needed?
//m_panelTimeStats->Dimensions(); //
const wxImage& logBtnImg = layOver(loadImage("log_file"), logOverlayImage, wxALIGN_BOTTOM | wxALIGN_RIGHT);
m_bpButtonToggleLog->init(layOver(generatePressedButtonBack(logBtnImg.GetSize() + wxSize(dipToScreen(10), dipToScreen(10))), logBtnImg), logBtnImg);
const int logBtnSize = m_bpButtonViewType->GetSize().GetHeight();
m_bpButtonToggleLog->SetMinSize({logBtnSize, logBtnSize});
m_bpButtonToggleLog->Show(static_cast<bool>(errorLog));
}
void MainDialog::onToggleLog(wxCommandEvent& event)
{
showLogPanel(!m_bpButtonToggleLog->isActive());
}
void MainDialog::showLogPanel(bool show)
{
m_bpButtonToggleLog->setActive(show);
if (wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog);
logPane.IsShown() != show)
{
if (!show)
{
if (logPane.IsMaximized())
auiMgr_.RestorePane(logPane); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?)
else //ensure current window sizes will be used when pane is shown again:
logPane.best_size = logPane.rect.GetSize();
}
logPane.Show(show);
auiMgr_.Update();
m_panelLog->Refresh(); //macOS: fix background corruption for the statistics boxes; call *after* wxAuiManager::Update()
}
if (show)
{
if (wxWindow* focus = wxWindow::FindFocus()) //restore when closing panel!
if (!isComponentOf(focus, m_panelLog))
focusAfterCloseLog_ = focus->GetId();
logPanel_->SetFocus();
}
else
{
if (isComponentOf(wxWindow::FindFocus(), m_panelLog))
if (wxWindow* oldFocusWin = wxWindow::FindWindowById(focusAfterCloseLog_))
oldFocusWin->SetFocus();
focusAfterCloseLog_ = wxID_ANY;
}
}
void MainDialog::onGridDoubleClickRim(GridClickEvent& event, bool leftSide)
{
if (!globalCfg_.externalApps.empty())
{
std::vector<FileSystemObject*> selectionL;
std::vector<FileSystemObject*> selectionR;
if (FileSystemObject* fsObj = filegrid::getDataView(*m_gridMainC).getFsObject(event.row_)) //selection must be a list of BOUND pointers!
(leftSide ? selectionL: selectionR) = {fsObj};
openExternalApplication(globalCfg_.externalApps[0].cmdLine, leftSide, selectionL, selectionR);
}
}
void MainDialog::onGridLabelLeftClickRim(GridLabelClickEvent& event, bool leftSide)
{
const ColumnTypeRim colType = static_cast<ColumnTypeRim>(event.colType_);
bool sortAscending = getDefaultSortDirection(colType);
if (auto sortInfo = filegrid::getDataView(*m_gridMainC).getSortConfig())
if (const ColumnTypeRim* sortType = std::get_if<ColumnTypeRim>(&sortInfo->sortCol))
if (*sortType == colType && sortInfo->onLeft == leftSide)
sortAscending = !sortInfo->ascending;
const ItemPathFormat itemPathFormat = leftSide ? globalCfg_.mainDlg.itemPathFormatLeftGrid : globalCfg_.mainDlg.itemPathFormatRightGrid;
filegrid::getDataView(*m_gridMainC).sortView(colType, itemPathFormat, leftSide, sortAscending);
updateGui(); //refresh gridDataView
m_gridMainL->clearSelection(GridEventPolicy::deny); //call *after* updateGui/updateGridViewData() has restored FileView::viewRef_
m_gridMainC->clearSelection(GridEventPolicy::deny);
m_gridMainR->clearSelection(GridEventPolicy::deny);
}
void MainDialog::onGridLabelLeftClickC(GridLabelClickEvent& event)
{
const ColumnTypeCenter colType = static_cast<ColumnTypeCenter>(event.colType_);
if (colType != ColumnTypeCenter::checkbox)
{
bool sortAscending = getDefaultSortDirection(colType);
if (auto sortInfo = filegrid::getDataView(*m_gridMainC).getSortConfig())
if (const ColumnTypeCenter* sortType = std::get_if<ColumnTypeCenter>(&sortInfo->sortCol))
if (*sortType == colType)
sortAscending = !sortInfo->ascending;
filegrid::getDataView(*m_gridMainC).sortView(colType, sortAscending);
updateGui(); //refresh gridDataView
m_gridMainL->clearSelection(GridEventPolicy::deny);
m_gridMainC->clearSelection(GridEventPolicy::deny);
m_gridMainR->clearSelection(GridEventPolicy::deny);
}
}
void MainDialog::swapSides()
{
if (std::exchange(operationInProgress_, true))
return;
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
FocusPreserver fp;
if (!folderCmp_.empty() && //require confirmation only *after* comparison
globalCfg_.confirmDlgs.confirmSwapSides)
{
bool dontWarnAgain = false;
switch (showConfirmationDialog(this, DialogInfoType::info,
PopupDialogCfg().setMainInstructions(_("Please confirm you want to swap sides.")).
setCheckBox(dontWarnAgain, _("&Don't show this dialog again")),
_("&Swap")))
{
case ConfirmationButton::accept: //swap
globalCfg_.confirmDlgs.confirmSwapSides = !dontWarnAgain;
break;
case ConfirmationButton::cancel:
return;
}
}
//------------------------------------------------------
//swap directory names:
LocalPairConfig lpc1st = firstFolderPair_->getValues();
std::swap(lpc1st.folderPathPhraseLeft, lpc1st.folderPathPhraseRight);
firstFolderPair_->setValues(lpc1st);
for (FolderPairPanel* panel : additionalFolderPairs_)
{
LocalPairConfig lpc = panel->getValues();
std::swap(lpc.folderPathPhraseLeft, lpc.folderPathPhraseRight);
panel->setValues(lpc);
}
//swap view filter
bool tmp = m_bpButtonShowLeftOnly->isActive();
m_bpButtonShowLeftOnly->setActive(m_bpButtonShowRightOnly->isActive());
m_bpButtonShowRightOnly->setActive(tmp);
tmp = m_bpButtonShowLeftNewer->isActive();
m_bpButtonShowLeftNewer->setActive(m_bpButtonShowRightNewer->isActive());
m_bpButtonShowRightNewer->setActive(tmp);
/* for sync preview and "mirror" variant swapping may create strange effect:
tmp = m_bpButtonShowCreateLeft->isActive();
m_bpButtonShowCreateLeft->setActive(m_bpButtonShowCreateRight->isActive());
m_bpButtonShowCreateRight->setActive(tmp);
tmp = m_bpButtonShowDeleteLeft->isActive();
m_bpButtonShowDeleteLeft->setActive(m_bpButtonShowDeleteRight->isActive());
m_bpButtonShowDeleteRight->setActive(tmp);
tmp = m_bpButtonShowUpdateLeft->isActive();
m_bpButtonShowUpdateLeft->setActive(m_bpButtonShowUpdateRight->isActive());
m_bpButtonShowUpdateRight->setActive(tmp);
*/
//----------------------------------------------------------------------
if (!folderCmp_.empty())
{
const auto& guiCfg = getConfig();
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
Zstr("") /*soundFileAlertPending*/);
try
{
statusHandler.initNewPhase(-1, -1, ProcessPhase::none);
swapGrids(getConfig().mainCfg, folderCmp_,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
}
updateGui(); //e.g. unsaved changes
flashStatusInfo(_("Left and right sides have been swapped"));
}
void MainDialog::updateGridViewData()
{
auto updateFilterButton = [&](ToggleButton& btn, const char* imgName, int itemCount)
{
const bool show = itemCount > 0;
if (show)
{
int& itemCountDrawn = buttonLabelItemCount_[&btn];
assert(itemCount != 0); //itemCountDrawn defaults to 0!
if (itemCountDrawn != itemCount) //perf: only regenerate button labels when needed!
{
itemCountDrawn = itemCount;
//accessibility: always set both foreground AND background colors!
wxImage imgCountPressed = mirrorIfRtl(createImageFromText(formatNumber(itemCount), btn.GetFont().Bold(), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)));
wxImage imgCountReleased = mirrorIfRtl(createImageFromText(formatNumber(itemCount), btn.GetFont(), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT)));
imgCountReleased = resizeCanvas(imgCountReleased, imgCountPressed.GetSize(), wxALIGN_CENTER); //match with imgCountPressed's bold font
//add bottom/right border space
imgCountPressed = resizeCanvas(imgCountPressed, imgCountPressed .GetSize() + wxSize(dipToScreen(5), dipToScreen(5)), wxALIGN_TOP | wxALIGN_LEFT);
imgCountReleased = resizeCanvas(imgCountReleased, imgCountReleased.GetSize() + wxSize(dipToScreen(5), dipToScreen(5)), wxALIGN_TOP | wxALIGN_LEFT);
wxImage imgCategory = loadImage(imgName);
imgCategory = resizeCanvas(imgCategory, imgCategory.GetSize() + wxSize(dipToScreen(5), dipToScreen(2)), wxALIGN_CENTER);
wxImage imgIconReleased = imgCategory.ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally!
brighten(imgIconReleased, 80);
wxImage imgButtonPressed = stackImages(imgCategory, imgCountPressed, ImageStackLayout::horizontal, ImageStackAlignment::bottom);
wxImage imgButtonReleased = stackImages(imgIconReleased, imgCountReleased, ImageStackLayout::horizontal, ImageStackAlignment::bottom);
btn.init(mirrorIfRtl(layOver(generatePressedButtonBack(imgButtonPressed.GetSize()), imgButtonPressed)),
mirrorIfRtl(imgButtonReleased));
}
}
if (btn.IsShown() != show)
btn.Show(show);
};
FileView::FileStats fileStatsLeft;
FileView::FileStats fileStatsRight;
if (m_bpButtonViewType->isActive())
{
const FileView::ActionViewStats viewStats = filegrid::getDataView(*m_gridMainC).applyActionFilter(m_bpButtonShowExcluded->isActive(),
m_bpButtonShowCreateLeft ->isActive(),
m_bpButtonShowCreateRight->isActive(),
m_bpButtonShowDeleteLeft ->isActive(),
m_bpButtonShowDeleteRight->isActive(),
m_bpButtonShowUpdateLeft ->isActive(),
m_bpButtonShowUpdateRight->isActive(),
m_bpButtonShowDoNothing ->isActive(),
m_bpButtonShowEqual ->isActive(),
m_bpButtonShowConflict ->isActive());
fileStatsLeft = viewStats.fileStatsLeft;
fileStatsRight = viewStats.fileStatsRight;
//sync preview buttons
updateFilterButton(*m_bpButtonShowExcluded, "cat_excluded", viewStats.excluded);
updateFilterButton(*m_bpButtonShowEqual, "cat_equal", viewStats.equal);
updateFilterButton(*m_bpButtonShowConflict, "cat_conflict", viewStats.conflict);
updateFilterButton(*m_bpButtonShowCreateLeft, "so_create_left", viewStats.createLeft);
updateFilterButton(*m_bpButtonShowCreateRight, "so_create_right", viewStats.createRight);
updateFilterButton(*m_bpButtonShowDeleteLeft, "so_delete_left", viewStats.deleteLeft);
updateFilterButton(*m_bpButtonShowDeleteRight, "so_delete_right", viewStats.deleteRight);
updateFilterButton(*m_bpButtonShowUpdateLeft, "so_update_left", viewStats.updateLeft);
updateFilterButton(*m_bpButtonShowUpdateRight, "so_update_right", viewStats.updateRight);
updateFilterButton(*m_bpButtonShowDoNothing, "so_none", viewStats.updateNone);
m_bpButtonShowLeftOnly ->Hide();
m_bpButtonShowRightOnly ->Hide();
m_bpButtonShowLeftNewer ->Hide();
m_bpButtonShowRightNewer->Hide();
m_bpButtonShowDifferent ->Hide();
}
else
{
const FileView::DifferenceViewStats viewStats = filegrid::getDataView(*m_gridMainC).applyDifferenceFilter(m_bpButtonShowExcluded->isActive(),
m_bpButtonShowLeftOnly ->isActive(),
m_bpButtonShowRightOnly ->isActive(),
m_bpButtonShowLeftNewer ->isActive(),
m_bpButtonShowRightNewer->isActive(),
m_bpButtonShowDifferent ->isActive(),
m_bpButtonShowEqual ->isActive(),
m_bpButtonShowConflict ->isActive());
fileStatsLeft = viewStats.fileStatsLeft;
fileStatsRight = viewStats.fileStatsRight;
//comparison result view buttons
updateFilterButton(*m_bpButtonShowExcluded, "cat_excluded", viewStats.excluded);
updateFilterButton(*m_bpButtonShowEqual, "cat_equal", viewStats.equal);
updateFilterButton(*m_bpButtonShowConflict, "cat_conflict", viewStats.conflict);
m_bpButtonShowCreateLeft ->Hide();
m_bpButtonShowCreateRight->Hide();
m_bpButtonShowDeleteLeft ->Hide();
m_bpButtonShowDeleteRight->Hide();
m_bpButtonShowUpdateLeft ->Hide();
m_bpButtonShowUpdateRight->Hide();
m_bpButtonShowDoNothing ->Hide();
updateFilterButton(*m_bpButtonShowLeftOnly, "cat_left_only", viewStats.leftOnly);
updateFilterButton(*m_bpButtonShowRightOnly, "cat_right_only", viewStats.rightOnly);
updateFilterButton(*m_bpButtonShowLeftNewer, "cat_left_newer", viewStats.leftNewer);
updateFilterButton(*m_bpButtonShowRightNewer, "cat_right_newer", viewStats.rightNewer);
updateFilterButton(*m_bpButtonShowDifferent, "cat_different", viewStats.different);
}
const bool anyViewButtonShown = m_bpButtonShowExcluded ->IsShown() ||
m_bpButtonShowEqual ->IsShown() ||
m_bpButtonShowConflict ->IsShown() ||
m_bpButtonShowCreateLeft ->IsShown() ||
m_bpButtonShowCreateRight->IsShown() ||
m_bpButtonShowDeleteLeft ->IsShown() ||
m_bpButtonShowDeleteRight->IsShown() ||
m_bpButtonShowUpdateLeft ->IsShown() ||
m_bpButtonShowUpdateRight->IsShown() ||
m_bpButtonShowDoNothing ->IsShown() ||
m_bpButtonShowLeftOnly ->IsShown() ||
m_bpButtonShowRightOnly ->IsShown() ||
m_bpButtonShowLeftNewer ->IsShown() ||
m_bpButtonShowRightNewer->IsShown() ||
m_bpButtonShowDifferent ->IsShown();
m_bpButtonViewType ->Show(anyViewButtonShown);
m_bpButtonViewFilterContext->Show(anyViewButtonShown);
//m_panelViewFilter->Dimensions(); -> yes, needed, but will also be called in updateStatistics();
//all three grids retrieve their data directly via gridDataView
filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR);
//overview panel
if (m_bpButtonViewType->isActive())
treegrid::getDataView(*m_gridOverview).applyActionFilter(m_bpButtonShowExcluded ->isActive(),
m_bpButtonShowCreateLeft ->isActive(),
m_bpButtonShowCreateRight->isActive(),
m_bpButtonShowDeleteLeft ->isActive(),
m_bpButtonShowDeleteRight->isActive(),
m_bpButtonShowUpdateLeft ->isActive(),
m_bpButtonShowUpdateRight->isActive(),
m_bpButtonShowDoNothing ->isActive(),
m_bpButtonShowEqual ->isActive(),
m_bpButtonShowConflict ->isActive());
else
treegrid::getDataView(*m_gridOverview).applyDifferenceFilter(m_bpButtonShowExcluded ->isActive(),
m_bpButtonShowLeftOnly ->isActive(),
m_bpButtonShowRightOnly ->isActive(),
m_bpButtonShowLeftNewer ->isActive(),
m_bpButtonShowRightNewer->isActive(),
m_bpButtonShowDifferent ->isActive(),
m_bpButtonShowEqual ->isActive(),
m_bpButtonShowConflict ->isActive());
m_gridOverview->Refresh();
//update status bar information
setStatusBarFileStats(fileStatsLeft, fileStatsRight);
}
void MainDialog::setStatusBarFileStats(FileView::FileStats statsLeft,
FileView::FileStats statsRight)
{
//update status information
bSizerStatusLeftDirectories->Show(statsLeft.folderCount > 0);
bSizerStatusLeftFiles ->Show(statsLeft.fileCount > 0);
setText(*m_staticTextStatusLeftDirs, _P("1 directory", "%x directories", statsLeft.folderCount));
setText(*m_staticTextStatusLeftFiles, _P("1 file", "%x files", statsLeft.fileCount));
setText(*m_staticTextStatusLeftBytes, L'(' + formatFilesizeShort(statsLeft.bytes) + L')');
//------------------------------------------------------------------------------
bSizerStatusRightDirectories->Show(statsRight.folderCount > 0);
bSizerStatusRightFiles ->Show(statsRight.fileCount > 0);
setText(*m_staticTextStatusRightDirs, _P("1 directory", "%x directories", statsRight.folderCount));
setText(*m_staticTextStatusRightFiles, _P("1 file", "%x files", statsRight.fileCount));
setText(*m_staticTextStatusRightBytes, L'(' + formatFilesizeShort(statsRight.bytes) + L')');
//------------------------------------------------------------------------------
wxString statusCenterNew;
if (filegrid::getDataView(*m_gridMainC).rowsTotal() > 0)
{
statusCenterNew = _P("Showing %y of 1 item", "Showing %y of %x items", filegrid::getDataView(*m_gridMainC).rowsTotal());
replace(statusCenterNew, L"%y", formatNumber(filegrid::getDataView(*m_gridMainC).rowsOnView())); //%x used as plural form placeholder!
}
setStatusInfo(statusCenterNew, false /*highlight*/);
}
void MainDialog::applyFilterConfig()
{
applyFiltering(folderCmp_, getConfig().mainCfg);
updateGui();
//updateGuiDelayedIf(currentCfg.hideExcludedItems); //show update GUI before removing rows
}
void MainDialog::applySyncDirections()
{
if (!folderCmp_.empty())
{
if (std::exchange(operationInProgress_, true))
//can't just skip:t now's a really bad time! Hopefully never happens!?
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Sync direction changed while other operation running.");
ZEN_ON_SCOPE_EXIT(operationInProgress_ = false);
FocusPreserver fp;
UiInputDisabler uiBlock(*this, true /*enableAbort*/); //StatusHandlerTemporaryPanel calls wxApp::Yield(), so avoid unexpected callbacks!
const auto& guiCfg = getConfig();
const auto& directCfgs = extractDirectionCfg(folderCmp_, getConfig().mainCfg);
StatusHandlerTemporaryPanel statusHandler(*this, std::chrono::system_clock::now() /*startTime*/,
false /*ignoreErrors*/,
guiCfg.mainCfg.autoRetryCount,
guiCfg.mainCfg.autoRetryDelay,
Zstr("") /*soundFileAlertPending*/);
try
{
statusHandler.initNewPhase(-1, -1, ProcessPhase::none);
redetermineSyncDirection(directCfgs,
statusHandler); //throw CancelProcess
}
catch (CancelProcess&) {}
const StatusHandlerTemporaryPanel::Result r = statusHandler.prepareResult(); //noexcept
setLastOperationLog(r.summary, r.errorLog.ptr());
}
updateGui(); //e.g. unsaved changes
}
void MainDialog::onSearchGridEnter(wxCommandEvent& event)
{
startFindNext(true /*searchAscending*/);
}
void MainDialog::onHideSearchPanel(wxCommandEvent& event)
{
showFindPanel(false /*show*/);
}
void MainDialog::onSearchPanelKeyPressed(wxKeyEvent& event)
{
switch (event.GetKeyCode())
{
case WXK_RETURN:
case WXK_NUMPAD_ENTER: //catches ENTER keys while focus is on *any* part of m_panelSearch! Seems to obsolete onSearchGridEnter()!
startFindNext(true /*searchAscending*/);
return;
case WXK_ESCAPE:
showFindPanel(false /*show*/);
return;
}
event.Skip();
}
void MainDialog::showFindPanel(bool show) //CTRL + F or F3 with empty search phrase
{
if (auiMgr_.GetPane(m_panelSearch).IsShown() != show)
{
auiMgr_.GetPane(m_panelSearch).Show(show);
auiMgr_.Update();
}
if (show)
{
m_textCtrlSearchTxt->SelectAll();
if (wxWindow* focus = wxWindow::FindFocus()) //restore when closing panel!
if (!isComponentOf(focus, m_panelSearch))
focusAfterCloseSearch_ = focus->GetId();
m_textCtrlSearchTxt->SetFocus();
}
else
{
if (isComponentOf(wxWindow::FindFocus(), m_panelSearch))
if (wxWindow* oldFocusWin = wxWindow::FindWindowById(focusAfterCloseSearch_))
oldFocusWin->SetFocus();
focusAfterCloseSearch_ = wxID_ANY;
}
}
void MainDialog::startFindNext(bool searchAscending) //F3 or ENTER in m_textCtrlSearchTxt
{
const std::wstring& searchString = utfTo<std::wstring>(trimCpy(m_textCtrlSearchTxt->GetValue()));
if (searchString.empty())
showFindPanel(true /*show*/);
else
{
Grid* grid1 = m_gridMainL;
Grid* grid2 = m_gridMainR;
wxWindow* focus = wxWindow::FindFocus();
if ((isComponentOf(focus, m_panelSearch) ? focusAfterCloseSearch_ : focus->GetId()) == m_gridMainR->getMainWin().GetId())
std::swap(grid1, grid2); //select side to start search at grid cursor position
wxBeginBusyCursor(wxHOURGLASS_CURSOR);
const std::pair<const Grid*, ptrdiff_t> result = findGridMatch(*grid1, *grid2, utfTo<std::wstring>(searchString),
m_checkBoxMatchCase->GetValue(), searchAscending);
//parameter owned by GUI, *not* globalCfg structure! => we should better implement a getGlocalCfg()!
wxEndBusyCursor();
if (Grid* grid = const_cast<Grid*>(result.first)) //grid wasn't const when passing to findAndSelectNext(), so this is legal
{
assert(result.second >= 0);
filegrid::setScrollMaster(*grid);
grid->setGridCursor(result.second, GridEventPolicy::allow);
focusAfterCloseSearch_ = grid->getMainWin().GetId();
if (!isComponentOf(wxWindow::FindFocus(), m_panelSearch))
grid->getMainWin().SetFocus();
}
else
{
showFindPanel(true /*show*/);
showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setTitle(_("Find")).
setMainInstructions(replaceCpy(_("Cannot find %x"), L"%x", fmtPath(searchString))));
}
}
}
void MainDialog::onTopFolderPairAdd(wxCommandEvent& event)
{
insertAddFolderPair({LocalPairConfig()}, 0);
moveAddFolderPairUp(0);
}
void MainDialog::onTopFolderPairRemove(wxCommandEvent& event)
{
assert(!additionalFolderPairs_.empty());
if (!additionalFolderPairs_.empty())
{
moveAddFolderPairUp(0);
removeAddFolderPair(0);
}
}
void MainDialog::onLocalCompCfg(wxCommandEvent& event)
{
const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event
for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it)
if (eventObj == (*it)->m_bpButtonLocalCompCfg)
{
showConfigDialog(SyncConfigPanel::compare, (it - additionalFolderPairs_.begin()) + 1);
break;
}
}
void MainDialog::onLocalSyncCfg(wxCommandEvent& event)
{
const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event
for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it)
if (eventObj == (*it)->m_bpButtonLocalSyncCfg)
{
showConfigDialog(SyncConfigPanel::sync, (it - additionalFolderPairs_.begin()) + 1);
break;
}
}
void MainDialog::onLocalFilterCfg(wxCommandEvent& event)
{
const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event
for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it)
if (eventObj == (*it)->m_bpButtonLocalFilter)
{
showConfigDialog(SyncConfigPanel::filter, (it - additionalFolderPairs_.begin()) + 1);
break;
}
}
void MainDialog::onRemoveFolderPair(wxCommandEvent& event)
{
const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event
for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it)
if (eventObj == (*it)->m_bpButtonRemovePair)
{
removeAddFolderPair(it - additionalFolderPairs_.begin());
break;
}
}
void MainDialog::onShowFolderPairOptions(wxEvent& event)
{
const wxObject* const eventObj = event.GetEventObject(); //find folder pair originating the event
for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it)
if (eventObj == (*it)->m_bpButtonFolderPairOptions)
{
const ptrdiff_t pos = it - additionalFolderPairs_.begin();
ContextMenu menu;
menu.addItem(_("Add folder pair"), [this, pos] { insertAddFolderPair({LocalPairConfig()}, pos); }, loadImage("item_add_sicon"));
menu.addSeparator();
menu.addItem(_("Move up" ) + L"\tAlt+Page Up", [this, pos] { moveAddFolderPairUp(pos); }, loadImage("move_up_sicon"));
menu.addItem(_("Move down") + L"\tAlt+Page Down", [this, pos] { moveAddFolderPairUp(pos + 1); }, loadImage("move_down_sicon"),
pos + 1 < makeSigned(additionalFolderPairs_.size()));
menu.popup(*(*it)->m_bpButtonFolderPairOptions, {(*it)->m_bpButtonFolderPairOptions->GetSize().x, 0});
break;
}
}
void MainDialog::onTopFolderPairKeyEvent(wxKeyEvent& event)
{
const int keyCode = event.GetKeyCode();
if (event.AltDown())
switch (keyCode)
{
case WXK_PAGEDOWN: //Alt + Page Down
case WXK_NUMPAD_PAGEDOWN:
if (!additionalFolderPairs_.empty())
{
moveAddFolderPairUp(0);
additionalFolderPairs_[0]->m_folderPathLeft->SetFocus();
}
return;
}
event.Skip();
}
void MainDialog::onAddFolderPairKeyEvent(wxKeyEvent& event)
{
const int keyCode = event.GetKeyCode();
auto getAddFolderPairPos = [&]() -> ptrdiff_t //find folder pair originating the event
{
if (auto eventObj = dynamic_cast<const wxWindow*>(event.GetEventObject()))
for (auto it = additionalFolderPairs_.begin(); it != additionalFolderPairs_.end(); ++it)
if (isComponentOf(eventObj, *it))
return it - additionalFolderPairs_.begin();
return -1;
};
if (event.AltDown())
switch (keyCode)
{
case WXK_PAGEUP: //Alt + Page Up
case WXK_NUMPAD_PAGEUP:
if (const ptrdiff_t pos = getAddFolderPairPos();
pos >= 0)
{
moveAddFolderPairUp(pos);
(pos == 0 ? m_folderPathLeft : additionalFolderPairs_[pos - 1]->m_folderPathLeft)->SetFocus();
}
return;
case WXK_PAGEDOWN: //Alt + Page Down
case WXK_NUMPAD_PAGEDOWN:
if (const ptrdiff_t pos = getAddFolderPairPos();
0 <= pos && pos + 1 < makeSigned(additionalFolderPairs_.size()))
{
moveAddFolderPairUp(pos + 1);
additionalFolderPairs_[pos + 1]->m_folderPathLeft->SetFocus();
}
return;
}
event.Skip();
}
void MainDialog::updateGuiForFolderPair()
{
recalcMaxFolderPairsVisible();
//adapt delete top folder pair button
m_bpButtonRemovePair->Show(!additionalFolderPairs_.empty());
m_panelTopLeft->Layout();
//adapt local filter and sync cfg for first folder pair
const bool showLocalCfgFirstPair = !additionalFolderPairs_.empty() ||
firstFolderPair_->getCompConfig() ||
firstFolderPair_->getSyncConfig() ||
!isNullFilter(firstFolderPair_->getFilterConfig());
//harmonize with MainDialog::showConfigDialog()!
m_bpButtonLocalCompCfg->Show(showLocalCfgFirstPair);
m_bpButtonLocalSyncCfg->Show(showLocalCfgFirstPair);
m_bpButtonLocalFilter ->Show(showLocalCfgFirstPair);
setImage(*m_bpButtonSwapSides, loadImage(showLocalCfgFirstPair ? "swap_slim" : "swap"));
//update sub-panel sizes for calculations below!!!
m_panelTopCenter->GetSizer()->SetSizeHints(m_panelTopCenter); //~=Fit() + SetMinSize()
const int firstPairHeight = std::max(m_panelDirectoryPairs->ClientToWindowSize(m_panelTopLeft ->GetSize()).y, //include m_panelDirectoryPairs window borders!
m_panelDirectoryPairs->ClientToWindowSize(m_panelTopCenter->GetSize()).y); //
const int addPairHeight = !additionalFolderPairs_.empty() ? additionalFolderPairs_[0]->GetSize().y : 0;
const double addPairCountMax = std::max(globalCfg_.mainDlg.folderPairsVisibleMax - 1 + 0.5, 1.5);
const double addPairCountMin = std::min<double>(1.5, additionalFolderPairs_.size()); //add 0.5 to indicate additional folders
const double addPairCountOpt = std::min<double>(addPairCountMax, additionalFolderPairs_.size()); //
addPairCountLast_ = addPairCountOpt;
wxAuiPaneInfo& dirPane = auiMgr_.GetPane(m_panelDirectoryPairs);
//make sure user cannot fully shrink additional folder pairs
dirPane.MinSize(dipToWxsize(100), firstPairHeight + addPairCountMin * addPairHeight);
dirPane.BestSize(-1, firstPairHeight + addPairCountOpt * addPairHeight);
//########################################################################################################################
//wxAUI hack: call wxAuiPaneInfo::Fixed() to apply best size:
dirPane.Fixed();
auiMgr_.Update();
//now make resizable again
dirPane.Resizable();
auiMgr_.Update();
//alternative: dirPane.Hide() + .Show() seems to work equally well
//########################################################################################################################
//it seems there is no GetSizer()->SetSizeHints(this)/Fit() required due to wxAui "magic"
//=> *massive* perf improvement on OS X!
}
void MainDialog::recalcMaxFolderPairsVisible()
{
const int firstPairHeight = std::max(m_panelDirectoryPairs->ClientToWindowSize(m_panelTopLeft ->GetSize()).y, //include m_panelDirectoryPairs window borders!
m_panelDirectoryPairs->ClientToWindowSize(m_panelTopCenter->GetSize()).y); //
const int addPairHeight = !additionalFolderPairs_.empty() ? additionalFolderPairs_[0]->GetSize().y :
m_bpButtonAddPair->GetSize().y; //an educated guess
//assert(firstPairHeight > 0 && addPairHeight > 0); -> wxWindows::GetSize() returns 0 if main window is minimized during sync! Test with "When finished: Exit"
if (addPairCountLast_ && firstPairHeight > 0 && addPairHeight > 0)
{
const double addPairCountCurrent = (m_panelDirectoryPairs->GetSize().y - firstPairHeight) / (1.0 * addPairHeight); //include m_panelDirectoryPairs window borders!
if (std::abs(addPairCountCurrent - *addPairCountLast_) > 0.4) //=> presumely changed by user!
{
globalCfg_.mainDlg.folderPairsVisibleMax = std::round(addPairCountCurrent) + 1;
}
}
}
void MainDialog::insertAddFolderPair(const std::vector<LocalPairConfig>& newPairs, size_t pos)
{
assert(pos <= additionalFolderPairs_.size() && additionalFolderPairs_.size() == bSizerAddFolderPairs->GetItemCount());
pos = std::min(pos, additionalFolderPairs_.size());
for (size_t i = 0; i < newPairs.size(); ++i)
{
FolderPairPanel* newPair = nullptr;
if (!folderPairScrapyard_.empty()) //construct cheaply from "spare parts"
{
newPair = folderPairScrapyard_.back().release(); //transfer ownership
folderPairScrapyard_.pop_back();
newPair->Show();
}
else
{
newPair = new FolderPairPanel(m_scrolledWindowFolderPairs, *this,
globalCfg_.mainDlg.folderLastSelectedLeft,
globalCfg_.mainDlg.folderLastSelectedRight,
globalCfg_.sftpKeyFileLastSelected);
//setHistory dropdown history
newPair->m_folderPathLeft ->setHistory(folderHistoryLeft_ );
newPair->m_folderPathRight->setHistory(folderHistoryRight_);
const wxSize optionsIconSize = loadImage("item_add").GetSize();
setImage(*(newPair->m_bpButtonFolderPairOptions), resizeCanvas(mirrorIfRtl(loadImage("button_arrow_right")), optionsIconSize, wxALIGN_CENTER));
//set width of left folder panel
const int width = m_panelTopLeft->GetSize().GetWidth();
newPair->m_panelLeft->SetMinSize({width, -1});
//register events
newPair->m_bpButtonFolderPairOptions->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onShowFolderPairOptions(event); });
newPair->m_bpButtonFolderPairOptions->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) { onShowFolderPairOptions(event); });
newPair->m_bpButtonRemovePair ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onRemoveFolderPair (event); });
static_cast<FolderPairPanelGenerated*>(newPair)->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onAddFolderPairKeyEvent(event); });
newPair->m_bpButtonLocalCompCfg->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onLocalCompCfg (event); });
newPair->m_bpButtonLocalSyncCfg->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onLocalSyncCfg (event); });
newPair->m_bpButtonLocalFilter ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onLocalFilterCfg(event); });
//important: make sure panel has proper default height!
newPair->GetSizer()->SetSizeHints(newPair); //~=Fit() + SetMinSize()
}
bSizerAddFolderPairs->Insert(pos + i, newPair, 0, wxEXPAND);
additionalFolderPairs_.insert(additionalFolderPairs_.begin() + pos + i, newPair);
//wxComboBox screws up miserably if width/height is smaller than the magic number 4! Problem occurs when trying to set tooltip
//so we have to update window sizes before setting configuration:
newPair->setValues(newPairs[i]);
}
updateGuiForFolderPair();
clearGrid(); //+ GUI update
}
void MainDialog::moveAddFolderPairUp(size_t pos)
{
assert(pos < additionalFolderPairs_.size());
if (pos < additionalFolderPairs_.size())
{
const LocalPairConfig cfgTmp = additionalFolderPairs_[pos]->getValues();
if (pos == 0)
{
additionalFolderPairs_[pos]->setValues(firstFolderPair_->getValues());
firstFolderPair_->setValues(cfgTmp);
}
else
{
additionalFolderPairs_[pos]->setValues(additionalFolderPairs_[pos - 1]->getValues());
additionalFolderPairs_[pos - 1]->setValues(cfgTmp);
}
//move comparison results, too!
if (!folderCmp_.empty())
std::swap(folderCmp_[pos], folderCmp_[pos + 1]); //invariant: folderCmp is empty or matches number of all folder pairs
filegrid::setData(*m_gridMainC, folderCmp_);
treegrid::setData(*m_gridOverview, folderCmp_);
updateGui();
}
}
void MainDialog::removeAddFolderPair(size_t pos)
{
assert(pos < additionalFolderPairs_.size());
if (pos < additionalFolderPairs_.size())
{
FolderPairPanel* panel = additionalFolderPairs_[pos];
additionalFolderPairs_.erase(additionalFolderPairs_.begin() + pos);
bSizerAddFolderPairs->Detach(panel); //Remove() does not work on wxWindow*, so do it manually
//more (non-portable) wxWidgets bullshit: on OS X wxWindow::Destroy() screws up and calls "operator delete" directly rather than
//the deferred deletion it is expected to do (and which is implemented correctly on Windows and Linux)
//http://bb10.com/python-wxpython-devel/2012-09/msg00004.html
//=> since we're in a mouse button callback of a sub-component of "panel" we need to delay deletion ourselves:
panel->Hide();
folderPairScrapyard_.emplace_back(panel); //transfer ownership
updateGuiForFolderPair();
clearGrid(pos + 1); //+ GUI update
}
}
void MainDialog::setAddFolderPairs(const std::vector<LocalPairConfig>& newPairs)
{
//FolderPairPanel are too expensive to casually throw away and recreate!
for (FolderPairPanel* panel : additionalFolderPairs_)
{
panel->Hide();
folderPairScrapyard_.emplace_back(panel); //transfer ownership
}
additionalFolderPairs_.clear();
bSizerAddFolderPairs->Clear(false /*delete_windows*/); //release ownership
insertAddFolderPair(newPairs, 0);
}
//########################################################################################################
void MainDialog::onMenuOptions(wxCommandEvent& event)
{
const ColorTheme colorThemeOld = globalCfg_.appColorTheme;
showOptionsDlg(this, globalCfg_);
if (!equalAppearance(globalCfg_.appColorTheme, colorThemeOld))
{
if (!folderCmp_.empty()) //otherwise: why bother the user?
switch (showConfirmationDialog(this, DialogInfoType::warning, PopupDialogCfg().setTitle(_("Confirm")).
setMainInstructions(_("The application must restart to change the color theme.") + L"\n\n" +
_("Restart now?")), _("&Restart")))
{
case ConfirmationButton::accept:
break;
case ConfirmationButton::cancel:
return;
}
try
{
changeColorTheme(globalCfg_.appColorTheme); //throw FileError
//should work on macOS/Linux, but not on Windows (until wxWidgets fixes their s...)
//show new dialog, then delete old one
MainDialog::create(getConfig(), activeConfigFiles_, getGlobalCfgBeforeExit(), globalCfgFilePath_, false /*startComparison*/);
Destroy();
}
catch (FileError&) //changing color scheme failed => restart app
{
onSystemShutdownRunTasks(); //LastRun.ffs_gui + GlobalSettings.xml +...
try
{
const Zstring ffsLaunchPath = getProcessPath(); //throw FileError
try
{
//run async, but give consoleExecute() some "time to fail"
const auto& [exitCode, output] = consoleExecute(ffsLaunchPath, 100 /*timeoutMs*/); //throw SysError, SysErrorTimeOut
if (exitCode != 0)
throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo<std::wstring>(exitCode)), utfTo<std::wstring>(output)));
}
catch (SysErrorTimeOut&) {}
catch (const SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(ffsLaunchPath)), e.toString()); }
}
catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); }
//don't continue after having called onSystemShutdownRunTasks()
// => also avoid ~MainDialog() calling getGlobalCfgBeforeExit() a second time and saving cfg needlessly
terminateProcess(static_cast<int>(FfsExitCode::success));
}
}
}
void MainDialog::onMenuExportFileList(wxCommandEvent& event)
{
wxBusyCursor dummy;
//https://en.wikipedia.org/wiki/Comma-separated_values
const lconv* localInfo = ::localeconv(); //always bound according to doc
const bool haveCommaAsDecimalSep = std::string(localInfo->decimal_point) == ",";
const char CSV_SEP = haveCommaAsDecimalSep ? ';' : ',';
auto fmtValue = [&](const std::wstring& val) -> std::string
{
std::string&& tmp = utfTo<std::string>(val);
if (contains(tmp, CSV_SEP))
return '"' + tmp + '"';
else
return std::move(tmp);
};
//generate header
std::string header;
header += BYTE_ORDER_MARK_UTF8;
header += fmtValue(_("Folder Pairs")) + LINE_BREAK;
for (const BaseFolderPair& baseFolder : asRange(folderCmp_))
{
header += fmtValue(AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::left >())) + CSV_SEP;
header += fmtValue(AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::right>())) + LINE_BREAK;
}
header += LINE_BREAK;
auto provLeft = m_gridMainL->getDataProvider();
auto provCenter = m_gridMainC->getDataProvider();
auto provRight = m_gridMainR->getDataProvider();
auto colAttrLeft = m_gridMainL->getColumnConfig();
auto colAttrCenter = m_gridMainC->getColumnConfig();
auto colAttrRight = m_gridMainR->getColumnConfig();
std::erase_if(colAttrLeft, [](const Grid::ColAttributes& ca) { return !ca.visible; });
std::erase_if(colAttrCenter, [](const Grid::ColAttributes& ca) { return !ca.visible || static_cast<ColumnTypeCenter>(ca.type) == ColumnTypeCenter::checkbox; });
std::erase_if(colAttrRight, [](const Grid::ColAttributes& ca) { return !ca.visible; });
if (provLeft && provCenter && provRight)
{
for (const Grid::ColAttributes& ca : colAttrLeft)
{
header += fmtValue(provLeft->getColumnLabel(ca.type));
header += CSV_SEP;
}
for (const Grid::ColAttributes& ca : colAttrCenter)
{
header += fmtValue(provCenter->getColumnLabel(ca.type));
header += CSV_SEP;
}
if (!colAttrRight.empty())
{
std::for_each(colAttrRight.begin(), colAttrRight.end() - 1,
[&](const Grid::ColAttributes& ca)
{
header += fmtValue(provRight->getColumnLabel(ca.type));
header += CSV_SEP;
});
header += fmtValue(provRight->getColumnLabel(colAttrRight.back().type));
}
header += LINE_BREAK;
try
{
Zstring title = Zstr("FreeFileSync");
if (const std::vector<std::wstring>& jobNames = getJobNames();
!jobNames.empty())
{
title = utfTo<Zstring>(jobNames[0]);
std::for_each(jobNames.begin() + 1, jobNames.end(), [&](const std::wstring& jobName)
{ title += Zstr(" + ") + utfTo<Zstring>(jobName); });
}
const Zstring shortGuid = printNumber<Zstring>(Zstr("%04x"), static_cast<unsigned int>(getCrc16(generateGUID())));
const Zstring csvFilePath = appendPath(tempFileBuf_.getAndCreateFolderPath(), //throw FileError
title + Zstr('~') + shortGuid + Zstr(".csv"));
const Zstring tmpFilePath = getPathWithTempName(csvFilePath);
FileOutputBuffered tmpFile(tmpFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError, (ErrorTargetExisting)
auto writeString = [&](const std::string& str) { tmpFile.write(str.data(), str.size()); }; //throw FileError
//main grid: write rows one after the other instead of creating one big string: memory allocation might fail; think 1 million rows!
writeString(header); //throw FileError
const size_t rowCount = m_gridMainL->getRowCount();
for (size_t row = 0; row < rowCount; ++row)
{
for (const Grid::ColAttributes& ca : colAttrLeft)
writeString(fmtValue(provLeft->getValue(row, ca.type)) += CSV_SEP); //throw FileError
for (const Grid::ColAttributes& ca : colAttrCenter)
writeString(fmtValue(provCenter->getValue(row, ca.type)) += CSV_SEP); //throw FileError
for (const Grid::ColAttributes& ca : colAttrRight)
writeString(fmtValue(provRight->getValue(row, ca.type)) += CSV_SEP); //throw FileError
writeString(LINE_BREAK); //throw FileError
}
tmpFile.finalize(); //throw FileError
//take over ownership:
ZEN_ON_SCOPE_FAIL( try { removeFilePlain(tmpFilePath); }
catch (const FileError& e) { logExtraError(e.toString()); });
//operation finished: move temp file transactionally
moveAndRenameItem(tmpFilePath, csvFilePath, true /*replaceExisting*/); //throw FileError, (ErrorMoveUnsupported), (ErrorTargetExisting)
openWithDefaultApp(csvFilePath); //throw FileError
flashStatusInfo(_("File list exported"));
}
catch (const FileError& e)
{
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
}
}
}
void MainDialog::onMenuCheckVersion(wxCommandEvent& event)
{
checkForUpdateNow(*this, globalCfg_.lastOnlineVersion);
}
void MainDialog::onStartupUpdateCheck(wxIdleEvent& event)
{
//execute just once per startup!
[[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onStartupUpdateCheck, this);
assert(ubOk);
auto showNewVersionReminder = [this]
{
if (haveNewerVersionOnline(globalCfg_.lastOnlineVersion))
{
auto menu = new wxMenu();
wxMenuItem* newItem = new wxMenuItem(menu, wxID_ANY, _("&Show details"));
Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent&) { checkForUpdateNow(*this, globalCfg_.lastOnlineVersion); }, newItem->GetId());
//show changelog + handle Supporter Edition auto-updater (including expiration)
menu->Append(newItem); //pass ownership
const std::wstring& blackStar = utfTo<std::wstring>("");
m_menubar->Append(menu, blackStar + L' ' + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(globalCfg_.lastOnlineVersion)) + L' ' + blackStar);
}
};
if (automaticUpdateCheckDue(globalCfg_.lastUpdateCheck))
{
flashStatusInfo(_("Searching for software updates..."));
guiQueue_.processAsync([resultPrep = automaticUpdateCheckPrepare(*this) /*prepare on main thread*/]
{ return automaticUpdateCheckRunAsync(resultPrep.ref()); }, //run on worker thread: (long-running part of the check)
[this, showNewVersionReminder] (SharedRef<const UpdateCheckResult>&& resultAsync)
{
const time_t lastUpdateCheckOld = globalCfg_.lastUpdateCheck;
automaticUpdateCheckEval(*this, globalCfg_.lastUpdateCheck, globalCfg_.lastOnlineVersion,
resultAsync.ref()); //run on main thread:
showNewVersionReminder();
if (globalCfg_.lastUpdateCheck == lastUpdateCheckOld)
flashStatusInfo(_("Software update check failed!"));
});
}
else
showNewVersionReminder();
}
void MainDialog::onLayoutWindowAsync(wxIdleEvent& event)
{
//execute just once per startup!
[[maybe_unused]] bool ubOk = Unbind(wxEVT_IDLE, &MainDialog::onLayoutWindowAsync, this);
assert(ubOk);
//adjust folder pair distortion on startup
for (FolderPairPanel* panel : additionalFolderPairs_)
panel->Layout();
Layout(); //strangely this layout call works if called in next idle event only
m_panelTopButtons->Layout();
//auiMgr_.Update(); fix view filter distortion; 2021-02-01: apparently not needed anymore!
}
void MainDialog::onMenuAbout(wxCommandEvent& event)
{
showAboutDialog(this);
}
void MainDialog::switchProgramLanguage(wxLanguage langId)
{
try
{
//set language *before* creating MainDialog!
setLanguage(langId); //throw FileError
}
catch (const FileError& e)
{
showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
return;
}
//create new dialog with respect to new language
GlobalConfig newGlobalCfg = getGlobalCfgBeforeExit();
newGlobalCfg.programLanguage = langId;
//show new dialog, then delete old one
MainDialog::create(getConfig(), activeConfigFiles_, newGlobalCfg, globalCfgFilePath_, false /*startComparison*/);
//don't use Close():
//1. we don't want to show the prompt to save current config in onClose()
//2. after getGlobalCfgBeforeExit() the old main dialog is invalid so we want to force deletion
Destroy(); //alternative: Close(true /*force*/)
}
void MainDialog::setGridViewType(GridViewType vt)
{
//if (m_bpButtonViewType->isActive() == value) return; support polling -> what about initialization?
m_bpButtonViewType->setActive(vt == GridViewType::action);
m_bpButtonViewType->SetToolTip((vt == GridViewType::action ? _("Action") : _("Difference")) + L" (F11)");
//toggle display of sync preview in middle grid
filegrid::setViewType(*m_gridMainC, vt);
updateGui();
}