summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2022-09-07 18:49:36 +0000
committerB. Stack <bgstack15@gmail.com>2022-09-07 18:49:36 +0000
commit62bcefb8b809a32c6d26ab04ca686578bba5567a (patch)
treefbc1dea58a6b28f1af4a9e9b2bc8e3e1d23b2103
parentMerge branch 'b11.23' into 'master' (diff)
parentadd upstream 11.24 (diff)
downloadFreeFileSync-11.24.tar.gz
FreeFileSync-11.24.tar.bz2
FreeFileSync-11.24.zip
Merge branch 'b11.24' into 'master'11.24
add upstream 11.24 See merge request opensource-tracking/FreeFileSync!47
-rw-r--r--Changelog.txt17
-rw-r--r--FreeFileSync/Build/Resources/Icons.zipbin364721 -> 364904 bytes
-rw-r--r--FreeFileSync/Build/Resources/Languages.zipbin512251 -> 512728 bytes
-rw-r--r--FreeFileSync/Source/Makefile29
-rw-r--r--FreeFileSync/Source/RealTimeSync/Makefile13
-rw-r--r--FreeFileSync/Source/RealTimeSync/application.cpp29
-rw-r--r--FreeFileSync/Source/RealTimeSync/config.cpp28
-rw-r--r--FreeFileSync/Source/RealTimeSync/folder_selector2.cpp13
-rw-r--r--FreeFileSync/Source/RealTimeSync/main_dlg.cpp15
-rw-r--r--FreeFileSync/Source/RealTimeSync/main_dlg.h4
-rw-r--r--FreeFileSync/Source/afs/abstract.h6
-rw-r--r--FreeFileSync/Source/afs/ftp.cpp195
-rw-r--r--FreeFileSync/Source/afs/gdrive.cpp93
-rw-r--r--FreeFileSync/Source/afs/native.cpp4
-rw-r--r--FreeFileSync/Source/application.cpp75
-rw-r--r--FreeFileSync/Source/base/algorithm.cpp65
-rw-r--r--FreeFileSync/Source/base/algorithm.h7
-rw-r--r--FreeFileSync/Source/base/comparison.cpp69
-rw-r--r--FreeFileSync/Source/base/db_file.cpp32
-rw-r--r--FreeFileSync/Source/base/db_file.h6
-rw-r--r--FreeFileSync/Source/base/dir_lock.cpp8
-rw-r--r--FreeFileSync/Source/base/file_hierarchy.h15
-rw-r--r--FreeFileSync/Source/base/parallel_scan.cpp2
-rw-r--r--FreeFileSync/Source/base/path_filter.cpp58
-rw-r--r--FreeFileSync/Source/base/path_filter.h2
-rw-r--r--FreeFileSync/Source/base/structures.h2
-rw-r--r--FreeFileSync/Source/base/synchronization.cpp388
-rw-r--r--FreeFileSync/Source/base/versioning.cpp4
-rw-r--r--FreeFileSync/Source/base_tools.cpp14
-rw-r--r--FreeFileSync/Source/config.cpp201
-rw-r--r--FreeFileSync/Source/config.h14
-rw-r--r--FreeFileSync/Source/localization.cpp57
-rw-r--r--FreeFileSync/Source/localization.h1
-rw-r--r--FreeFileSync/Source/log_file.cpp2
-rw-r--r--FreeFileSync/Source/parse_lng.h24
-rw-r--r--FreeFileSync/Source/ui/batch_config.cpp21
-rw-r--r--FreeFileSync/Source/ui/cfg_grid.cpp3
-rw-r--r--FreeFileSync/Source/ui/file_grid.cpp2
-rw-r--r--FreeFileSync/Source/ui/folder_selector.cpp4
-rw-r--r--FreeFileSync/Source/ui/gui_generated.cpp124
-rw-r--r--FreeFileSync/Source/ui/gui_generated.h12
-rw-r--r--FreeFileSync/Source/ui/gui_status_handler.cpp5
-rw-r--r--FreeFileSync/Source/ui/log_panel.cpp98
-rw-r--r--FreeFileSync/Source/ui/main_dlg.cpp332
-rw-r--r--FreeFileSync/Source/ui/progress_indicator.cpp2
-rw-r--r--FreeFileSync/Source/ui/progress_indicator.h3
-rw-r--r--FreeFileSync/Source/ui/search_grid.cpp12
-rw-r--r--FreeFileSync/Source/ui/sync_cfg.cpp151
-rw-r--r--FreeFileSync/Source/ui/version_check.cpp24
-rw-r--r--FreeFileSync/Source/version/version.h2
-rw-r--r--wx+/dc.h2
-rw-r--r--wx+/graph.cpp14
-rw-r--r--wx+/grid.cpp28
-rw-r--r--wx+/grid.h1
-rw-r--r--wx+/image_resources.cpp16
-rw-r--r--wx+/no_flicker.h10
-rw-r--r--wx+/popup_dlg.cpp1
-rw-r--r--wx+/rtl.h24
-rw-r--r--xBRZ/src/xbrz.cpp6
-rw-r--r--zen/build_info.h1
-rw-r--r--zen/file_access.cpp17
-rw-r--r--zen/file_access.h14
-rw-r--r--zen/file_path.cpp7
-rw-r--r--zen/file_path.h2
-rw-r--r--zen/file_traverser.h4
-rw-r--r--zen/format_unit.cpp27
-rw-r--r--zen/process_exec.cpp3
-rw-r--r--zen/resolve_path.cpp12
-rw-r--r--zen/socket.h10
-rw-r--r--zen/stl_tools.h37
-rw-r--r--zen/string_base.h31
-rw-r--r--zen/string_tools.h26
-rw-r--r--zen/string_traits.h4
-rw-r--r--zen/sys_info.cpp12
-rw-r--r--zen/thread.h2
-rw-r--r--zen/time.h44
-rw-r--r--zen/utf.h160
-rw-r--r--zen/zstring.cpp222
-rw-r--r--zen/zstring.h26
79 files changed, 1715 insertions, 1335 deletions
diff --git a/Changelog.txt b/Changelog.txt
index 5a53352c..ee265eee 100644
--- a/Changelog.txt
+++ b/Changelog.txt
@@ -1,3 +1,20 @@
+FreeFileSync 11.25
+------------------
+
+
+FreeFileSync 11.24 [2022-08-28]
+-------------------------------
+Enhanced filter syntax to match files only (append ':')
+Fixed "Some files will be synchronized as part of multiple base folders": no more false-positives
+Detect full path filter items and convert to relative path
+Auto-detect FTP server character encoding (UTF8 or ANSI)
+Cancel grid selection via Escape key or second mouse button
+Apply conflict preview limit accross all folder pairs
+Require config type and file extension to match
+Fixed view filter panel vertical layout
+Strict validation of UTF encoding
+
+
FreeFileSync 11.23 [2022-07-23]
-------------------------------
Format local file times with no limits on time span
diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip
index c7a5e698..7de873b1 100644
--- a/FreeFileSync/Build/Resources/Icons.zip
+++ b/FreeFileSync/Build/Resources/Icons.zip
Binary files differ
diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip
index 8f6c9f21..ec222bbc 100644
--- a/FreeFileSync/Build/Resources/Languages.zip
+++ b/FreeFileSync/Build/Resources/Languages.zip
Binary files differ
diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile
index 69cf2536..eb6a6dbe 100644
--- a/FreeFileSync/Source/Makefile
+++ b/FreeFileSync/Source/Makefile
@@ -1,30 +1,31 @@
+CXX ?= g++
exeName = FreeFileSync_$(shell arch)
-cxxFlags = -std=c++2b -pipe -DWXINTL_NO_GETTEXT_MACRO -I../.. -I../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \
+CXXFLAGS += -std=c++2b -pipe -DWXINTL_NO_GETTEXT_MACRO -I../.. -I../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \
-Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \
-O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread
-linkFlags = -s -no-pie `wx-config --libs std, aui, richtext --debug=no` -pthread
+LDFLAGS += -s -no-pie `wx-config --libs std, aui, richtext --debug=no` -pthread
-cxxFlags += `pkg-config --cflags openssl`
-linkFlags += `pkg-config --libs openssl`
+CXXFLAGS += `pkg-config --cflags openssl`
+LDFLAGS += `pkg-config --libs openssl`
-cxxFlags += `pkg-config --cflags libcurl`
-linkFlags += `pkg-config --libs libcurl`
+CXXFLAGS += `pkg-config --cflags libcurl`
+LDFLAGS += `pkg-config --libs libcurl`
-cxxFlags += `pkg-config --cflags libssh2`
-linkFlags += `pkg-config --libs libssh2`
+CXXFLAGS += `pkg-config --cflags libssh2`
+LDFLAGS += `pkg-config --libs libssh2`
-cxxFlags += `pkg-config --cflags gtk+-2.0`
+CXXFLAGS += `pkg-config --cflags gtk+-2.0`
#treat as system headers so that warnings are hidden:
-cxxFlags += -isystem/usr/include/gtk-2.0
+CXXFLAGS += -isystem/usr/include/gtk-2.0
#support for SELinux (optional)
SELINUX_EXISTING=$(shell pkg-config --exists libselinux && echo YES)
ifeq ($(SELINUX_EXISTING),YES)
-cxxFlags += `pkg-config --cflags libselinux` -DHAVE_SELINUX
-linkFlags += `pkg-config --libs libselinux`
+CXXFLAGS += `pkg-config --cflags libselinux` -DHAVE_SELINUX
+LDFLAGS += `pkg-config --libs libselinux`
endif
cppFiles=
@@ -116,11 +117,11 @@ all: ../Build/Bin/$(exeName)
../Build/Bin/$(exeName): $(objFiles)
mkdir -p $(dir $@)
- g++ -o $@ $^ $(linkFlags)
+ $(CXX) -o $@ $^ $(LDFLAGS)
$(tmpPath)/ffs/src/%.o : %
mkdir -p $(dir $@)
- g++ $(cxxFlags) -c $< -o $@
+ $(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(tmpPath)
diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile
index 26d370c0..661b6b78 100644
--- a/FreeFileSync/Source/RealTimeSync/Makefile
+++ b/FreeFileSync/Source/RealTimeSync/Makefile
@@ -1,15 +1,16 @@
+CXX ?= g++
exeName = RealTimeSync_$(shell arch)
-cxxFlags = -std=c++2b -pipe -DWXINTL_NO_GETTEXT_MACRO -I../../.. -I../../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \
+CXXFLAGS += -std=c++2b -pipe -DWXINTL_NO_GETTEXT_MACRO -I../../.. -I../../../zenXml -include "zen/i18n.h" -include "zen/warn_static.h" \
-Wall -Wfatal-errors -Wmissing-include-dirs -Wswitch-enum -Wcast-align -Wnon-virtual-dtor -Wno-unused-function -Wshadow -Wno-maybe-uninitialized \
-O3 -DNDEBUG `wx-config --cxxflags --debug=no` -pthread
-linkFlags = -s -no-pie `wx-config --libs std, aui, richtext --debug=no` -pthread
+LDFLAGS += -s -no-pie `wx-config --libs std, aui, richtext --debug=no` -pthread
#Gtk - support "no button border"
-cxxFlags += `pkg-config --cflags gtk+-2.0`
+CXXFLAGS += `pkg-config --cflags gtk+-2.0`
#treat as system headers so that warnings are hidden:
-cxxFlags += -isystem/usr/include/gtk-2.0
+CXXFLAGS += -isystem/usr/include/gtk-2.0
cppFiles=
cppFiles+=application.cpp
@@ -55,11 +56,11 @@ all: ../../Build/Bin/$(exeName)
../../Build/Bin/$(exeName): $(objFiles)
mkdir -p $(dir $@)
- g++ -o $@ $^ $(linkFlags)
+ $(CXX) -o $@ $^ $(LDFLAGS)
$(tmpPath)/ffs/src/rts/%.o : %
mkdir -p $(dir $@)
- g++ $(cxxFlags) -c $< -o $@
+ $(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(tmpPath)
diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp
index d7924d32..8928ab5d 100644
--- a/FreeFileSync/Source/RealTimeSync/application.cpp
+++ b/FreeFileSync/Source/RealTimeSync/application.cpp
@@ -93,7 +93,7 @@ bool Application::OnInit()
ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error));
::gtk_css_provider_load_from_path(provider, //GtkCssProvider* css_provider,
- appendPath(fff::getResourceDirPath(), fileName).c_str(), //const gchar* path,
+ appendPath(fff::getResourceDirPath(), fileName).c_str(), //const gchar* path,
&error); //GError** error
if (error)
throw SysError(formatGlibError("gtk_css_provider_load_from_path", error));
@@ -173,28 +173,21 @@ void Application::onEnterEventLoop(wxEvent& event)
std::vector<Zstring> commandArgs;
for (int i = 1; i < argc; ++i)
{
- Zstring filePath = getResolvedFilePath(utfTo<Zstring>(argv[i]));
-
- if (!fileAvailable(filePath)) //be a little tolerant
- {
- if (fileAvailable(filePath + Zstr(".ffs_real")))
- filePath += Zstr(".ffs_real");
- else if (fileAvailable(filePath + Zstr(".ffs_batch")))
- filePath += Zstr(".ffs_batch");
- else
- {
- showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(replaceCpy(_("Cannot find file %x."), L"%x", fmtPath(filePath))));
- return;
- }
- }
+ const Zstring& filePath = getResolvedFilePath(utfTo<Zstring>(argv[i]));
+#if 0
+ if (!fileAvailable(filePath)) //...be a little tolerant
+ for (const Zchar* ext : {Zstr(".ffs_real"), Zstr(".ffs_batch")})
+ if (fileAvailable(filePath + ext))
+ filePath += ext;
+#endif
commandArgs.push_back(filePath);
}
- Zstring cfgFilename;
+ Zstring cfgFilePath;
if (!commandArgs.empty())
- cfgFilename = commandArgs[0];
+ cfgFilePath = commandArgs[0];
- MainDialog::create(cfgFilename);
+ MainDialog::create(cfgFilePath);
}
diff --git a/FreeFileSync/Source/RealTimeSync/config.cpp b/FreeFileSync/Source/RealTimeSync/config.cpp
index 53b76fac..ff5cf29f 100644
--- a/FreeFileSync/Source/RealTimeSync/config.cpp
+++ b/FreeFileSync/Source/RealTimeSync/config.cpp
@@ -37,29 +37,15 @@ bool readText(const std::string& input, wxLanguage& value)
namespace
{
-enum class RtsXmlType
-{
- real,
- batch,
- global,
- other
-};
-RtsXmlType getXmlTypeNoThrow(const XmlDoc& doc) //throw()
+std::string getConfigType(const XmlDoc& doc)
{
if (doc.root().getName() == "FreeFileSync")
{
std::string type;
if (doc.root().getAttribute("XmlType", type))
- {
- if (type == "REAL")
- return RtsXmlType::real;
- else if (type == "BATCH")
- return RtsXmlType::batch;
- else if (type == "GLOBAL")
- return RtsXmlType::global;
- }
+ return type;
}
- return RtsXmlType::other;
+ return {};
}
@@ -90,7 +76,7 @@ void rts::readConfig(const Zstring& filePath, XmlRealConfig& cfg, std::wstring&
{
XmlDoc doc = loadXml(filePath); //throw FileError
- if (getXmlTypeNoThrow(doc) != RtsXmlType::real) //noexcept
+ if (getConfigType(doc) != "REAL")
throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
int formatVer = 0;
@@ -130,10 +116,8 @@ void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& cfg, std
XmlDoc doc = loadXml(filePath); //throw FileError
//quick exit if file is not an FFS XML
- const RtsXmlType xmlType = ::getXmlTypeNoThrow(doc);
-
//convert batch config to RealTimeSync config
- if (xmlType == RtsXmlType::batch)
+ if (getConfigType(doc) == "BATCH")
{
XmlIn in(doc);
@@ -180,7 +164,7 @@ wxLanguage rts::getProgramLanguage() //throw FileError
throw;
}
- if (getXmlTypeNoThrow(doc) != RtsXmlType::global) //noexcept
+ if (getConfigType(doc) != "GLOBAL")
throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
XmlIn in(doc);
diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp
index 01767ae3..671ce606 100644
--- a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp
+++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp
@@ -133,9 +133,16 @@ void FolderSelector2::onSelectDir(wxCommandEvent& event)
//IFileDialog requirements for default path: 1. accepts native paths only!!! 2. path must exist!
Zstring defaultFolderPath;
{
- auto folderExistsTimed = [waitEndTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const Zstring& folderPath)
+ auto folderAccessible = [waitEndTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const Zstring& folderPath)
{
- auto ft = runAsync([folderPath] { return dirAvailable(folderPath); });
+ auto ft = runAsync([folderPath]
+ {
+ try
+ {
+ return getItemType(folderPath) != ItemType::file; //throw FileError
+ }
+ catch (FileError&) { return false; }
+ });
return ft.wait_until(waitEndTime) == std::future_status::ready && ft.get(); //potentially slow network access: wait 200ms at most
};
@@ -145,7 +152,7 @@ void FolderSelector2::onSelectDir(wxCommandEvent& event)
if (const Zstring folderPath = getResolvedFilePath(folderPathPhrase);
!folderPath.empty())
- if (folderExistsTimed(folderPath))
+ if (folderAccessible(folderPath))
defaultFolderPath = folderPath;
};
diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp
index f6b9b337..d240dd0f 100644
--- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp
+++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp
@@ -62,13 +62,13 @@ private:
};
-void MainDialog::create(const Zstring& cfgFile)
+void MainDialog::create(const Zstring& cfgFilePath)
{
- /*MainDialog* frame = */ new MainDialog(cfgFile);
+ /*MainDialog* frame = */ new MainDialog(cfgFilePath);
}
-MainDialog::MainDialog(const Zstring& cfgFileName) :
+MainDialog::MainDialog(const Zstring& cfgFilePath) :
MainDlgGenerated(nullptr),
lastRunConfigPath_(appendPath(fff::getConfigDirPath(), Zstr("LastRun.ffs_real")))
{
@@ -102,7 +102,7 @@ MainDialog::MainDialog(const Zstring& cfgFileName) :
//--------------------------- load config values ------------------------------------
XmlRealConfig newConfig;
- Zstring currentConfigFile = cfgFileName;
+ Zstring currentConfigFile = cfgFilePath;
if (currentConfigFile.empty())
try
{
@@ -128,7 +128,7 @@ MainDialog::MainDialog(const Zstring& cfgFileName) :
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
}
- const bool startWatchingImmediately = loadCfgSuccess && !cfgFileName.empty();
+ const bool startWatchingImmediately = loadCfgSuccess && !cfgFilePath.empty();
setConfiguration(newConfig);
setLastUsedConfig(currentConfigFile);
@@ -253,7 +253,10 @@ void MainDialog::onConfigSave(wxCommandEvent& event)
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (fileSelector.ShowModal() != wxID_OK)
return;
- const Zstring targetFilePath = utfTo<Zstring>(fileSelector.GetPath());
+
+ Zstring targetFilePath = utfTo<Zstring>(fileSelector.GetPath());
+ if (!endsWithAsciiNoCase(targetFilePath, Zstr(".ffs_real"))) //no weird shit!
+ targetFilePath += Zstr(".ffs_real"); //https://freefilesync.org/forum/viewtopic.php?t=9451#p34724
const XmlRealConfig currentCfg = getConfiguration();
try
diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.h b/FreeFileSync/Source/RealTimeSync/main_dlg.h
index f83c5440..76b1111f 100644
--- a/FreeFileSync/Source/RealTimeSync/main_dlg.h
+++ b/FreeFileSync/Source/RealTimeSync/main_dlg.h
@@ -26,10 +26,10 @@ class DirectoryPanel;
class MainDialog: public MainDlgGenerated
{
public:
- static void create(const Zstring& cfgFile);
+ static void create(const Zstring& cfgFilePath);
private:
- MainDialog(const Zstring& cfgFileName);
+ MainDialog(const Zstring& cfgFilePath);
~MainDialog();
void onBeforeSystemShutdown(); //last chance to do something useful before killing the application!
diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h
index 716e3bef..0aae8bc0 100644
--- a/FreeFileSync/Source/afs/abstract.h
+++ b/FreeFileSync/Source/afs/abstract.h
@@ -132,7 +132,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t
struct StreamAttributes
{
- time_t modTime; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime; //number of seconds since Jan. 1st 1970 GMT
uint64_t fileSize;
FingerPrint filePrint; //optional
};
@@ -197,7 +197,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t
{
Zstring itemName;
uint64_t fileSize; //unit: bytes!
- time_t modTime; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime; //number of seconds since Jan. 1st 1970 GMT
FingerPrint filePrint; //optional; persistent + unique (relative to device) or 0!
bool isFollowedSymlink;
};
@@ -263,7 +263,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t
struct FileCopyResult
{
uint64_t fileSize = 0;
- time_t modTime = 0; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT
FingerPrint sourceFilePrint = 0; //optional
FingerPrint targetFilePrint = 0; //
std::optional<zen::FileError> errorModTime; //failure to set modification time
diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp
index 06add192..cd66c047 100644
--- a/FreeFileSync/Source/afs/ftp.cpp
+++ b/FreeFileSync/Source/afs/ftp.cpp
@@ -35,10 +35,12 @@ const int FTP_STREAM_BUFFER_SIZE = 512 * 1024; //unit: [byte]
const Zchar ftpPrefix[] = Zstr("ftp:");
+
enum class ServerEncoding
{
+ unknown,
utf8,
- ansi
+ ansi,
};
@@ -123,34 +125,6 @@ std::string utfToAnsiEncoding(const Zstring& str) //throw SysError
}
-Zstring serverToUtfEncoding(const std::string& str, ServerEncoding enc) //throw SysError
-{
- switch (enc)
- {
- case ServerEncoding::utf8:
- return utfTo<Zstring>(str);
- case ServerEncoding::ansi:
- return ansiToUtfEncoding(str); //throw SysError
- }
- assert(false);
- return {};
-}
-
-
-std::string utfToServerEncoding(const Zstring& str, ServerEncoding enc) //throw SysError
-{
- switch (enc)
- {
- case ServerEncoding::utf8:
- return utfTo<std::string>(str);
- case ServerEncoding::ansi:
- return utfToAnsiEncoding(str); //throw SysError
- }
- assert(false);
- return {};
-}
-
-
std::wstring getCurlDisplayPath(const FtpSessionId& sessionId, const AfsPath& afsPath)
{
Zstring displayPath = Zstring(ftpPrefix) + Zstr("//");
@@ -302,7 +276,7 @@ public:
const std::vector<CurlOption>& extraOptions, bool requiresUtf8) //throw SysError
{
if (requiresUtf8) //avoid endless recursion
- ensureUtf8(); //throw SysError
+ requestUtf8(); //throw SysError
if (!easyHandle_)
{
@@ -605,8 +579,7 @@ public:
else
{
const std::string homePathRaw = replaceCpy(std::string{itBegin, it}, "\"\"", '"');
- const ServerEncoding enc = getServerEncoding(); //throw SysError
- const Zstring homePathUtf = serverToUtfEncoding(homePathRaw, enc); //throw SysError
+ const Zstring homePathUtf = serverToUtfEncoding(homePathRaw); //throw SysError
return sanitizeDeviceRelativePath(homePathUtf);
}
}
@@ -627,13 +600,13 @@ public:
//make sure our binary-enabled session is still there (== libcurl behaves as we expect)
std::optional<curl_socket_t> currentSocket = getActiveSocket(); //throw SysError
- if (!currentSocket)
- throw SysError(L"Curl failed to cache FTP session."); //why is libcurl not caching the session???
-
- binaryEnabledSocket_ = *currentSocket; //remember what we did
+ if (currentSocket)
+ binaryEnabledSocket_ = *currentSocket; //remember what we did
//libcurl already buffers "conn->proto.ftpc.transfertype" but selfishly keeps it for itself!
//=> pray libcurl doesn't internally set "TYPE A"!
//=> this seems to be the only place where it does: https://github.com/curl/curl/issues/4342
+ else
+ throw SysError(L"Curl failed to cache FTP session."); //why is libcurl not caching the session???
}
//------------------------------------------------------------------------------------------------------------
@@ -642,8 +615,6 @@ public:
bool supportsClnt() { return getFeatureSupport(&Features::clnt); } //
bool supportsUtf8() { return getFeatureSupport(&Features::utf8); } //
- ServerEncoding getServerEncoding() { return supportsUtf8() ? ServerEncoding::utf8 : ServerEncoding::ansi; } //throw SysError
-
bool isHealthy() const
{
return std::chrono::steady_clock::now() - lastSuccessfulUseTime_ <= FTP_SESSION_MAX_IDLE_TIME;
@@ -653,12 +624,68 @@ public:
{
const Zstring serverPath = getServerRelPath(afsPath);
- if (afsPath.value.empty()) //endless recursion caveat!! getServerEncoding() transitively depends on getServerPathInternal()
+ if (afsPath.value.empty()) //endless recursion caveat!! utfToServerEncoding() transitively depends on getServerPathInternal()
return utfTo<std::string>(serverPath);
- const ServerEncoding encoding = getServerEncoding(); //throw SysError
+ return utfToServerEncoding(serverPath); //throw SysError
+ }
+
+ Zstring serverToUtfEncoding(const std::string& str) //throw SysError
+ {
+ if (isAsciiString(str)) //fast path
+ return {str.begin(), str.end()};
+
+ switch (encoding_) //throw SysError
+ {
+ case ServerEncoding::unknown:
+ /* "UTF-8 encodings [2] contain enough internal structure that it is always, in practice, possible to determine whether a UTF-8 or raw encoding has been used"
+ - https://www.rfc-editor.org/rfc/rfc3659#section-2.2
+ "encoding rules make it very unlikely that a character sequence from a different character set will be mistaken for a UTF-8 encoded character sequence."
+ - https://www.rfc-editor.org/rfc/rfc2640#section-2.2
+
+ => auto-detect encoding even if FEAT does not advertize UTF8: https://freefilesync.org/forum/viewtopic.php?t=9564 */
+ encoding_ = supportsUtf8() || isValidUtf(str) ? ServerEncoding::utf8 : ServerEncoding::ansi;
+ return serverToUtfEncoding(str); //throw SysError
+
+ case ServerEncoding::utf8:
+ if (!isValidUtf(str))
+ throw SysError(_("Invalid character encoding:") + L" [UTF-8] " + utfTo<std::wstring>(str));
- return utfToServerEncoding(serverPath, encoding); //throw SysError
+ return utfTo<Zstring>(str);
+
+ case ServerEncoding::ansi:
+ return ansiToUtfEncoding(str); //throw SysError
+ }
+ assert(false);
+ return {};
+ }
+
+ std::string utfToServerEncoding(const Zstring& str) //throw SysError
+ {
+ if (isAsciiString(str)) //fast path
+ return {str.begin(), str.end()};
+ switch (encoding_) //throw SysError
+ {
+ case ServerEncoding::unknown:
+ if (!supportsUtf8())
+ throw SysError(_("Failed to auto-detect character encoding:") + L' ' + utfTo<std::wstring>(str)); //might be ANSI or UTF8 with non-compliant server...
+
+ encoding_ = ServerEncoding::utf8;
+ return utfToServerEncoding(str); //throw SysError
+
+ case ServerEncoding::utf8:
+ //validate! we consider REPLACEMENT_CHAR as indication for server using ANSI encoding in serverToUtfEncoding()
+ if (!isValidUtf(str))
+ throw SysError(_("Invalid character encoding:") + (sizeof(str[0]) == 1 ? L" [UTF-8] " : L" [UTF-16] ") + utfTo<std::wstring>(str));
+ static_assert(sizeof(str[0]) == 1 || sizeof(str[0]) == 2);
+
+ return utfTo<std::string>(str);
+
+ case ServerEncoding::ansi:
+ return utfToAnsiEncoding(str); //throw SysError
+ }
+ assert(false);
+ return {};
}
private:
@@ -693,36 +720,37 @@ private:
return path;
}
- void ensureUtf8() //throw SysError
+ void requestUtf8() //throw SysError
{
+ //Some RFC-2640-non-compliant servers require UTF8 to be explicitly enabled: https://wiki.filezilla-project.org/Character_Encoding#Conflicting_specification
+ //e.g. this one (Microsoft FTP Service): https://freefilesync.org/forum/viewtopic.php?t=4303
+
+ //check supportsUtf8()? no, let's *always* request UTF8, even if server does not set UTF8 in FEAT response! e.g. like https://freefilesync.org/forum/viewtopic.php?t=9564
+ //=> should not hurt + we rely on auto-encoding detection anyway; see serverToUtfEncoding()
+
//"OPTS UTF8 ON" needs to be activated each time libcurl internally creates a new session
//hopyfully libcurl will offer a better solution: https://github.com/curl/curl/issues/1457
- //Some RFC-2640-non-compliant servers require UTF8 to be explicitly enabled: https://wiki.filezilla-project.org/Character_Encoding#Conflicting_specification
- //e.g. this one (Microsoft FTP Service): https://freefilesync.org/forum/viewtopic.php?t=4303
- if (supportsUtf8()) //throw SysError
- {
- //[!] supportsUtf8() is buffered! => FTP session might not yet exist (or was closed by libcurl after a failure)
- if (std::optional<curl_socket_t> currentSocket = getActiveSocket()) //throw SysError
- if (*currentSocket == utf8EnabledSocket_) //caveat: a non-UTF8-enabled session might already exist, e.g. from a previous call to supportsMlsd()
- return;
+ if (std::optional<curl_socket_t> currentSocket = getActiveSocket()) //throw SysError
+ if (*currentSocket == utf8RequestedSocket_) //caveat: a non-UTF8-enabled session might already exist, e.g. from a previous call to supportsMlsd()
+ return;
- //some servers even require "CLNT" before accepting "OPTS UTF8 ON": https://social.msdn.microsoft.com/Forums/en-US/d602574f-8a69-4d69-b337-52b6081902cf/problem-with-ftpwebrequestopts-utf8-on-501-please-clnt-first
- if (supportsClnt()) //throw SysError
- runSingleFtpCommand("CLNT FreeFileSync", false /*requiresUtf8*/); //throw SysError
+ //some servers even require "CLNT" before accepting "OPTS UTF8 ON": https://social.msdn.microsoft.com/Forums/en-US/d602574f-8a69-4d69-b337-52b6081902cf/problem-with-ftpwebrequestopts-utf8-on-501-please-clnt-first
+ if (supportsClnt()) //throw SysError
+ runSingleFtpCommand("CLNT FreeFileSync", false /*requiresUtf8*/); //throw SysError
- //"prefix the command with an asterisk to make libcurl continue even if the command fails"
- //-> ignore if server does not know this legacy command (but report all *other* issues; else getActiveSocket() below won't return value and hide real error!)
- //"If an RFC 2640 compliant client sends OPTS UTF-8 ON, it has to use UTF-8 regardless whether OPTS UTF-8 ON succeeds or not. "
- runSingleFtpCommand("*OPTS UTF8 ON", false /*requiresUtf8*/); //throw SysError
+ //"prefix the command with an asterisk to make libcurl continue even if the command fails"
+ //-> ignore if server does not know this legacy command (but report all *other* issues; else getActiveSocket() below won't return value and hide real error!)
+ //"If an RFC 2640 compliant client sends OPTS UTF-8 ON, it has to use UTF-8 regardless whether OPTS UTF-8 ON succeeds or not. "
+ runSingleFtpCommand("*OPTS UTF8 ON", false /*requiresUtf8*/); //throw SysError
- //make sure our unicode-enabled session is still there (== libcurl behaves as we expect)
- std::optional<curl_socket_t> currentSocket = getActiveSocket(); //throw SysError
- if (!currentSocket)
- throw SysError(L"Curl failed to cache FTP session."); //why is libcurl not caching the session???
+ //make sure our Unicode-enabled session is still there (== libcurl behaves as we expect)
+ std::optional<curl_socket_t> currentSocket = getActiveSocket(); //throw SysError
+ if (currentSocket)
+ utf8RequestedSocket_ = *currentSocket; //remember what we did
+ else
+ throw SysError(L"Curl failed to cache FTP session."); //why is libcurl not caching the session???
- utf8EnabledSocket_ = *currentSocket; //remember what we did
- }
}
std::optional<curl_socket_t> getActiveSocket() //throw SysError
@@ -765,7 +793,7 @@ private:
{
//*: ignore error if server does not support/allow FEAT
featureCache_ = parseFeatResponse(runSingleFtpCommand("*FEAT", false /*requiresUtf8*/)); //throw SysError
- //used by ensureUtf8()! => requiresUtf8 = false!!!
+ //used by requestUtf8()! => requiresUtf8 = false!!!
sf->access([&](FeatureList& feat) { feat[sessionId_.server] = featureCache_; });
}
@@ -822,9 +850,11 @@ private:
const FtpSessionId sessionId_;
CURL* easyHandle_ = nullptr;
- curl_socket_t utf8EnabledSocket_ = 0;
+ curl_socket_t utf8RequestedSocket_ = 0;
curl_socket_t binaryEnabledSocket_ = 0;
+ ServerEncoding encoding_ = ServerEncoding::unknown;
+
std::optional<Features> featureCache_;
std::optional<AfsPath> homePathCached_;
@@ -1098,11 +1128,10 @@ public:
session.perform(afsDirPath, true /*isDir*/, pathMethod, options, true /*requiresUtf8*/); //throw SysError
- const ServerEncoding encoding = session.getServerEncoding(); //throw SysError
if (session.supportsMlsd()) //throw SysError
- output = parseMlsd(rawListing, encoding); //throw SysError
+ output = parseMlsd(rawListing, session); //throw SysError
else
- output = parseUnknown(rawListing, encoding); //throw SysError
+ output = parseUnknown(rawListing, session); //throw SysError
});
}
catch (const SysError& e)
@@ -1116,12 +1145,12 @@ private:
FtpDirectoryReader (const FtpDirectoryReader&) = delete;
FtpDirectoryReader& operator=(const FtpDirectoryReader&) = delete;
- static std::vector<FtpItem> parseMlsd(const std::string& buf, ServerEncoding enc) //throw SysError
+ static std::vector<FtpItem> parseMlsd(const std::string& buf, FtpSession& session) //throw SysError
{
std::vector<FtpItem> output;
for (const std::string& line : splitFtpResponse(buf))
{
- const FtpItem item = parseMlstLine(line, enc); //throw SysError
+ const FtpItem item = parseMlstLine(line, session); //throw SysError
if (item.itemName != Zstr(".") &&
item.itemName != Zstr(".."))
output.push_back(item);
@@ -1129,7 +1158,7 @@ private:
return output;
}
- static FtpItem parseMlstLine(const std::string& rawLine, ServerEncoding enc) //throw SysError
+ static FtpItem parseMlstLine(const std::string& rawLine, FtpSession& session) //throw SysError
{
/* https://tools.ietf.org/html/rfc3659
type=cdir;sizd=4096;modify=20170116230740;UNIX.mode=0755;UNIX.uid=874;UNIX.gid=869;unique=902g36e1c55; .
@@ -1148,7 +1177,7 @@ private:
throw SysError(L"Item name not available.");
const std::string facts(itBegin, itBlank);
- item.itemName = serverToUtfEncoding(std::string(itBlank + 1, rawLine.end()), enc); //throw SysError
+ item.itemName = session.serverToUtfEncoding(std::string(itBlank + 1, rawLine.end())); //throw SysError
std::string typeFact;
std::optional<uint64_t> fileSize;
@@ -1220,15 +1249,15 @@ private:
}
}
- static std::vector<FtpItem> parseUnknown(const std::string& buf, ServerEncoding enc) //throw SysError
+ static std::vector<FtpItem> parseUnknown(const std::string& buf, FtpSession& session) //throw SysError
{
if (!buf.empty() && isDigit(buf[0])) //lame test to distinguish Unix/Dos formats as internally used by libcurl
- return parseWindows(buf, enc); //throw SysError
- return parseUnix(buf, enc); //
+ return parseWindows(buf, session); //throw SysError
+ return parseUnix(buf, session); //
}
//"ls -l"
- static std::vector<FtpItem> parseUnix(const std::string& buf, ServerEncoding enc) //throw SysError
+ static std::vector<FtpItem> parseUnix(const std::string& buf, FtpSession& session) //throw SysError
{
const std::vector<std::string> lines = splitFtpResponse(buf);
auto it = lines.begin();
@@ -1254,14 +1283,14 @@ private:
{
try
{
- parseUnixLine(*it, utcTimeNow, utcCurrentYear, true /*haveGroup*/, enc); //throw SysError
+ parseUnixLine(*it, utcTimeNow, utcCurrentYear, true /*haveGroup*/, session); //throw SysError
return true;
}
catch (SysError&)
{
try
{
- parseUnixLine(*it, utcTimeNow, utcCurrentYear, false /*haveGroup*/, enc); //throw SysError
+ parseUnixLine(*it, utcTimeNow, utcCurrentYear, false /*haveGroup*/, session); //throw SysError
return false;
}
catch (SysError&) {}
@@ -1269,7 +1298,7 @@ private:
}
}();
- const FtpItem item = parseUnixLine(*it, utcTimeNow, utcCurrentYear, *unixListingHaveGroup_, enc); //throw SysError
+ const FtpItem item = parseUnixLine(*it, utcTimeNow, utcCurrentYear, *unixListingHaveGroup_, session); //throw SysError
if (item.itemName != Zstr(".") &&
item.itemName != Zstr(".."))
output.push_back(item);
@@ -1278,7 +1307,7 @@ private:
return output;
}
- static FtpItem parseUnixLine(const std::string& rawLine, time_t utcTimeNow, int utcCurrentYear, bool haveGroup, ServerEncoding enc) //throw SysError
+ static FtpItem parseUnixLine(const std::string& rawLine, time_t utcTimeNow, int utcCurrentYear, bool haveGroup, FtpSession& session) //throw SysError
{
/* total 4953 <- optional first line
drwxr-xr-x 1 root root 4096 Jan 10 11:58 version
@@ -1413,7 +1442,7 @@ private:
else
item.fileSize = fileSize;
- item.itemName = serverToUtfEncoding(itemName, enc); //throw SysError
+ item.itemName = session.serverToUtfEncoding(itemName); //throw SysError
item.modTime = modTime;
return item;
@@ -1426,7 +1455,7 @@ private:
//"dir"
- static std::vector<FtpItem> parseWindows(const std::string& buf, ServerEncoding enc) //throw SysError
+ static std::vector<FtpItem> parseWindows(const std::string& buf, FtpSession& session) //throw SysError
{
/* Test server: test.rebex.net username:demo pw:password useTls = true
@@ -1541,7 +1570,7 @@ private:
FtpItem item;
if (isDir)
item.type = AFS::ItemType::folder;
- item.itemName = serverToUtfEncoding(itemName, enc); //throw SysError
+ item.itemName = session.serverToUtfEncoding(itemName); //throw SysError
item.fileSize = fileSize;
item.modTime = modTime;
diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp
index ddb12f6d..a1b628a7 100644
--- a/FreeFileSync/Source/afs/gdrive.cpp
+++ b/FreeFileSync/Source/afs/gdrive.cpp
@@ -445,11 +445,14 @@ GdriveAccessInfo gdriveExchangeAuthCode(const GdriveAuthCode& authCode, int time
GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const std::function<void()>& updateGui /*throw X*/, int timeoutSec) //throw SysError, X
{
//spin up a web server to wait for the HTTP GET after Google authentication
- ::addrinfo hints = {};
- hints.ai_family = AF_INET; //make sure our server is reached by IPv4 127.0.0.1, not IPv6 [::1]
- hints.ai_socktype = SOCK_STREAM; //we *do* care about this one!
- hints.ai_flags = AI_PASSIVE; //the returned socket addresses will be suitable for bind(2)ing a socket that will accept(2) connections.
- hints.ai_flags |= AI_ADDRCONFIG; //no such issue on Linux: https://bugs.chromium.org/p/chromium/issues/detail?id=5234
+ const ::addrinfo hints
+ {
+ .ai_flags = AI_PASSIVE //the returned socket addresses will be suitable for bind(2)ing a socket that will accept(2) connections.
+ | AI_ADDRCONFIG //no such issue on Linux: https://bugs.chromium.org/p/chromium/issues/detail?id=5234
+ ,
+ .ai_family = AF_INET, //make sure our server is reached by IPv4 127.0.0.1, not IPv6 [::1]
+ .ai_socktype = SOCK_STREAM, //we *do* care about this one!
+ };
::addrinfo* servinfo = nullptr;
ZEN_ON_SCOPE_EXIT(if (servinfo) ::freeaddrinfo(servinfo));
@@ -1860,12 +1863,12 @@ public:
if (itemId.empty())
break;
- GdriveItemDetails details = {};
+ GdriveItemDetails details = {}; //read in correct sequence!
details.itemName = utfTo<Zstring>(readContainer<std::string>(stream)); //
details.type = readNumber<GdriveItemType>(stream); //
details.owner = readNumber <FileOwner>(stream); //
details.fileSize = readNumber <uint64_t>(stream); //SysErrorUnexpectedEos
- details.modTime = readNumber <int64_t>(stream); //
+ details.modTime = static_cast<time_t>(readNumber<int64_t>(stream)); //
details.targetId = readContainer<std::string>(stream); //
size_t parentsCount = readNumber<uint32_t>(stream); //SysErrorUnexpectedEos
@@ -1964,10 +1967,12 @@ public:
{
if (afsPath.value.empty()) //location root not covered by itemDetails_
{
- GdriveItemDetails rootDetails = {};
- rootDetails.type = GdriveItemType::folder;
- //rootDetails.itemName =... => better leave empty for a root item!
- rootDetails.owner = sharedDriveName_.empty() ? FileOwner::me : FileOwner::none;
+ GdriveItemDetails rootDetails
+ {
+ .type = GdriveItemType::folder,
+ //.itemName =... => better leave empty for a root item!
+ .owner = sharedDriveName_.empty() ? FileOwner::me : FileOwner::none,
+ };
return {locationRootId, std::move(rootDetails)};
}
@@ -2038,12 +2043,14 @@ public:
void notifyFolderCreated(const FileStateDelta& stateDelta, const std::string& folderId, const Zstring& folderName, const std::string& parentId)
{
- GdriveItemDetails details = {};
- details.itemName = folderName;
- details.type = GdriveItemType::folder;
- details.owner = FileOwner::me;
- details.modTime = std::time(nullptr);
- details.parentIds.push_back(parentId);
+ GdriveItemDetails details
+ {
+ .itemName = folderName,
+ .modTime = std::time(nullptr),
+ .type = GdriveItemType::folder,
+ .owner = FileOwner::me,
+ .parentIds{parentId},
+ };
//avoid needless conflicts due to different Google Drive folder modTime!
if (auto it = itemDetails_.find(folderId); it != itemDetails_.end())
@@ -2054,13 +2061,15 @@ public:
void notifyShortcutCreated(const FileStateDelta& stateDelta, const std::string& shortcutId, const Zstring& shortcutName, const std::string& parentId, const std::string& targetId)
{
- GdriveItemDetails details = {};
- details.itemName = shortcutName;
- details.type = GdriveItemType::shortcut;
- details.owner = FileOwner::me;
- details.modTime = std::time(nullptr);
- details.targetId = targetId;
- details.parentIds.push_back(parentId);
+ GdriveItemDetails details
+ {
+ .itemName = shortcutName,
+ .modTime = std::time(nullptr),
+ .type = GdriveItemType::shortcut,
+ .owner = FileOwner::me,
+ .targetId = targetId,
+ .parentIds{parentId},
+ };
//avoid needless conflicts due to different Google Drive folder modTime!
if (auto it = itemDetails_.find(shortcutId); it != itemDetails_.end())
@@ -3207,12 +3216,16 @@ struct OutputStreamGdrive : public AFS::OutputStreamImpl
//already existing: creates duplicate
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
- GdriveItem newFileItem = {};
- newFileItem.itemId = fileIdNew;
- newFileItem.details.itemName = fileName;
- newFileItem.details.type = GdriveItemType::file;
- newFileItem.details.owner = FileOwner::me;
- newFileItem.details.fileSize = asyncStreamIn->getTotalBytesRead();
+ GdriveItem newFileItem
+ {
+ .itemId = fileIdNew,
+ .details{
+ .itemName = fileName,
+ .fileSize = asyncStreamIn->getTotalBytesRead(),
+ .type = GdriveItemType::file,
+ .owner = FileOwner::me,
+ }
+ };
if (modTime) //else: whatever modTime Google Drive selects will be notified after GDRIVE_SYNC_INTERVAL
newFileItem.details.modTime = *modTime;
newFileItem.details.parentIds.push_back(parentId);
@@ -3611,14 +3624,18 @@ private:
//buffer new file state ASAP (don't wait GDRIVE_SYNC_INTERVAL)
accessGlobalFileState(fsTarget.gdriveLogin_, [&](GdriveFileStateAtLocation& fileState) //throw SysError
{
- GdriveItem newFileItem = {};
- newFileItem.itemId = fileIdTrg;
- newFileItem.details.itemName = itemNameNew;
- newFileItem.details.type = GdriveItemType::file;
- newFileItem.details.owner = fileState.all().getSharedDriveName().empty() ? FileOwner::me : FileOwner::none;
- newFileItem.details.fileSize = itemDetailsSrc.fileSize;
- newFileItem.details.modTime = itemDetailsSrc.modTime;
- newFileItem.details.parentIds.push_back(parentIdTrg);
+ const GdriveItem newFileItem
+ {
+ .itemId = fileIdTrg,
+ .details{
+ .itemName = itemNameNew,
+ .fileSize = itemDetailsSrc.fileSize,
+ .modTime = itemDetailsSrc.modTime,
+ .type = GdriveItemType::file,
+ .owner = fileState.all().getSharedDriveName().empty() ? FileOwner::me : FileOwner::none,
+ .parentIds{parentIdTrg},
+ }
+ };
fileState.all().notifyItemCreated(aaiTrg.stateDelta, newFileItem);
});
diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp
index 12b69b3d..8e7a6337 100644
--- a/FreeFileSync/Source/afs/native.cpp
+++ b/FreeFileSync/Source/afs/native.cpp
@@ -174,7 +174,7 @@ std::vector<FsItem> getDirContentFlat(const Zstring& dirPath) //throw FileError
struct FsItemDetails
{
ItemType type;
- time_t modTime; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime; //number of seconds since Jan. 1st 1970 GMT
uint64_t fileSize; //unit: bytes!
AFS::FingerPrint filePrint;
};
@@ -605,7 +605,7 @@ private:
catch (FileError&) {});
if (copyFilePermissions)
- copyItemPermissions(getNativePath(afsSource), nativePathTarget, ProcSymlink::direct); //throw FileError
+ copyItemPermissions(getNativePath(afsSource), nativePathTarget, ProcSymlink::asLink); //throw FileError
}
//already existing: undefined behavior! (e.g. fail/overwrite)
diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp
index e9849f80..598eed5c 100644
--- a/FreeFileSync/Source/application.cpp
+++ b/FreeFileSync/Source/application.cpp
@@ -24,7 +24,6 @@
#include "ui/batch_status_handler.h"
#include "ui/main_dlg.h"
#include "base_tools.h"
-//#include "log_file.h"
#include "ffs_paths.h"
#include <gtk/gtk.h>
@@ -55,10 +54,10 @@ void showSyntaxHelp()
setTitle(_("Command line")).
setDetailInstructions(_("Syntax:") + L"\n\n" +
L"FreeFileSync" + L'\n' +
- L" [" + _("config files:") + L" *.ffs_gui/*.ffs_batch]" + L'\n' +
- L" [-DirPair " + _("directory") + L' ' + _("directory") + L"]" L"\n" +
- L" [-Edit]" + L'\n' +
- L" [" + _("global config file:") + L" GlobalSettings.xml]" + L"\n\n" +
+ TAB_SPACE + L"[" + _("config files:") + L" *.ffs_gui/*.ffs_batch]" + L'\n' +
+ TAB_SPACE + L"[-DirPair " + _("directory") + L' ' + _("directory") + L"]" L"\n" +
+ TAB_SPACE + L"[-Edit]" + L'\n' +
+ TAB_SPACE + L"[" + _("global config file:") + L" GlobalSettings.xml]" + L"\n\n" +
_("config files:") + L'\n' +
_("Any number of FreeFileSync \"ffs_gui\" and/or \"ffs_batch\" configuration files.") + L"\n\n" +
@@ -270,7 +269,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs)
{
//parse command line arguments
std::vector<std::pair<Zstring, Zstring>> dirPathPhrasePairs;
- std::vector<std::pair<Zstring, XmlType>> configFiles; //XmlType: batch or GUI files only
+ std::vector<Zstring> cfgFilePaths;
Zstring globalConfigFile;
bool openForEdit = false;
{
@@ -351,34 +350,20 @@ void Application::launch(const std::vector<Zstring>& commandArgs)
}
else
{
- Zstring filePath = getResolvedFilePath(*it);
-
+ const Zstring& filePath = getResolvedFilePath(*it);
+#if 0
if (!fileAvailable(filePath)) //...be a little tolerant
- {
- if (fileAvailable(filePath + Zstr(".ffs_batch")))
- filePath += Zstr(".ffs_batch");
- else if (fileAvailable(filePath + Zstr(".ffs_gui")))
- filePath += Zstr(".ffs_gui");
- else if (fileAvailable(filePath + Zstr(".xml")))
- filePath += Zstr(".xml");
- else
- throw FileError(replaceCpy(_("Cannot find file %x."), L"%x", fmtPath(filePath)));
- }
-
- switch (getXmlType(filePath)) //throw FileError
- {
- case XmlType::gui:
- configFiles.emplace_back(filePath, XmlType::gui);
- break;
- case XmlType::batch:
- configFiles.emplace_back(filePath, XmlType::batch);
- break;
- case XmlType::global:
- globalConfigFile = filePath;
- break;
- case XmlType::other:
- throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
- }
+ for (const Zchar* ext : {Zstr(".ffs_gui"), Zstr(".ffs_batch"), Zstr(".xml")})
+ if (fileAvailable(filePath + ext))
+ filePath += ext;
+#endif
+ if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui")) ||
+ endsWithAsciiNoCase(filePath, Zstr(".ffs_batch")))
+ cfgFilePaths.push_back(filePath);
+ else if (endsWithAsciiNoCase(filePath, Zstr(".xml")))
+ globalConfigFile = filePath;
+ else
+ throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
}
}
//----------------------------------------------------------------------------------------------------
@@ -415,7 +400,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs)
//---------------------------
const Zstring globalConfigFilePath = !globalConfigFile.empty() ? globalConfigFile : getGlobalConfigDefaultPath();
- if (configFiles.empty())
+ if (cfgFilePaths.empty())
{
//gui mode: default startup
if (dirPathPhrasePairs.empty())
@@ -431,25 +416,25 @@ void Application::launch(const std::vector<Zstring>& commandArgs)
runGuiMode(globalConfigFilePath, guiCfg, std::vector<Zstring>(), !openForEdit /*startComparison*/);
}
}
- else if (configFiles.size() == 1)
+ else if (cfgFilePaths.size() == 1)
{
- const Zstring filepath = configFiles[0].first;
+ const Zstring filePath = cfgFilePaths[0];
//batch mode
- if (configFiles[0].second == XmlType::batch && !openForEdit)
+ if (endsWithAsciiNoCase(filePath, Zstr(".ffs_batch")) && !openForEdit)
{
- auto [batchCfg, warningMsg] = readBatchConfig(filepath); //throw FileError
+ auto [batchCfg, warningMsg] = readBatchConfig(filePath); //throw FileError
if (!warningMsg.empty())
throw FileError(warningMsg); //batch mode: break on errors AND even warnings!
replaceDirectories(batchCfg.mainCfg); //throw FileError
- runBatchMode(globalConfigFilePath, batchCfg, filepath);
+ runBatchMode(globalConfigFilePath, batchCfg, filePath);
}
//GUI mode: single config (ffs_gui *or* ffs_batch)
else
{
- auto [guiCfg, warningMsg] = readAnyConfig({filepath}); //throw FileError
+ auto [guiCfg, warningMsg] = readAnyConfig({filePath}); //throw FileError
if (!warningMsg.empty())
showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg));
//what about simulating changed config on parsing errors?
@@ -458,7 +443,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs)
//what about simulating changed config due to directory replacement?
//-> propably fine to not show as changed on GUI and not ask user to save on exit!
- runGuiMode(globalConfigFilePath, guiCfg, {filepath}, !openForEdit); //caveat: guiCfg and filepath do not match if directories were set/replaced via command line!
+ runGuiMode(globalConfigFilePath, guiCfg, {filePath}, !openForEdit); //caveat: guiCfg and filepath do not match if directories were set/replaced via command line!
}
}
//gui mode: merged configs
@@ -467,16 +452,12 @@ void Application::launch(const std::vector<Zstring>& commandArgs)
if (!dirPathPhrasePairs.empty())
throw FileError(_("Directories cannot be set for more than one configuration file."));
- std::vector<Zstring> filePaths;
- for (const auto& [filePath, xmlType] : configFiles)
- filePaths.push_back(filePath);
-
- const auto& [guiCfg, warningMsg] = readAnyConfig(filePaths); //throw FileError
+ const auto& [guiCfg, warningMsg] = readAnyConfig(cfgFilePaths); //throw FileError
if (!warningMsg.empty())
showNotificationDialog(nullptr, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(warningMsg));
//what about simulating changed config on parsing errors?
- runGuiMode(globalConfigFilePath, guiCfg, filePaths, !openForEdit /*startComparison*/);
+ runGuiMode(globalConfigFilePath, guiCfg, cfgFilePaths, !openForEdit /*startComparison*/);
}
}
catch (const FileError& e)
diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp
index 2ce3cd41..df760e4e 100644
--- a/FreeFileSync/Source/base/algorithm.cpp
+++ b/FreeFileSync/Source/base/algorithm.cpp
@@ -565,14 +565,15 @@ private:
if (dbEntryL && dbEntryR && dbEntryL != dbEntryR) //conflict: which db entry to use?
return file.setSyncDirConflict(txtDbAmbiguous_);
+ if (const InSyncFile* dbEntry = dbEntryL ? dbEntryL : dbEntryR;
+ dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_)) //check *before* misleadingly reporting txtNoSideChanged_
+ return file.setSyncDirConflict(txtDbNotInSync_);
+
const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(file, dbEntryL, ignoreTimeShiftMinutes_);
const bool changeOnRight = !matchesDbEntry<SelectSide::right>(file, dbEntryR, ignoreTimeShiftMinutes_);
if (changeOnLeft == changeOnRight)
file.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_);
- else if (const InSyncFile* dbEntry = dbEntryL ? dbEntryL : dbEntryR;
- dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_))
- file.setSyncDirConflict(txtDbNotInSync_);
else
file.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left);
}
@@ -601,14 +602,15 @@ private:
if (dbEntryL && dbEntryR && dbEntryL != dbEntryR) //conflict: which db entry to use?
return symlink.setSyncDirConflict(txtDbAmbiguous_);
+ if (const InSyncSymlink* dbEntry = dbEntryL ? dbEntryL : dbEntryR;
+ dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_))
+ return symlink.setSyncDirConflict(txtDbNotInSync_);
+
const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(symlink, dbEntryL, ignoreTimeShiftMinutes_);
const bool changeOnRight = !matchesDbEntry<SelectSide::right>(symlink, dbEntryR, ignoreTimeShiftMinutes_);
if (changeOnLeft == changeOnRight)
symlink.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_);
- else if (const InSyncSymlink* dbEntry = dbEntryL ? dbEntryL : dbEntryR;
- dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_))
- symlink.setSyncDirConflict(txtDbNotInSync_);
else
symlink.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left);
}
@@ -653,15 +655,18 @@ private:
if (cat != DIR_EQUAL)
{
- const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(folder, dbEntryL);
- const bool changeOnRight = !matchesDbEntry<SelectSide::right>(folder, dbEntryR);
-
- if (changeOnLeft == changeOnRight)
- folder.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_);
- else if (dbEntry && !stillInSync(*dbEntry))
+ if (dbEntry && !stillInSync(*dbEntry))
folder.setSyncDirConflict(txtDbNotInSync_);
else
- folder.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left);
+ {
+ const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(folder, dbEntryL);
+ const bool changeOnRight = !matchesDbEntry<SelectSide::right>(folder, dbEntryR);
+
+ if (changeOnLeft == changeOnRight)
+ folder.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_);
+ else
+ folder.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left);
+ }
}
recurse(folder, dbEntry);
@@ -669,9 +674,9 @@ private:
//need ref-counted strings! see FileSystemObject::syncDirectionConflict_
const Zstringc txtBothSidesChanged_ = utfTo<Zstringc>(_("Both sides have changed since last synchronization."));
- const Zstringc txtNoSideChanged_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + _("No change since last synchronization."));
- const Zstringc txtDbNotInSync_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + _("The database entry is not in sync considering current settings."));
- const Zstringc txtDbAmbiguous_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + _("The database entry is ambiguous."));
+ const Zstringc txtNoSideChanged_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("No change since last synchronization."));
+ const Zstringc txtDbNotInSync_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("The database entry is not in sync considering current settings."));
+ const Zstringc txtDbAmbiguous_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("The database entry is ambiguous."));
const CompareVariant cmpVar_;
const int fileTimeTolerance_;
@@ -915,10 +920,9 @@ private:
file.setActive(matchSize<SelectSide::left>(file) &&
matchTime<SelectSide::left>(file));
else
- {
- //the only case with partially unclear semantics:
- //file and time filters may match or not match on each side, leaving a total of 16 combinations for both sides!
- /*
+ /* the only case with partially unclear semantics:
+ file and time filters may match or not match on each side, leaving a total of 16 combinations for both sides!
+
ST S T - ST := match size and time
--------- S := match size only
ST |I|I|I|I| T := match time only
@@ -929,13 +933,11 @@ private:
------------ ? := unclear
- |I|E|E|E|
------------
- */
- //let's set ? := E
+ let's set ? := E */
file.setActive((matchSize<SelectSide::right>(file) &&
matchTime<SelectSide::right>(file)) ||
(matchSize<SelectSide::left>(file) &&
matchTime<SelectSide::left>(file)));
- }
}
}
@@ -1062,15 +1064,15 @@ void fff::applyTimeSpanFilter(FolderComparison& folderCmp, time_t timeFrom, time
}
-std::optional<PathDependency> fff::getPathDependency(const AbstractPath& basePathL, const PathFilter& filterL,
- const AbstractPath& basePathR, const PathFilter& filterR)
+std::optional<PathDependency> fff::getPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL,
+ const AbstractPath& folderPathR, const PathFilter& filterR)
{
- if (!AFS::isNullPath(basePathL) && !AFS::isNullPath(basePathR))
+ if (!AFS::isNullPath(folderPathL) && !AFS::isNullPath(folderPathR))
{
- if (basePathL.afsDevice == basePathR.afsDevice)
+ if (folderPathL.afsDevice == folderPathR.afsDevice)
{
- const std::vector<Zstring> relPathL = split(basePathL.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
- const std::vector<Zstring> relPathR = split(basePathR.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
+ const std::vector<Zstring> relPathL = split(folderPathL.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
+ const std::vector<Zstring> relPathR = split(folderPathR.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
const bool leftParent = relPathL.size() <= relPathR.size();
@@ -1084,8 +1086,6 @@ std::optional<PathDependency> fff::getPathDependency(const AbstractPath& basePat
{
relDirPath = appendPath(relDirPath, itemName);
});
- const AbstractPath& basePathP = leftParent ? basePathL : basePathR;
- const AbstractPath& basePathC = leftParent ? basePathR : basePathL;
const PathFilter& filterP = leftParent ? filterL : filterR;
//if there's a dependency, check if the sub directory is (fully) excluded via filter
@@ -1094,7 +1094,7 @@ std::optional<PathDependency> fff::getPathDependency(const AbstractPath& basePat
// - user may have manually excluded the conflicting items or changed the filter settings without running a re-compare
bool childItemMightMatch = true;
if (relDirPath.empty() || filterP.passDirFilter(relDirPath, &childItemMightMatch) || childItemMightMatch)
- return PathDependency({basePathP, basePathC, relDirPath});
+ return PathDependency{leftParent ? folderPathL : folderPathR, relDirPath};
}
}
}
@@ -1231,6 +1231,7 @@ void copyToAlternateFolderFrom(const std::vector<const FileSystemObject*>& rowsT
callback.requestUiUpdate(); //throw X => not reliably covered by PercentStatReporter::updateStatus()! e.g. during first few seconds: STATUS_PERCENT_DELAY!
});
//result.errorModTime? => probably irrelevant (behave like Windows Explorer)
+ warn_static("no, should be logged at least!")
});
statReporter.updateStatus(1, 0); //throw X
},
diff --git a/FreeFileSync/Source/base/algorithm.h b/FreeFileSync/Source/base/algorithm.h
index 16c9c179..385d3087 100644
--- a/FreeFileSync/Source/base/algorithm.h
+++ b/FreeFileSync/Source/base/algorithm.h
@@ -41,12 +41,11 @@ void setActiveStatus(bool newStatus, FileSystemObject& fsObj); //activate or
struct PathDependency
{
- AbstractPath basePathParent;
- AbstractPath basePathChild;
+ AbstractPath folderPathParent;
Zstring relPath; //filled if child path is subfolder of parent path; empty if child path == parent path
};
-std::optional<PathDependency> getPathDependency(const AbstractPath& basePathL, const PathFilter& filterL,
- const AbstractPath& basePathR, const PathFilter& filterR);
+std::optional<PathDependency> getPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL,
+ const AbstractPath& folderPathR, const PathFilter& filterR);
std::pair<std::wstring, int> getSelectedItemsAsString( //returns string with item names and total count of selected(!) items, NOT total files/dirs!
std::span<const FileSystemObject* const> selectionLeft, //all pointers need to be bound!
diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp
index 569ddb96..1a1301bc 100644
--- a/FreeFileSync/Source/base/comparison.cpp
+++ b/FreeFileSync/Source/base/comparison.cpp
@@ -1,4 +1,4 @@
-// *****************************************************************************
+// *****************************************************************************
// * 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 *
@@ -144,6 +144,8 @@ ResolvedBaseFolders initializeBaseFolders(const std::vector<FolderPairCfg>& fpCf
}
callback.reportWarning(msg, warnings.warnFoldersDifferInCase); //throw X
+
+ //what about /folder and /Folder/subfolder? => yes, inconsistent, but doesn't matter for FFS
}
//---------------------------------------------------------------------------
@@ -248,15 +250,15 @@ template <SelectSide side, class FileOrLinkPair> inline
Zstringc getConflictInvalidDate(const FileOrLinkPair& file)
{
return utfTo<Zstringc>(replaceCpy(_("File %x has an invalid date."), L"%x", fmtPath(AFS::getDisplayPath(file.template getAbstractPath<side>()))) + L'\n' +
- _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<side>()));
+ TAB_SPACE + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<side>()));
}
Zstringc getConflictSameDateDiffSize(const FilePair& file)
{
return utfTo<Zstringc>(_("Files have the same date but a different size.") + L'\n' +
- arrowLeft + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::left >()) + L" " + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::left>()) + L'\n' +
- arrowRight + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::right>()) + L" " + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::right>()));
+ TAB_SPACE + arrowLeft + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::left >()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::left>()) + L'\n' +
+ TAB_SPACE + arrowRight + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::right>()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::right>()));
}
@@ -269,8 +271,8 @@ Zstringc getConflictSkippedBinaryComparison()
Zstringc getDescrDiffMetaShortnameCase(const FileSystemObject& fsObj)
{
return utfTo<Zstringc>(_("Items differ in attributes only") + L'\n' +
- arrowLeft + L' ' + fmtPath(fsObj.getItemName<SelectSide::left >()) + L'\n' +
- arrowRight + L' ' + fmtPath(fsObj.getItemName<SelectSide::right>()));
+ TAB_SPACE + arrowLeft + L' ' + fmtPath(fsObj.getItemName<SelectSide::left >()) + L'\n' +
+ TAB_SPACE + arrowRight + L' ' + fmtPath(fsObj.getItemName<SelectSide::right>()));
}
@@ -279,8 +281,8 @@ template <class FileOrLinkPair>
Zstringc getDescrDiffMetaData(const FileOrLinkPair& file)
{
return utfTo<Zstringc>(_("Items differ in attributes only") + L'\n' +
- arrowLeft + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<SelectSide::left >()) + L'\n' +
- arrowRight + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<SelectSide::right>()));
+ TAB_SPACE + arrowLeft + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<SelectSide::left >()) + L'\n' +
+ TAB_SPACE + arrowRight + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<SelectSide::right>()));
}
#endif
@@ -545,7 +547,6 @@ std::vector<std::shared_ptr<BaseFolderPair>> ComparisonBuffer::compareByContent(
fpWorkload.push_back({posL, posR, std::move(filesToCompareBytewise)});
};
- //PERF_START;
std::vector<std::shared_ptr<BaseFolderPair>> output;
const Zstringc txtConflictSkippedBinaryComparison = getConflictSkippedBinaryComparison(); //avoid premature pess.: save memory via ref-counted string
@@ -711,23 +712,19 @@ const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstri
template <class MapType, class Function>
void forEachSorted(const MapType& fileMap, Function fun)
{
- struct FileRef
- {
- Zstring upperCaseName;
- const typename MapType::value_type* ref;
- };
+ using FileRef = const typename MapType::value_type*;
+
std::vector<FileRef> fileList;
- fileList.reserve(fileMap.size()); //perf: ~5% shorter runtime
+ fileList.reserve(fileMap.size());
for (const auto& item : fileMap)
- fileList.push_back({getUpperCase(item.first), &item});
+ fileList.push_back(&item);
- //primary sort: ignore Unicode normal form and upper/lower case
- //=> natural default sequence on file grid UI
- std::sort(fileList.begin(), fileList.end(), [](const FileRef& lhs, const FileRef& rhs) { return lhs.upperCaseName < rhs.upperCaseName; });
+ //sort for natural default sequence on UI file grid:
+ std::sort(fileList.begin(), fileList.end(), [](const FileRef& lhs, const FileRef& rhs) { return compareNoCase(lhs->first /*item name*/, rhs->first) < 0; });
for (const auto& item : fileList)
- fun(item.ref->first, item.ref->second);
+ fun(item->first, item->second);
}
@@ -761,22 +758,22 @@ void matchFolders(const MapType& mapLeft, const MapType& mapRight, ProcessLeftOn
{
struct FileRef
{
- Zstring upperCaseName; //buffer expensive getUpperCase() calls!!
+ //perf: buffer ZstringNoCase instead of compareNoCase()/equalNoCase()? => makes no (significant) difference!
const typename MapType::value_type* ref;
bool leftSide;
};
std::vector<FileRef> fileList;
fileList.reserve(mapLeft.size() + mapRight.size()); //perf: ~5% shorter runtime
- for (const auto& item : mapLeft ) fileList.push_back({getUpperCase(item.first), &item, true });
- for (const auto& item : mapRight) fileList.push_back({getUpperCase(item.first), &item, false});
+ for (const auto& item : mapLeft ) fileList.push_back({&item, true });
+ for (const auto& item : mapRight) fileList.push_back({&item, false});
//primary sort: ignore Unicode normal form and upper/lower case
- //bonus: natural default sequence on file grid UI
- std::sort(fileList.begin(), fileList.end(), [](const FileRef& lhs, const FileRef& rhs) { return lhs.upperCaseName < rhs.upperCaseName; });
+ //bonus: natural default sequence on UI file grid
+ std::sort(fileList.begin(), fileList.end(), [](const FileRef& lhs, const FileRef& rhs) { return compareNoCase(lhs.ref->first /*item name*/, rhs.ref->first) < 0; });
using ItType = typename std::vector<FileRef>::iterator;
- auto tryMatchRange = [&](ItType it, ItType itLast) //auto? compiler error on VS 17.2...
+ auto tryMatchRange = [&](ItType it, ItType itLast) //auto parameters? compiler error on VS 17.2...
{
const size_t equalCountL = std::count_if(it, itLast, [](const FileRef& fr) { return fr.leftSide; });
const size_t equalCountR = itLast - it - equalCountL;
@@ -800,7 +797,7 @@ void matchFolders(const MapType& mapLeft, const MapType& mapRight, ProcessLeftOn
for (auto it = fileList.begin(); it != fileList.end();)
{
//find equal range: ignore case, ignore Unicode normalization
- auto itEndEq = std::find_if(it + 1, fileList.end(), [&](const FileRef& fr) { return fr.upperCaseName != it->upperCaseName; });
+ auto itEndEq = std::find_if(it + 1, fileList.end(), [&](const FileRef& fr) { return !equalNoCase(fr.ref->first, it->ref->first); });
if (!tryMatchRange(it, itEndEq))
{
//secondary sort: respect case, ignore unicode normal forms
@@ -1088,25 +1085,31 @@ FolderComparison fff::compare(WarningDialogs& warnings,
_("The corresponding folder will be considered as empty."), warnings.warnInputFieldEmpty);
}
- //check whether one side is a sub directory of the other side (folder-pair-wise!)
- //similar check (warnDependentBaseFolders) if one directory is read/written by multiple pairs not before beginning of synchronization
+ //Check whether one side is a sub directory of the other side (folder-pair-wise!)
+ //The similar check (warnDependentBaseFolders) if one directory is read/written by multiple pairs not before beginning of synchronization
{
std::wstring msg;
+ bool shouldExclude = false;
for (const auto& [folderPair, fpCfg] : workLoad)
if (std::optional<PathDependency> pd = getPathDependency(folderPair.folderPathLeft, fpCfg.filter.nameFilter.ref(),
folderPair.folderPathRight, fpCfg.filter.nameFilter.ref()))
{
msg += L"\n\n" +
- AFS::getDisplayPath(folderPair.folderPathLeft) + L'\n' +
+ AFS::getDisplayPath(folderPair.folderPathLeft) + L" <-> " + L'\n' +
AFS::getDisplayPath(folderPair.folderPathRight);
if (!pd->relPath.empty())
- msg += L'\n' + _("Exclude:") + L' ' + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR);
+ {
+ shouldExclude = true;
+ msg += std::wstring() + L'\n' + L"⇒ " +
+ _("Exclude:") + L' ' + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR);
+ }
}
if (!msg.empty())
- callback.reportWarning(_("One base folder of a folder pair is contained in the other one.") + L'\n' + //throw X
- _("The folder should be excluded from synchronization via filter.") + msg, warnings.warnDependentFolderPair);
+ callback.reportWarning(_("One base folder of a folder pair is contained in the other one.") +
+ (shouldExclude ? L'\n' + _("The folder should be excluded from synchronization via filter.") : L"") +
+ msg, warnings.warnDependentFolderPair); //throw X
}
//-------------------end of basic checks------------------------------------------
diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp
index a509986a..a37e336e 100644
--- a/FreeFileSync/Source/base/db_file.cpp
+++ b/FreeFileSync/Source/base/db_file.cpp
@@ -433,8 +433,8 @@ private:
const Zstring itemName = readItemName(); //
const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(streamInSmallNum_)); //
- const InSyncDescrLink dataL(readNumber<int64_t>(streamInBigNum_)); //throw SysErrorUnexpectedEos
- const InSyncDescrLink dataT(readNumber<int64_t>(streamInBigNum_)); //
+ const InSyncDescrLink dataL{static_cast<time_t>(readNumber<int64_t>(streamInBigNum_))}; //throw SysErrorUnexpectedEos
+ const InSyncDescrLink dataT{static_cast<time_t>(readNumber<int64_t>(streamInBigNum_))}; //
container.addSymlink(itemName,
selectParam<leadSide>(dataL, dataT),
@@ -456,7 +456,7 @@ private:
InSyncDescrFile readFileDescr() //throw SysErrorUnexpectedEos
{
- const auto modTime = readNumber<int64_t>(streamInBigNum_); //throw SysErrorUnexpectedEos
+ const auto modTime = static_cast<time_t>(readNumber<int64_t>(streamInBigNum_)); //throw SysErrorUnexpectedEos
AFS::FingerPrint filePrint = 0;
if (streamVersion_ == 3) //TODO: remove migration code at some time! 2021-02-14
@@ -473,7 +473,7 @@ private:
else
filePrint = readNumber<AFS::FingerPrint>(streamInBigNum_); //throw SysErrorUnexpectedEos
- return InSyncDescrFile(modTime, filePrint);
+ return {modTime, filePrint};
}
//TODO: remove migration code at some time! 2017-02-01
@@ -495,11 +495,11 @@ private:
const Zstring itemName = utfTo<Zstring>(readContainer<std::string>(inputBoth_));
const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(inputBoth_));
const uint64_t fileSize = readNumber<uint64_t>(inputBoth_);
- const auto modTimeL = readNumber<int64_t>(inputLeft_);
+ const auto modTimeL = static_cast<time_t>(readNumber<int64_t>(inputLeft_));
/*const auto fileIdL =*/ readContainer<std::string>(inputLeft_);
- const auto modTimeR = readNumber<int64_t>(inputRight_);
+ const auto modTimeR = static_cast<time_t>(readNumber<int64_t>(inputRight_));
/*const auto fileIdR =*/ readContainer<std::string>(inputRight_);
- container.addFile(itemName, InSyncDescrFile(modTimeL, AFS::FingerPrint()), InSyncDescrFile(modTimeR, AFS::FingerPrint()), cmpVar, fileSize);
+ container.addFile(itemName, InSyncDescrFile{modTimeL, AFS::FingerPrint()}, InSyncDescrFile{modTimeR, AFS::FingerPrint()}, cmpVar, fileSize);
}
size_t linkCount = readNumber<uint32_t>(inputBoth_);
@@ -507,9 +507,9 @@ private:
{
const Zstring itemName = utfTo<Zstring>(readContainer<std::string>(inputBoth_));
const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(inputBoth_));
- const auto modTimeL = readNumber<int64_t>(inputLeft_);
- const auto modTimeR = readNumber<int64_t>(inputRight_);
- container.addSymlink(itemName, InSyncDescrLink(modTimeL), InSyncDescrLink(modTimeR), cmpVar);
+ const auto modTimeL = static_cast<time_t>(readNumber<int64_t>(inputLeft_));
+ const auto modTimeR = static_cast<time_t>(readNumber<int64_t>(inputRight_));
+ container.addSymlink(itemName, InSyncDescrLink{modTimeL}, InSyncDescrLink{modTimeR}, cmpVar);
}
size_t dirCount = readNumber<uint32_t>(inputBoth_);
@@ -578,10 +578,10 @@ private:
//create or update new "in-sync" state
dbFiles.insert_or_assign(file.getItemNameAny(),
- InSyncFile(InSyncDescrFile(file.getLastWriteTime<SelectSide::left >(),
- file.getFilePrint <SelectSide::left >()),
- InSyncDescrFile(file.getLastWriteTime<SelectSide::right>(),
- file.getFilePrint <SelectSide::right>()),
+ InSyncFile(InSyncDescrFile{file.getLastWriteTime<SelectSide::left >(),
+ file.getFilePrint <SelectSide::left >()},
+ InSyncDescrFile{file.getLastWriteTime<SelectSide::right>(),
+ file.getFilePrint <SelectSide::right>()},
activeCmpVar_,
file.getFileSize<SelectSide::left>()));
toPreserve.insert(file.getItemNameAny());
@@ -618,8 +618,8 @@ private:
//create or update new "in-sync" state
dbSymlinks.insert_or_assign(symlink.getItemNameAny(),
- InSyncSymlink(InSyncDescrLink(symlink.getLastWriteTime<SelectSide::left >()),
- InSyncDescrLink(symlink.getLastWriteTime<SelectSide::right>()),
+ InSyncSymlink(InSyncDescrLink{symlink.getLastWriteTime<SelectSide::left >()},
+ InSyncDescrLink{symlink.getLastWriteTime<SelectSide::right>()},
activeCmpVar_));
toPreserve.insert(symlink.getItemNameAny());
}
diff --git a/FreeFileSync/Source/base/db_file.h b/FreeFileSync/Source/base/db_file.h
index 83174c3b..552cdfef 100644
--- a/FreeFileSync/Source/base/db_file.h
+++ b/FreeFileSync/Source/base/db_file.h
@@ -19,18 +19,12 @@ const Zchar SYNC_DB_FILE_ENDING[] = Zstr(".ffs_db"); //don't use Zstring as glob
struct InSyncDescrFile //subset of FileAttributes
{
- InSyncDescrFile(time_t modTimeIn, AFS::FingerPrint filePrintIn) :
- modTime(modTimeIn),
- filePrint(filePrintIn) {}
-
time_t modTime = 0;
AFS::FingerPrint filePrint = 0; //optional!
};
struct InSyncDescrLink
{
- explicit InSyncDescrLink(time_t modTimeIn) : modTime(modTimeIn) {}
-
time_t modTime = 0;
};
diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp
index ad2e06f6..ee49cb9d 100644
--- a/FreeFileSync/Source/base/dir_lock.cpp
+++ b/FreeFileSync/Source/base/dir_lock.cpp
@@ -167,9 +167,11 @@ struct LockInformation //throw FileError
LockInformation getLockInfoFromCurrentProcess() //throw FileError
{
- LockInformation lockInfo = {};
- lockInfo.lockId = generateGUID();
- lockInfo.userId = utfTo<std::string>(getLoginUser()); //throw FileError
+ LockInformation lockInfo =
+ {
+ .lockId = generateGUID(),
+ .userId = utfTo<std::string>(getLoginUser()), //throw FileError
+ };
const std::string osName = "Linux";
diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h
index 1ae07f8c..2062a4ce 100644
--- a/FreeFileSync/Source/base/file_hierarchy.h
+++ b/FreeFileSync/Source/base/file_hierarchy.h
@@ -35,7 +35,7 @@ struct FileAttributes
static_assert(std::is_signed_v<time_t>, "... and signed!");
}
- time_t modTime = 0; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT
uint64_t fileSize = 0;
AFS::FingerPrint filePrint = 0; //optional
bool isFollowedSymlink = false;
@@ -49,7 +49,7 @@ struct LinkAttributes
LinkAttributes() {}
explicit LinkAttributes(time_t modTimeIn) : modTime(modTimeIn) {}
- time_t modTime = 0; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT
};
@@ -75,7 +75,7 @@ constexpr SelectSide getOtherSide = side == SelectSide::left ? SelectSide::right
template <SelectSide side, class T> inline
-static T& selectParam(T& left, T& right)
+T& selectParam(T& left, T& right)
{
if constexpr (side == SelectSide::left)
return left;
@@ -155,7 +155,7 @@ class FileSystemObject;
------------------------------------------------------------------*/
-struct PathInformation //diamond-shaped inheritence!
+struct PathInformation //diamond-shaped inheritance!
{
virtual ~PathInformation() {}
@@ -296,10 +296,10 @@ public:
template <SelectSide side> BaseFolderStatus getFolderStatus() const; //base folder status at the time of comparison!
template <SelectSide side> void setFolderStatus(BaseFolderStatus value); //update after creating the directory in FFS
- //get settings which were used while creating BaseFolderPair
+ //get settings which were used while creating BaseFolderPair:
const PathFilter& getFilter() const { return filter_.ref(); }
CompareVariant getCompVariant() const { return cmpVar_; }
- int getFileTimeTolerance() const { return fileTimeTolerance_; }
+ int getFileTimeTolerance() const { return fileTimeTolerance_; }
const std::vector<unsigned int>& getIgnoredTimeShift() const { return ignoreTimeShiftMinutes_; }
void flip() override;
@@ -703,6 +703,9 @@ public:
}
private:
+ RecursiveObjectVisitor (const RecursiveObjectVisitor&) = delete;
+ RecursiveObjectVisitor& operator=(const RecursiveObjectVisitor&) = delete;
+
const Function1 onFolder_;
const Function2 onFile_;
const Function3 onSymlink_;
diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp
index a0d40da2..2352fec4 100644
--- a/FreeFileSync/Source/base/parallel_scan.cpp
+++ b/FreeFileSync/Source/base/parallel_scan.cpp
@@ -362,7 +362,7 @@ DirCallback::HandleLink DirCallback::onSymlink(const AFS::SymlinkInfo& si) //thr
case SymLinkHandling::exclude:
return HandleLink::skip;
- case SymLinkHandling::direct:
+ case SymLinkHandling::asLink:
if (cfg_.filter.ref().passFileFilter(relPath)) //always use file filter: Link type may not be "stable" on Linux!
{
output_.addSubLink(si.itemName, LinkAttributes(si.modTime));
diff --git a/FreeFileSync/Source/base/path_filter.cpp b/FreeFileSync/Source/base/path_filter.cpp
index ad61cda5..207a4fc6 100644
--- a/FreeFileSync/Source/base/path_filter.cpp
+++ b/FreeFileSync/Source/base/path_filter.cpp
@@ -43,30 +43,30 @@ std::vector<Zstring> fff::splitByDelimiter(const Zstring& filterPhrase)
void NameFilter::parseFilterPhrase(const Zstring& filterPhrase, FilterSet& filter)
{
//normalize filter: 1. ignore Unicode normalization form 2. ignore case
- Zstring filterPhraseFmt = getUpperCase(filterPhrase);
+ Zstring filterPhraseNorm = getUpperCase(filterPhrase);
//3. fix path separator
- if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(filterPhraseFmt, Zstr('/'), FILE_NAME_SEPARATOR);
- if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(filterPhraseFmt, Zstr('\\'), FILE_NAME_SEPARATOR);
+ if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(filterPhraseNorm, Zstr('/'), FILE_NAME_SEPARATOR);
+ if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(filterPhraseNorm, Zstr('\\'), FILE_NAME_SEPARATOR);
+ static_assert(FILE_NAME_SEPARATOR == '/');
const Zstring sepAsterisk = Zstr("/*");
const Zstring asteriskSep = Zstr("*/");
- static_assert(FILE_NAME_SEPARATOR == '/');
auto processTail = [&](const Zstring& phrase)
{
- if (endsWith(phrase, FILE_NAME_SEPARATOR) || //only relevant for folder filtering
- endsWith(phrase, sepAsterisk)) // abc\*
+ if (endsWith(phrase, Zstr(':'))) //file-only tag
+ filter.fileMasks.insert({phrase.begin(), phrase.end() - 1});
+ else if (endsWith(phrase, FILE_NAME_SEPARATOR) || //folder-only tag
+ endsWith(phrase, sepAsterisk)) // abc\*
+ filter.folderMasks.insert(beforeLast(phrase, FILE_NAME_SEPARATOR, IfNotFoundReturn::none));
+ else
{
- const Zstring dirPhrase = beforeLast(phrase, FILE_NAME_SEPARATOR, IfNotFoundReturn::none);
- if (!dirPhrase.empty())
- filter.folderMasks.insert(dirPhrase);
+ filter.fileMasks .insert(phrase);
+ filter.folderMasks.insert(phrase);
}
- else if (!phrase.empty())
- filter.fileFolderMasks.insert(phrase);
};
- for (const Zstring& itemPhrase : splitByDelimiter(filterPhraseFmt))
- {
+ for (const Zstring& itemPhrase : splitByDelimiter(filterPhraseNorm))
/* phrase | action
+---------+--------
| \blah | remove \
@@ -78,15 +78,17 @@ void NameFilter::parseFilterPhrase(const Zstring& filterPhrase, FilterSet& filte
| *\blah | -> add blah
| *\*blah | -> add *blah
+---------+--------
- | blah\ | remove \; folder only
- | blah*\ | remove \; folder only
- | blah\*\ | remove \; folder only
+ | blah: | remove : (file only)
+ | blah\*: | remove : (file only)
+ +---------+--------
+ | blah\ | remove \ (folder only)
+ | blah*\ | remove \ (folder only)
+ | blah\*\ | remove \ (folder only)
+---------+--------
| blah* |
- | blah\* | remove \*; folder only
- | blah*\* | remove \*; folder only
+ | blah\* | remove \* (folder only)
+ | blah*\* | remove \* (folder only)
+---------+-------- */
-
if (startsWith(itemPhrase, FILE_NAME_SEPARATOR)) // \abc
processTail(afterFirst(itemPhrase, FILE_NAME_SEPARATOR, IfNotFoundReturn::none));
else
@@ -95,13 +97,15 @@ void NameFilter::parseFilterPhrase(const Zstring& filterPhrase, FilterSet& filte
if (startsWith(itemPhrase, asteriskSep)) // *\abc
processTail(afterFirst(itemPhrase, asteriskSep, IfNotFoundReturn::none));
}
- }
}
void NameFilter::MaskMatcher::insert(const Zstring& mask)
{
assert(mask == getUpperCase(mask));
+ if (mask.empty())
+ return;
+
if (contains(mask, Zstr('?')) ||
contains(mask, Zstr('*')))
realMasks_.insert(mask);
@@ -256,11 +260,11 @@ bool NameFilter::passFileFilter(const Zstring& relFilePath) const
const Zchar* sepPos = findLast(pathFmt.begin(), pathFmt.end(), FILE_NAME_SEPARATOR);
- if (excludeFilter.fileFolderMasks.matches(pathFmt.begin(), pathFmt.end()) || //either match on file or any parent folder
+ if (excludeFilter.fileMasks.matches(pathFmt.begin(), pathFmt.end()) || //either match on file or any parent folder
(sepPos != pathFmt.end() && excludeFilter.folderMasks.matches(pathFmt.begin(), sepPos))) //match on any parent folder only
return false;
- return includeFilter.fileFolderMasks.matches(pathFmt.begin(), pathFmt.end()) ||
+ return includeFilter.fileMasks.matches(pathFmt.begin(), pathFmt.end()) ||
(sepPos != pathFmt.end() && includeFilter.folderMasks.matches(pathFmt.begin(), sepPos));
}
@@ -273,8 +277,7 @@ bool NameFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMa
//normalize input: 1. ignore Unicode normalization form 2. ignore case
const Zstring& pathFmt = getUpperCase(relDirPath);
- if (excludeFilter.fileFolderMasks.matches(pathFmt.begin(), pathFmt.end()) ||
- excludeFilter.folderMasks .matches(pathFmt.begin(), pathFmt.end()))
+ if (excludeFilter.folderMasks.matches(pathFmt.begin(), pathFmt.end()))
{
if (childItemMightMatch)
*childItemMightMatch = false; //perf: no need to traverse deeper; subfolders/subfiles would be excluded by filter anyway!
@@ -286,13 +289,12 @@ bool NameFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMa
return false;
}
- if (includeFilter.fileFolderMasks.matches(pathFmt.begin(), pathFmt.end()) ||
- includeFilter.folderMasks .matches(pathFmt.begin(), pathFmt.end()))
+ if (includeFilter.folderMasks.matches(pathFmt.begin(), pathFmt.end()))
return true;
if (childItemMightMatch)
- *childItemMightMatch = includeFilter.fileFolderMasks.matchesBegin(pathFmt) || //might match a file or folder in subdirectory
- includeFilter.folderMasks .matchesBegin(pathFmt); //
+ *childItemMightMatch = includeFilter.fileMasks .matchesBegin(pathFmt) || //might match a file or folder in subdirectory
+ includeFilter.folderMasks.matchesBegin(pathFmt); //
return false;
}
diff --git a/FreeFileSync/Source/base/path_filter.h b/FreeFileSync/Source/base/path_filter.h
index 4640996c..f7ca25c8 100644
--- a/FreeFileSync/Source/base/path_filter.h
+++ b/FreeFileSync/Source/base/path_filter.h
@@ -115,7 +115,7 @@ private:
struct FilterSet
{
- MaskMatcher fileFolderMasks;
+ MaskMatcher fileMasks;
MaskMatcher folderMasks;
std::strong_ordering operator<=>(const FilterSet&) const = default;
diff --git a/FreeFileSync/Source/base/structures.h b/FreeFileSync/Source/base/structures.h
index da118913..7c95e41f 100644
--- a/FreeFileSync/Source/base/structures.h
+++ b/FreeFileSync/Source/base/structures.h
@@ -29,7 +29,7 @@ enum class CompareVariant
enum class SymLinkHandling
{
exclude,
- direct,
+ asLink,
follow
};
diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp
index e9958606..7aa49f2e 100644
--- a/FreeFileSync/Source/base/synchronization.cpp
+++ b/FreeFileSync/Source/base/synchronization.cpp
@@ -1,4 +1,4 @@
-// *****************************************************************************
+// *****************************************************************************
// * 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 *
@@ -447,6 +447,208 @@ bool significantDifferenceDetected(const SyncStatistics& folderPairStat)
return nonMatchingRows >= 10 && nonMatchingRows > 0.5 * folderPairStat.rowCount();
}
+//---------------------------------------------------------------------------------------------
+
+struct ChildPathRef
+{
+ const FileSystemObject* fsObj = nullptr;
+ uint64_t childPathHash = 0;
+};
+
+
+template <SelectSide side>
+class GetChildPathsHashed
+{
+public:
+ static std::vector<ChildPathRef> execute(const ContainerObject& folder)
+ {
+ GetChildPathsHashed inst;
+ inst.recurse(folder, FNV1aHash<uint64_t>().get() /*don't start with 0!*/);
+ return std::move(inst.childPathRefs_);
+ }
+
+private:
+ GetChildPathsHashed (const GetChildPathsHashed&) = delete;
+ GetChildPathsHashed& operator=(const GetChildPathsHashed&) = delete;
+
+ GetChildPathsHashed() {}
+
+ void recurse(const ContainerObject& hierObj, uint64_t parentPathHash)
+ {
+ for (const FilePair& file : hierObj.refSubFiles())
+ if (file.isActive())
+ childPathRefs_.push_back({&file, getPathHash(file, parentPathHash)});
+
+ for (const SymlinkPair& symlink : hierObj.refSubLinks())
+ if (symlink.isActive())
+ childPathRefs_.push_back({&symlink, getPathHash(symlink, parentPathHash)});
+
+ for (const FolderPair& subFolder : hierObj.refSubFolders())
+ {
+ const uint64_t folderPathHash = getPathHash(subFolder, parentPathHash);
+
+ if (subFolder.isActive())
+ childPathRefs_.push_back({&subFolder, folderPathHash});
+
+ recurse(subFolder, folderPathHash);
+ }
+ }
+
+ static uint64_t getPathHash(const FileSystemObject& fsObj, uint64_t parentPathHash)
+ {
+ FNV1aHash<uint64_t> hash(parentPathHash);
+ const Zstring& itemName = fsObj.getItemName<side>();
+
+ if (isAsciiString(itemName)) //fast path: no need for extra memory allocation!
+ for (const Zchar c : itemName)
+ hash.add(asciiToUpper(c));
+ else
+ for (const Zchar c : getUpperCase(itemName))
+ hash.add(c);
+
+ return hash.get();
+ }
+
+ std::vector<ChildPathRef> childPathRefs_;
+};
+
+
+template <SelectSide side>
+bool plannedWriteAccess(const FileSystemObject& fsObj)
+{
+ if (std::optional<SelectSide> dir = getTargetDirection(fsObj.getSyncOperation()))
+ return side == *dir;
+ else
+ return false;
+}
+
+
+template <SelectSide sideL, SelectSide sideR>
+std::weak_ordering comparePathRef(const ChildPathRef& lhs, const ChildPathRef& rhs)
+{
+ if (const std::weak_ordering cmp = lhs.childPathHash <=> rhs.childPathHash;
+ cmp != std::weak_ordering::equivalent)
+ return cmp; //fast path!
+
+ return compareNoCase(lhs.fsObj->getAbstractPath<sideL>().afsPath.value, //fsObj may come from *different* BaseFolderPair
+ rhs.fsObj->getAbstractPath<sideR>().afsPath.value); // => don't compare getRelativePath()!
+}
+
+
+template <SelectSide side>
+void sortAndRemoveDuplicates(std::vector<ChildPathRef>& pathRefs)
+{
+ std::sort(pathRefs.begin(), pathRefs.end(), [](const ChildPathRef& lhs, const ChildPathRef& rhs)
+ {
+ if (const std::weak_ordering cmp = comparePathRef<side, side>(lhs, rhs);
+ cmp != std::weak_ordering::equivalent)
+ return cmp < 0;
+
+ return //multiple (case-insensitive) relPaths? => order write-access before read-access, so that std::unique leaves a write if existing!
+ plannedWriteAccess<side>(*lhs.fsObj) >
+ plannedWriteAccess<side>(*rhs.fsObj);
+ });
+
+ pathRefs.erase(std::unique(pathRefs.begin(), pathRefs.end(),
+ [](const ChildPathRef& lhs, const ChildPathRef& rhs) { return comparePathRef<side, side>(lhs, rhs) == std::weak_ordering::equivalent; }),
+ pathRefs.end());
+
+ //let's not use removeDuplicates(): we rely too much on implementation details!
+}
+
+template <SelectSide side>
+std::wstring formatRaceItem(const FileSystemObject& fsObj)
+{
+ return AFS::getDisplayPath(fsObj.base().getAbstractPath<side>()) + (plannedWriteAccess<side>(fsObj) ? L" 💾 " : L" 👓 " ) +
+ utfTo<std::wstring>(fsObj.getRelativePath<side>()); //e.g. C:\Folder 💾 subfolder\file.txt
+}
+
+struct PathRaceCondition
+{
+ std::wstring itemList;
+ size_t count = 0;
+};
+
+
+template <SelectSide side1, SelectSide side2>
+void getChildItemRaceCondition(std::vector<ChildPathRef>& pathRefs1, std::vector<ChildPathRef>& pathRefs2, PathRaceCondition& result)
+{
+ //use case-sensitive comparison because items were scanned by FFS (=> no messy user input)?
+ //not good enough! E.g. not-yet-existing files are set to be created with different case!
+ // + (weird) a file and a folder are set to be created with same name
+ // => (throw hands in the air) fine, check path only and don't consider case
+
+ sortAndRemoveDuplicates<side1>(pathRefs1);
+ sortAndRemoveDuplicates<side2>(pathRefs2);
+
+ mergeTraversal(pathRefs1.begin(), pathRefs1.end(),
+ pathRefs2.begin(), pathRefs2.end(),
+ [](const ChildPathRef&) {} /*left only*/,
+ [&](const ChildPathRef& lhs, const ChildPathRef& rhs)
+ {
+ if (plannedWriteAccess<side1>(*lhs.fsObj) ||
+ plannedWriteAccess<side2>(*rhs.fsObj))
+ {
+ if (result.count < CONFLICTS_PREVIEW_MAX)
+ result.itemList += formatRaceItem<side1>(*lhs.fsObj) + L"\n" +
+ formatRaceItem<side2>(*rhs.fsObj) + L"\n\n";
+ ++result.count;
+ }
+ },
+ [](const ChildPathRef&) {} /*right only*/, comparePathRef<side1, side2>);
+}
+
+
+//check if some files/folders are included more than once and form a race condition (:= multiple accesses of which at least one is a write)
+// - checking filter for subfolder exclusion is not good enough: one folder may have a *.txt include-filter, the other a *.lng include filter => still no dependencies
+// - user may have manually excluded the conflicting items or changed the filter settings without running a re-compare
+template <SelectSide sideP, SelectSide sideC>
+void getPathRaceCondition(const BaseFolderPair& baseFolderP, const BaseFolderPair& baseFolderC, PathRaceCondition& result)
+{
+ const AbstractPath basePathP = baseFolderP.getAbstractPath<sideP>(); //parent/child notion is tentative at this point
+ const AbstractPath basePathC = baseFolderC.getAbstractPath<sideC>(); //=> will be swapped if necessary
+
+ if (!AFS::isNullPath(basePathP) && !AFS::isNullPath(basePathC))
+ if (basePathP.afsDevice == basePathC.afsDevice)
+ {
+ if (basePathP.afsPath.value.size() > basePathC.afsPath.value.size())
+ return getPathRaceCondition<sideC, sideP>(baseFolderC, baseFolderP, result);
+
+ const std::vector<Zstring> relPathP = split(basePathP.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
+ const std::vector<Zstring> relPathC = split(basePathC.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip);
+
+ if (relPathP.size() <= relPathC.size() &&
+ /**/std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalNoCase(lhs, rhs); }))
+ {
+ //=> at this point parent/child folders are confirmed
+ //now find child folder match inside baseFolderP
+ //e.g. C:\folder <-> C:\folder\sub => find "sub" inside C:\folder
+ std::vector<const ContainerObject*> childFolderP{&baseFolderP};
+
+ std::for_each(relPathC.begin() + relPathP.size(), relPathC.end(), [&](const Zstring& itemName)
+ {
+ std::vector<const ContainerObject*> childFolderP2;
+
+ for (const ContainerObject* childFolder : childFolderP)
+ for (const FolderPair& folder : childFolder->refSubFolders())
+ if (equalNoCase(folder.getItemName<sideP>(), itemName))
+ childFolderP2.push_back(&folder);
+ //no "break"? yes, weird, but there could be more than one (for case-sensitive file system)
+
+ childFolderP = std::move(childFolderP2);
+ });
+
+ std::vector<ChildPathRef> pathRefsP;
+ for (const ContainerObject* childFolder : childFolderP)
+ append(pathRefsP, GetChildPathsHashed<sideP>::execute(*childFolder));
+
+ std::vector<ChildPathRef> pathRefsC = GetChildPathsHashed<sideC>::execute(baseFolderC);
+
+ getChildItemRaceCondition<sideP, sideC>(pathRefsP, pathRefsC, result);
+ }
+ }
+}
+
//#################################################################################################################
//--------------------- data verification -------------------------
@@ -2227,11 +2429,11 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//-------------------execute basic checks all at once BEFORE starting sync--------------------------------------
- std::vector<int /*we really want bool*/> skipFolderPair(folderCmp.size(), false); //folder pairs may be skipped after fatal errors were found
+ std::vector<unsigned char /*we really want bool*/> skipFolderPair(folderCmp.size(), false); //folder pairs may be skipped after fatal errors were found
- std::map<const BaseFolderPair*, std::pair<int, std::vector<SyncStatistics::ConflictInfo>>> checkUnresolvedConflicts;
+ std::vector<std::tuple<const BaseFolderPair*, int /*conflict count*/, std::vector<SyncStatistics::ConflictInfo>>> checkUnresolvedConflicts;
- std::vector<std::tuple<AbstractPath, const PathFilter*, bool /*write access*/>> checkReadWriteBaseFolders;
+ std::vector<std::tuple<const BaseFolderPair*, SelectSide, bool /*write access*/>> checkBaseFolderRaceCondition;
std::vector<std::pair<AbstractPath, AbstractPath>> checkSignificantDiffPairs;
@@ -2246,17 +2448,17 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
std::set<AbstractPath> checkVersioningLimitPaths;
//------------------- start checking folder pairs -------------------
- for (auto itBase = begin(folderCmp); itBase != end(folderCmp); ++itBase)
+ for (size_t folderIndex = 0; folderIndex < folderCmp.size(); ++folderIndex)
{
- BaseFolderPair& baseFolder = *itBase;
- const size_t folderIndex = itBase - begin(folderCmp);
- const FolderPairSyncCfg& folderPairCfg = syncConfig [folderIndex];
+ BaseFolderPair& baseFolder = *folderCmp[folderIndex];
+ const FolderPairSyncCfg& folderPairCfg = syncConfig[folderIndex];
const SyncStatistics& folderPairStat = folderPairStats[folderIndex];
const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase);
- //aggregate *all* conflicts:
- checkUnresolvedConflicts[&baseFolder] = std::pair(folderPairStat.conflictCount(), folderPairStat.getConflictsPreview());
+ //prepare conflict preview:
+ if (folderPairStat.conflictCount() > 0)
+ checkUnresolvedConflicts.emplace_back(&baseFolder, folderPairStat.conflictCount(), folderPairStat.getConflictsPreview());
//consider *all* paths that might be used during versioning limit at some time
if (folderPairCfg.handleDeletion == DeletionPolicy::versioning &&
@@ -2268,7 +2470,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//================ begin of checks that may SKIP folder pairs ===================
//===============================================================================
- //exclude a few pathological cases (including empty left, right folders)
+ //exclude a few pathological cases:
if (baseFolder.getAbstractPath<SelectSide::left >() ==
baseFolder.getAbstractPath<SelectSide::right>())
{
@@ -2311,7 +2513,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
continue;
}
- //allow propagation of deletions only from *null-* or *existing* source folder:
+ //allow propagation of deletions only from *empty* or *existing* source folder:
auto sourceFolderMissing = [&](const AbstractPath& baseFolderPath, BaseFolderStatus folderStatus) //we need to evaluate existence status from time of comparison!
{
if (!AFS::isNullPath(baseFolderPath))
@@ -2349,13 +2551,13 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//prepare: check if versioning path itself will be synchronized (and was not excluded via filter)
checkVersioningPaths.insert(versioningFolderPath);
- checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath<SelectSide::left >(), &baseFolder.getFilter());
- checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath<SelectSide::right>(), &baseFolder.getFilter());
}
+ checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath<SelectSide::left >(), &baseFolder.getFilter());
+ checkVersioningBasePaths.emplace_back(baseFolder.getAbstractPath<SelectSide::right>(), &baseFolder.getFilter());
- //prepare: check if folders are used by multiple pairs in read/write access
- checkReadWriteBaseFolders.emplace_back(baseFolder.getAbstractPath<SelectSide::left >(), &baseFolder.getFilter(), writeLeft);
- checkReadWriteBaseFolders.emplace_back(baseFolder.getAbstractPath<SelectSide::right>(), &baseFolder.getFilter(), writeRight);
+ //prepare: check if some files are used by multiple pairs in read/write access
+ checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::left, writeLeft);
+ checkBaseFolderRaceCondition.emplace_back(&baseFolder, SelectSide::right, writeRight);
//check if more than 50% of total number of files/dirs are to be created/overwritten/deleted
if (!AFS::isNullPath(baseFolder.getAbstractPath<SelectSide::left >()) &&
@@ -2412,29 +2614,51 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
checkRecycler(baseFolder.getAbstractPath<SelectSide::right>());
}
}
- //-----------------------------------------------------------------
+ //--------------------------------------------------------------------------------------
//check if unresolved conflicts exist
- if (std::any_of(checkUnresolvedConflicts.begin(), checkUnresolvedConflicts.end(), [](const auto& item) { return item.second.first > 0; }))
+ if (!checkUnresolvedConflicts.empty())
{
- std::wstring msg = _("The following items have unresolved conflicts and will not be synchronized:");
+ //distribute CONFLICTS_PREVIEW_MAX over all pairs, not *per* pair, or else log size with many folder pairs can blow up!
+ std::vector<std::vector<SyncStatistics::ConflictInfo>> conflictPreviewTrim(checkUnresolvedConflicts.size());
- for (const auto& [baseFolder, conflicts] : checkUnresolvedConflicts)
+ size_t previewRemain = CONFLICTS_PREVIEW_MAX;
+ for (size_t i = 0; ; ++i)
{
- const auto& [conflictCount, conflictPreview] = conflicts;
- if (conflictCount > 0)
- {
- msg += L"\n\n" + _("Folder pair:") + L' ' +
- AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::left >()) + L" <-> " +
- AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::right>());
+ const size_t previewRemainOld = previewRemain;
- for (const SyncStatistics::ConflictInfo& item : conflictPreview)
- msg += L'\n' + utfTo<std::wstring>(item.relPath) + L": " + item.msg;
+ for (size_t j = 0; j < checkUnresolvedConflicts.size(); ++j)
+ {
+ const auto& [baseFolder, conflictCount, conflictPreview] = checkUnresolvedConflicts[j];
- if (makeUnsigned(conflictCount) > conflictPreview.size())
- msg += L"\n [...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", conflictCount), //%x used as plural form placeholder!
- L"%y", formatNumber(conflictPreview.size()));
+ if (i < conflictPreview.size())
+ {
+ conflictPreviewTrim[j].push_back(conflictPreview[i]);
+ if (--previewRemain == 0)
+ goto break2; //sigh
+ }
}
+ if (previewRemain == previewRemainOld)
+ break;
+ }
+break2:
+
+ std::wstring msg = _("The following items have unresolved conflicts and will not be synchronized:");
+
+ auto itPrevi = conflictPreviewTrim.begin();
+ for (const auto& [baseFolder, conflictCount, conflictPreview] : checkUnresolvedConflicts)
+ {
+ msg += L"\n\n" + _("Folder pair:") + L' ' +
+ AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::left >()) + L" <-> " +
+ AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::right>());
+
+ for (const SyncStatistics::ConflictInfo& item : *itPrevi)
+ msg += L'\n' + utfTo<std::wstring>(item.relPath) + L": " + item.msg;
+
+ if (makeUnsigned(conflictCount) > itPrevi->size())
+ msg += L"\n [...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", conflictCount), //%x used as plural form placeholder!
+ L"%y", formatNumber(itPrevi->size()));
+ ++itPrevi;
}
callback.reportWarning(msg, warnings.warnUnresolvedConflicts);
@@ -2460,8 +2684,8 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
for (const auto& [folderPath, space] : checkDiskSpaceMissing)
msg += L"\n\n" + AFS::getDisplayPath(folderPath) + L'\n' +
- _("Required:") + L' ' + formatFilesizeShort(space.first) + L'\n' +
- _("Available:") + L' ' + formatFilesizeShort(space.second);
+ TAB_SPACE + _("Required:") + L' ' + formatFilesizeShort(space.first) + L'\n' +
+ TAB_SPACE + _("Available:") + L' ' + formatFilesizeShort(space.second);
callback.reportWarning(msg, warnings.warnNotEnoughDiskSpace);
}
@@ -2480,35 +2704,41 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//check if folders are used by multiple pairs in read/write access
{
- std::set<AbstractPath> dependentFolders;
+ PathRaceCondition conflicts;
//race condition := multiple accesses of which at least one is a write
- for (auto it = checkReadWriteBaseFolders.begin(); it != checkReadWriteBaseFolders.end(); ++it)
- {
- const auto& [basePath1, filter1, writeAccess1] = *it;
- if (writeAccess1)
- for (auto it2 = checkReadWriteBaseFolders.begin(); it2 != checkReadWriteBaseFolders.end(); ++it2)
+ //=> use "writeAccess" to reduce list of - not necessarily conflicting - candidates to check (=> perf!)
+ for (auto it = checkBaseFolderRaceCondition.begin(); it != checkBaseFolderRaceCondition.end(); ++it)
+ if (const auto& [baseFolder1, side1, writeAccess1] = *it;
+ writeAccess1)
+ for (auto it2 = checkBaseFolderRaceCondition.begin(); it2 != checkBaseFolderRaceCondition.end(); ++it2)
{
- const auto& [basePath2, filter2, writeAccess2] = *it2;
+ const auto& [baseFolder2, side2, writeAccess2] = *it2;
if (!writeAccess2 ||
it < it2) //avoid duplicate comparisons
- if (std::optional<PathDependency> pd = getPathDependency(basePath1, *filter1,
- basePath2, *filter2))
- {
- dependentFolders.insert(pd->basePathParent);
- dependentFolders.insert(pd->basePathChild);
- }
+ {
+ //"The Things We Do for [Perf]"
+ /**/ if (side1 == SelectSide::left && side2 == SelectSide::left ) getPathRaceCondition<SelectSide::left, SelectSide::left >(*baseFolder1, *baseFolder2, conflicts);
+ else if (side1 == SelectSide::left && side2 == SelectSide::right) getPathRaceCondition<SelectSide::left, SelectSide::right>(*baseFolder1, *baseFolder2, conflicts);
+ else if (side1 == SelectSide::right && side2 == SelectSide::left ) getPathRaceCondition<SelectSide::right, SelectSide::left >(*baseFolder1, *baseFolder2, conflicts);
+ else getPathRaceCondition<SelectSide::right, SelectSide::right>(*baseFolder1, *baseFolder2, conflicts);
+ }
}
- }
+ assert(makeUnsigned(std::count(conflicts.itemList.begin(), conflicts.itemList.end(), L'\n')) == 3 * std::min(conflicts.count, CONFLICTS_PREVIEW_MAX));
- if (!dependentFolders.empty())
+ if (conflicts.count > 0)
{
std::wstring msg = _("Some files will be synchronized as part of multiple base folders.") + L'\n' +
- _("To avoid conflicts, set up exclude filters so that each updated file is included by only one base folder.") + L'\n';
+ _("To avoid conflicts, set up exclude filters so that each updated file is included by only one base folder.") + L"\n\n" +
+ conflicts.itemList;
- for (const AbstractPath& baseFolderPath : dependentFolders)
- msg += L'\n' + AFS::getDisplayPath(baseFolderPath);
+ assert(endsWith(conflicts.itemList, L"\n\n"));
+ if (conflicts.count > CONFLICTS_PREVIEW_MAX)
+ msg += L"[...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", conflicts.count), //%x used as plural form placeholder!
+ L"%y", formatNumber(CONFLICTS_PREVIEW_MAX));
+ else
+ trim(msg);
callback.reportWarning(msg, warnings.warnDependentBaseFolders);
}
@@ -2517,26 +2747,34 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//check if versioning path itself will be synchronized (and was not excluded via filter)
{
std::wstring msg;
+ bool shouldExclude = false;
+
for (const AbstractPath& versioningFolderPath : checkVersioningPaths)
{
- std::map<AbstractPath, std::wstring> uniqueMsgs; //=> at most one msg per base folder (*and* per versioningFolderPath)
+ std::set<AbstractPath> foldersWithWarnings; //=> at most one msg per base folder (*and* per versioningFolderPath)
for (const auto& [folderPath, filter] : checkVersioningBasePaths) //may contain duplicate paths, but with *different* hard filter!
if (std::optional<PathDependency> pd = getPathDependency(versioningFolderPath, NullFilter(), folderPath, *filter))
- {
- std::wstring line = L"\n\n" + _("Versioning folder:") + L" \t" + AFS::getDisplayPath(versioningFolderPath) +
- L'\n' + _("Base folder:") + L" \t" + AFS::getDisplayPath(folderPath);
- if (pd->basePathParent == folderPath && !pd->relPath.empty())
- line += L'\n' + _("Exclude:") + L" \t" + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR);
-
- uniqueMsgs[folderPath] = line;
- }
- for (const auto& [folderPath, perFolderMsg] : uniqueMsgs)
- msg += perFolderMsg;
+ if (const auto [it, inserted] = foldersWithWarnings.insert(folderPath);
+ inserted)
+ {
+ msg += L"\n\n" +
+ _("Base folder:") + L" \t" + AFS::getDisplayPath(folderPath) + L'\n' +
+ _("Versioning folder:") + L" \t" + AFS::getDisplayPath(versioningFolderPath);
+ if (pd->folderPathParent == folderPath) //else: probably fine? :>
+ if (!pd->relPath.empty())
+ {
+ shouldExclude = true;
+ msg += std::wstring() + L'\n' + L"⇒ " +
+ _("Exclude:") + L" \t" + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR);
+ }
+ warn_static("else: ???")
+ }
}
if (!msg.empty())
- callback.reportWarning(_("The versioning folder is contained in a base folder.") + L'\n' +
- _("The folder should be excluded from synchronization via filter.") + msg, warnings.warnVersioningFolderPartOfSync);
+ callback.reportWarning(_("The versioning folder is contained in a base folder.") +
+ (shouldExclude ? L'\n' + _("The folder should be excluded from synchronization via filter.") : L"") +
+ msg, warnings.warnVersioningFolderPartOfSync);
}
//warn if versioning folder paths differ only in case => possible pessimization for applyVersioningLimit()
@@ -2558,6 +2796,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
}
callback.reportWarning(msg, warnings.warnFoldersDifferInCase); //throw X
}
+ //what about /folder and /Folder/subfolder? => yes, inconsistent, but doesn't matter for FFS
}
//-------------------end of basic checks------------------------------------------
@@ -2622,11 +2861,10 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
try
{
//loop through all directory pairs
- for (auto itBase = begin(folderCmp); itBase != end(folderCmp); ++itBase)
+ for (size_t folderIndex = 0; folderIndex < folderCmp.size(); ++folderIndex)
{
- BaseFolderPair& baseFolder = *itBase;
- const size_t folderIndex = itBase - begin(folderCmp);
- const FolderPairSyncCfg& folderPairCfg = syncConfig [folderIndex];
+ BaseFolderPair& baseFolder = *folderCmp[folderIndex];
+ const FolderPairSyncCfg& folderPairCfg = syncConfig[folderIndex];
const SyncStatistics& folderPairStat = folderPairStats[folderIndex];
if (skipFolderPair[folderIndex]) //folder pairs may be skipped after fatal errors were found
@@ -2634,8 +2872,8 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//------------------------------------------------------------------------------------------
callback.logInfo(_("Synchronizing folder pair:") + L' ' + getVariantNameWithSymbol(folderPairCfg.syncVar) + L'\n' + //throw X
- L" " + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::left >()) + L'\n' +
- L" " + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::right>()));
+ TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::left >()) + L'\n' +
+ TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::right>()));
//------------------------------------------------------------------------------------------
//checking a second time: 1. a long time may have passed since syncing the previous folder pairs!
@@ -2734,15 +2972,15 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//(try to gracefully) write database file
if (folderPairCfg.saveSyncDB)
{
- saveLastSynchronousState(baseFolder, failSafeFileCopy, //throw X
- callback /*throw X*/);
- guardDbSave.dismiss(); //[!] after "graceful" try: user might have cancelled during DB write: ensure DB is still written
+ saveLastSynchronousState(baseFolder, failSafeFileCopy,
+ callback /*throw X*/); //throw X
+ guardDbSave.dismiss(); //[!] dismiss *after* "graceful" try: user might cancel during DB write: ensure DB is still written
}
}
//-----------------------------------------------------------------------------------------------------
applyVersioningLimit(versionLimitFolders,
- callback /*throw X*/);
+ callback /*throw X*/); //throw X
}
catch (const std::exception& e)
{
diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp
index 6627b242..1a17c6b2 100644
--- a/FreeFileSync/Source/base/versioning.cpp
+++ b/FreeFileSync/Source/base/versioning.cpp
@@ -399,7 +399,7 @@ std::weak_ordering fff::operator<=>(const VersioningLimitFolder& lhs, const Vers
void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimits,
- PhaseCallback& callback /*throw X*/)
+ PhaseCallback& callback /*throw X*/) //throw X
{
//--------- determine existing folder paths for traversal ---------
std::set<DirectoryKey> foldersToRead;
@@ -423,7 +423,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& folderLimi
false /*allowUserInteraction*/, callback); //throw X
foldersToRead.clear();
for (const AbstractPath& folderPath : status.existing)
- foldersToRead.insert(DirectoryKey({folderPath, makeSharedRef<NullFilter>(), SymLinkHandling::direct}));
+ foldersToRead.insert(DirectoryKey({folderPath, makeSharedRef<NullFilter>(), SymLinkHandling::asLink}));
if (!status.failedChecks.empty())
{
diff --git a/FreeFileSync/Source/base_tools.cpp b/FreeFileSync/Source/base_tools.cpp
index dd6f3f11..e642463a 100644
--- a/FreeFileSync/Source/base_tools.cpp
+++ b/FreeFileSync/Source/base_tools.cpp
@@ -59,25 +59,25 @@ void fff::logNonDefaultSettings(const XmlGlobalSettings& activeSettings, PhaseCa
std::wstring changedSettingsMsg;
if (activeSettings.failSafeFileCopy != defaultSettings.failSafeFileCopy)
- changedSettingsMsg += L"\n " + _("Fail-safe file copy") + L" - " + (activeSettings.failSafeFileCopy ? _("Enabled") : _("Disabled"));
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("Fail-safe file copy")) + L" - " + (activeSettings.failSafeFileCopy ? _("Enabled") : _("Disabled"));
if (activeSettings.copyLockedFiles != defaultSettings.copyLockedFiles)
- changedSettingsMsg += L"\n " + _("Copy locked files") + L" - " + (activeSettings.copyLockedFiles ? _("Enabled") : _("Disabled"));
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("Copy locked files")) + L" - " + (activeSettings.copyLockedFiles ? _("Enabled") : _("Disabled"));
if (activeSettings.copyFilePermissions != defaultSettings.copyFilePermissions)
- changedSettingsMsg += L"\n " + _("Copy file access permissions") + L" - " + (activeSettings.copyFilePermissions ? _("Enabled") : _("Disabled"));
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("Copy file access permissions")) + L" - " + (activeSettings.copyFilePermissions ? _("Enabled") : _("Disabled"));
if (activeSettings.fileTimeTolerance != defaultSettings.fileTimeTolerance)
- changedSettingsMsg += L"\n " + _("File time tolerance") + L" - " + numberTo<std::wstring>(activeSettings.fileTimeTolerance);
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("File time tolerance")) + L" - " + numberTo<std::wstring>(activeSettings.fileTimeTolerance);
if (activeSettings.runWithBackgroundPriority != defaultSettings.runWithBackgroundPriority)
- changedSettingsMsg += L"\n " + _("Run with background priority") + L" - " + (activeSettings.runWithBackgroundPriority ? _("Enabled") : _("Disabled"));
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("Run with background priority")) + L" - " + (activeSettings.runWithBackgroundPriority ? _("Enabled") : _("Disabled"));
if (activeSettings.createLockFile != defaultSettings.createLockFile)
- changedSettingsMsg += L"\n " + _("Lock directories during sync") + L" - " + (activeSettings.createLockFile ? _("Enabled") : _("Disabled"));
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("Lock directories during sync")) + L" - " + (activeSettings.createLockFile ? _("Enabled") : _("Disabled"));
if (activeSettings.verifyFileCopy != defaultSettings.verifyFileCopy)
- changedSettingsMsg += L"\n " + _("Verify copied files") + L" - " + (activeSettings.verifyFileCopy ? _("Enabled") : _("Disabled"));
+ changedSettingsMsg += L"\n" + (TAB_SPACE + _("Verify copied files")) + L" - " + (activeSettings.verifyFileCopy ? _("Enabled") : _("Disabled"));
if (!changedSettingsMsg.empty())
callback.logInfo(_("Using non-default global settings:") + changedSettingsMsg); //throw X
diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp
index 4c4bdc66..21bb4e0c 100644
--- a/FreeFileSync/Source/config.cpp
+++ b/FreeFileSync/Source/config.cpp
@@ -23,7 +23,7 @@ using namespace fff; //required for correct overload resolution!
namespace
{
//-------------------------------------------------------------------------------------------------------------------------------
-const int XML_FORMAT_GLOBAL_CFG = 24; //2022-04-29
+const int XML_FORMAT_GLOBAL_CFG = 25; //2022-08-26
const int XML_FORMAT_SYNC_CFG = 17; //2020-10-14
//-------------------------------------------------------------------------------------------------------------------------------
}
@@ -39,53 +39,6 @@ const ExternalApp fff::extCommandOpenDefault
{L"Open with default application", "xdg-open \"%local_path%\""};
-XmlType getXmlTypeNoThrow(const XmlDoc& doc) //noexcept
-{
- if (doc.root().getName() == "FreeFileSync")
- {
- std::string type;
- if (doc.root().getAttribute("XmlType", type))
- {
- if (type == "GUI")
- return XmlType::gui;
- else if (type == "BATCH")
- return XmlType::batch;
- else if (type == "GLOBAL")
- return XmlType::global;
- }
- }
- return XmlType::other;
-}
-
-
-XmlType fff::getXmlType(const Zstring& filePath) //throw FileError
-{
- //quick exit if file is not an XML
- XmlDoc doc = loadXml(filePath); //throw FileError
- return ::getXmlTypeNoThrow(doc);
-}
-
-
-void setXmlType(XmlDoc& doc, XmlType type) //throw()
-{
- switch (type)
- {
- case XmlType::gui:
- doc.root().setAttribute("XmlType", "GUI");
- break;
- case XmlType::batch:
- doc.root().setAttribute("XmlType", "BATCH");
- break;
- case XmlType::global:
- doc.root().setAttribute("XmlType", "GLOBAL");
- break;
- case XmlType::other:
- assert(false);
- break;
- }
-}
-
-
XmlGlobalSettings::XmlGlobalSettings() :
@@ -436,7 +389,7 @@ void writeText(const SymLinkHandling& value, std::string& output)
case SymLinkHandling::exclude:
output = "Exclude";
break;
- case SymLinkHandling::direct:
+ case SymLinkHandling::asLink:
output = "Direct";
break;
case SymLinkHandling::follow:
@@ -452,7 +405,7 @@ bool readText(const std::string& input, SymLinkHandling& value)
if (tmp == "Exclude")
value = SymLinkHandling::exclude;
else if (tmp == "Direct")
- value = SymLinkHandling::direct;
+ value = SymLinkHandling::asLink;
else if (tmp == "Follow")
value = SymLinkHandling::follow;
else
@@ -1271,32 +1224,32 @@ void readConfig(const XmlIn& in, LocalPairConfig& lpc, std::map<AfsDevice, size_
void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer)
{
- XmlIn inMain = formatVer < 10 ? in["MainConfig"] : in; //TODO: remove if parameter migration after some time! 2018-02-25
+ XmlIn in2 = formatVer < 10 ? in["MainConfig"] : in; //TODO: remove if parameter migration after some time! 2018-02-25
if (formatVer < 10) //TODO: remove if parameter migration after some time! 2018-02-25
- readConfig(inMain["Comparison"], mainCfg.cmpCfg);
+ readConfig(in2["Comparison"], mainCfg.cmpCfg);
else
- readConfig(inMain["Compare"], mainCfg.cmpCfg);
+ readConfig(in2["Compare"], mainCfg.cmpCfg);
//###########################################################
//read sync configuration
if (formatVer < 10) //TODO: remove if parameter migration after some time! 2018-02-25
- readConfig(inMain["SyncConfig"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer);
+ readConfig(in2["SyncConfig"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer);
else
- readConfig(inMain["Synchronize"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer);
+ readConfig(in2["Synchronize"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer);
//###########################################################
//read filter settings
if (formatVer < 10) //TODO: remove if parameter migration after some time! 2018-02-25
- readConfig(inMain["GlobalFilter"], mainCfg.globalFilter);
+ readConfig(in2["GlobalFilter"], mainCfg.globalFilter);
else
- readConfig(inMain["Filter"], mainCfg.globalFilter);
+ readConfig(in2["Filter"], mainCfg.globalFilter);
//###########################################################
//read folder pairs
bool firstItem = true;
- for (XmlIn inPair = inMain["FolderPairs"]["Pair"]; inPair; inPair.next())
+ for (XmlIn inPair = in2["FolderPairs"]["Pair"]; inPair; inPair.next())
{
LocalPairConfig lpc;
readConfig(inPair, lpc, mainCfg.deviceParallelOps, formatVer);
@@ -1317,28 +1270,28 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer)
else
//TODO: remove if parameter migration after some time! 2018-02-24
if (formatVer < 10)
- inMain["IgnoreErrors"](mainCfg.ignoreErrors);
+ in2["IgnoreErrors"](mainCfg.ignoreErrors);
else
{
- inMain["Errors"].attribute("Ignore", mainCfg.ignoreErrors);
- inMain["Errors"].attribute("Retry", mainCfg.autoRetryCount);
- inMain["Errors"].attribute("Delay", mainCfg.autoRetryDelay);
+ in2["Errors"].attribute("Ignore", mainCfg.ignoreErrors);
+ in2["Errors"].attribute("Retry", mainCfg.autoRetryCount);
+ in2["Errors"].attribute("Delay", mainCfg.autoRetryDelay);
}
//TODO: remove if parameter migration after some time! 2017-10-24
if (formatVer < 8)
- inMain["OnCompletion"](mainCfg.postSyncCommand);
+ in2["OnCompletion"](mainCfg.postSyncCommand);
else
{
- inMain["PostSyncCommand"](mainCfg.postSyncCommand);
- inMain["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
+ in2["PostSyncCommand"](mainCfg.postSyncCommand);
+ in2["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
}
//TODO: remove if parameter migration after some time! 2018-08-13
if (formatVer < 14)
; //path will be extracted from BatchExclusiveConfig
else
- inMain["LogFolder"](mainCfg.altLogFolderPathPhrase);
+ in2["LogFolder"](mainCfg.altLogFolderPathPhrase);
//TODO: remove after migration! 2020-04-24
if (formatVer < 16)
@@ -1349,8 +1302,8 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer)
;
else
{
- inMain["EmailNotification"](mainCfg.emailNotifyAddress);
- inMain["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
+ in2["EmailNotification"](mainCfg.emailNotifyAddress);
+ in2["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
}
}
@@ -1594,6 +1547,10 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
inOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync);
}
+ //TODO: remove after migration! 2022-08-26
+ if (formatVer < 25)
+ cfg.warnDlgs.warnDependentBaseFolders = true; //new semantics! should not be ignored
+
//TODO: remove after migration! 2021-12-02
if (formatVer < 23)
{
@@ -2100,11 +2057,21 @@ std::pair<ConfigType, std::wstring /*warningMsg*/> parseConfig(const XmlDoc& doc
template <class ConfigType>
-std::pair<ConfigType, std::wstring /*warningMsg*/> readConfig(const Zstring& filePath, XmlType type, int currentXmlFormatVer) //throw FileError
+std::pair<ConfigType, std::wstring /*warningMsg*/> readConfig(const Zstring& filePath, const char* expectedCfgType, int currentXmlFormatVer) //throw FileError
{
XmlDoc doc = loadXml(filePath); //throw FileError
- if (getXmlTypeNoThrow(doc) != type) //noexcept
+ const std::string cfgType =[&]
+ {
+ if (doc.root().getName() == "FreeFileSync")
+ {
+ std::string type;
+ if (doc.root().getAttribute("XmlType", type))
+ return type;
+ }
+ return std::string();
+ }();
+ if (cfgType != expectedCfgType)
throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
return parseConfig<ConfigType>(doc, filePath, currentXmlFormatVer);
@@ -2114,19 +2081,19 @@ std::pair<ConfigType, std::wstring /*warningMsg*/> readConfig(const Zstring& fil
std::pair<XmlGuiConfig, std::wstring /*warningMsg*/> fff::readGuiConfig(const Zstring& filePath)
{
- return readConfig<XmlGuiConfig>(filePath, XmlType::gui, XML_FORMAT_SYNC_CFG); //throw FileError
+ return readConfig<XmlGuiConfig>(filePath, "GUI", XML_FORMAT_SYNC_CFG); //throw FileError
}
std::pair<XmlBatchConfig, std::wstring /*warningMsg*/> fff::readBatchConfig(const Zstring& filePath)
{
- return readConfig<XmlBatchConfig>(filePath, XmlType::batch, XML_FORMAT_SYNC_CFG); //throw FileError
+ return readConfig<XmlBatchConfig>(filePath, "BATCH", XML_FORMAT_SYNC_CFG); //throw FileError
}
std::pair<XmlGlobalSettings, std::wstring /*warningMsg*/> fff::readGlobalConfig(const Zstring& filePath)
{
- return readConfig<XmlGlobalSettings>(filePath, XmlType::global, XML_FORMAT_GLOBAL_CFG); //throw FileError
+ return readConfig<XmlGlobalSettings>(filePath, "GLOBAL", XML_FORMAT_GLOBAL_CFG); //throw FileError
}
@@ -2140,41 +2107,31 @@ std::pair<XmlGuiConfig, std::wstring /*warningMsg*/> fff::readAnyConfig(const st
for (auto it = filePaths.begin(); it != filePaths.end(); ++it)
{
- const Zstring& filePath = *it;
const bool firstItem = it == filePaths.begin(); //init all non-"mainCfg" settings with first config file
+ const Zstring& filePath = *it;
- XmlDoc doc = loadXml(filePath); //throw FileError
-
- switch (getXmlTypeNoThrow(doc))
+ if (endsWithAsciiNoCase(filePath, Zstr(".ffs_gui")))
{
- case XmlType::gui:
- {
- const auto& [guiCfg, warningMsg] = parseConfig<XmlGuiConfig>(doc, filePath, XML_FORMAT_SYNC_CFG); //nothrow
- if (firstItem)
- cfg = guiCfg;
- mainCfgs.push_back(guiCfg.mainCfg);
-
- if (!warningMsg.empty())
- warningMsgAll += warningMsg + L"\n\n";
- }
- break;
+ const auto& [guiCfg, warningMsg] = readGuiConfig(filePath); //throw FileError
+ if (firstItem)
+ cfg = guiCfg;
+ mainCfgs.push_back(guiCfg.mainCfg);
- case XmlType::batch:
- {
- const auto& [batchCfg, warningMsg] = parseConfig<XmlBatchConfig>(doc, filePath, XML_FORMAT_SYNC_CFG); //nothrow
- if (firstItem)
- cfg = convertBatchToGui(batchCfg);
- mainCfgs.push_back(batchCfg.mainCfg);
-
- if (!warningMsg.empty())
- warningMsgAll += warningMsg + L"\n\n";
- }
- break;
+ if (!warningMsg.empty())
+ warningMsgAll += warningMsg + L"\n\n";
+ }
+ else if (endsWithAsciiNoCase(filePath, Zstr(".ffs_batch")))
+ {
+ const auto& [batchCfg, warningMsg] = readBatchConfig(filePath); //throw FileError
+ if (firstItem)
+ cfg = convertBatchToGui(batchCfg);
+ mainCfgs.push_back(batchCfg.mainCfg);
- case XmlType::global:
- case XmlType::other:
- throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
+ if (!warningMsg.empty())
+ warningMsgAll += warningMsg + L"\n\n";
}
+ else
+ throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath)));
}
cfg.mainCfg = merge(mainCfgs);
@@ -2293,41 +2250,36 @@ void writeConfig(const LocalPairConfig& lpc, const std::map<AfsDevice, size_t>&
void writeConfig(const MainConfiguration& mainCfg, XmlOut& out)
{
- XmlOut outMain = out;
-
- XmlOut outCmp = outMain["Compare"];
-
+ XmlOut outCmp = out["Compare"];
writeConfig(mainCfg.cmpCfg, outCmp);
//###########################################################
- XmlOut outSync = outMain["Synchronize"];
-
+ XmlOut outSync = out["Synchronize"];
writeConfig(mainCfg.syncCfg, mainCfg.deviceParallelOps, outSync);
//###########################################################
- XmlOut outFilter = outMain["Filter"];
- //write filter settings
+ XmlOut outFilter = out["Filter"];
writeConfig(mainCfg.globalFilter, outFilter);
//###########################################################
- XmlOut outFp = outMain["FolderPairs"];
+ XmlOut outFp = out["FolderPairs"];
//write folder pairs
writeConfig(mainCfg.firstPair, mainCfg.deviceParallelOps, outFp);
for (const LocalPairConfig& lpc : mainCfg.additionalPairs)
writeConfig(lpc, mainCfg.deviceParallelOps, outFp);
- outMain["Errors"].attribute("Ignore", mainCfg.ignoreErrors);
- outMain["Errors"].attribute("Retry", mainCfg.autoRetryCount);
- outMain["Errors"].attribute("Delay", mainCfg.autoRetryDelay);
+ out["Errors"].attribute("Ignore", mainCfg.ignoreErrors);
+ out["Errors"].attribute("Retry", mainCfg.autoRetryCount);
+ out["Errors"].attribute("Delay", mainCfg.autoRetryDelay);
- outMain["PostSyncCommand"](mainCfg.postSyncCommand);
- outMain["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
+ out["PostSyncCommand"](mainCfg.postSyncCommand);
+ out["PostSyncCommand"].attribute("Condition", mainCfg.postSyncCondition);
- outMain["LogFolder"](mainCfg.altLogFolderPathPhrase);
+ out["LogFolder"](mainCfg.altLogFolderPathPhrase);
- outMain["EmailNotification"](mainCfg.emailNotifyAddress);
- outMain["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
+ out["EmailNotification"](mainCfg.emailNotifyAddress);
+ out["EmailNotification"].attribute("Condition", mainCfg.emailNotifyCondition);
}
@@ -2528,11 +2480,10 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out)
template <class ConfigType>
-void writeConfig(const ConfigType& cfg, XmlType type, int xmlFormatVer, const Zstring& filePath)
+void writeConfig(const ConfigType& cfg, const char* cfgType, int xmlFormatVer, const Zstring& filePath)
{
XmlDoc doc("FreeFileSync");
- setXmlType(doc, type); //throw()
-
+ doc.root().setAttribute("XmlType", cfgType);
doc.root().setAttribute("XmlFormat", xmlFormatVer);
XmlOut out(doc);
@@ -2544,19 +2495,19 @@ void writeConfig(const ConfigType& cfg, XmlType type, int xmlFormatVer, const Zs
void fff::writeConfig(const XmlGuiConfig& cfg, const Zstring& filePath)
{
- ::writeConfig(cfg, XmlType::gui, XML_FORMAT_SYNC_CFG, filePath); //throw FileError
+ ::writeConfig(cfg, "GUI", XML_FORMAT_SYNC_CFG, filePath); //throw FileError
}
void fff::writeConfig(const XmlBatchConfig& cfg, const Zstring& filePath)
{
- ::writeConfig(cfg, XmlType::batch, XML_FORMAT_SYNC_CFG, filePath); //throw FileError
+ ::writeConfig(cfg, "BATCH", XML_FORMAT_SYNC_CFG, filePath); //throw FileError
}
void fff::writeConfig(const XmlGlobalSettings& cfg, const Zstring& filePath)
{
- ::writeConfig(cfg, XmlType::global, XML_FORMAT_GLOBAL_CFG, filePath); //throw FileError
+ ::writeConfig(cfg, "GLOBAL", XML_FORMAT_GLOBAL_CFG, filePath); //throw FileError
}
diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h
index 9e7c0a39..74e05cdb 100644
--- a/FreeFileSync/Source/config.h
+++ b/FreeFileSync/Source/config.h
@@ -20,16 +20,6 @@
namespace fff
{
-enum class XmlType
-{
- gui,
- batch,
- global,
- other
-};
-XmlType getXmlType(const Zstring& filePath); //throw FileError
-
-
enum class BatchErrorHandling
{
showPopup,
@@ -185,7 +175,7 @@ struct XmlGlobalSettings
int syncOverdueDays = 7;
ColumnTypeCfg lastSortColumn = cfgGridLastSortColumnDefault;
bool lastSortAscending = getDefaultSortDirection(cfgGridLastSortColumnDefault);
- size_t histItemsMax = 100;
+ size_t histItemsMax = 100; //do we need to limit config items at all?
Zstring lastSelectedFile;
std::vector<ConfigFileItem> fileHistory;
std::vector<Zstring> lastUsedFiles;
@@ -253,7 +243,7 @@ struct XmlGlobalSettings
std::vector<ExternalApp> externalApps{extCommandFileManager, extCommandOpenDefault};
- time_t lastUpdateCheck = 0; //number of seconds since 00:00 hours, Jan 1, 1970 UTC
+ time_t lastUpdateCheck = 0; //number of seconds since Jan 1, 1970 GMT
std::string lastOnlineVersion;
std::string welcomeShownVersion; //last FFS version for which the welcome dialog was shown
diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp
index 0d653b34..d0aa5692 100644
--- a/FreeFileSync/Source/localization.cpp
+++ b/FreeFileSync/Source/localization.cpp
@@ -114,11 +114,13 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi
else
assert(false);
}
- catch (FileError&) //fall back to folder
+ catch (FileError&) //fall back to folder: dev build (only!?)
{
const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none);
- if (dirAvailable(fallbackFolder)) //Debug build (only!?)
- traverseFolder(fallbackFolder, [&](const FileInfo& fi)
+ if (!itemStillExists(fallbackFolder)) //throw FileError
+ throw;
+
+ traverseFolder(fallbackFolder, [&](const FileInfo& fi)
{
if (endsWith(fi.fullPath, Zstr(".lng")))
{
@@ -126,23 +128,22 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi
streams.emplace_back(fi.itemName, std::move(stream));
}
}, nullptr, nullptr, [](const std::wstring& errorMsg) { throw FileError(errorMsg); });
- else
- throw;
}
//--------------------------------------------------------------------
- std::vector<TranslationInfo> translations;
+ std::vector<TranslationInfo> translations
{
//default entry:
- TranslationInfo newEntry;
- newEntry.languageID = wxLANGUAGE_ENGLISH_US;
- newEntry.languageName = L"English";
- newEntry.translatorName = L"Zenju";
- newEntry.languageFlag = "flag_usa";
- newEntry.lngFileName = Zstr("");
- newEntry.lngStream = "";
- translations.push_back(newEntry);
- }
+ {
+ .languageID = wxLANGUAGE_ENGLISH_US,
+ .locale = "en_US",
+ .languageName = L"English",
+ .translatorName = L"Zenju",
+ .languageFlag = "flag_usa",
+ .lngFileName = Zstr(""),
+ .lngStream = "",
+ }
+ };
for (/*const*/ auto& [fileName, stream] : streams)
try
@@ -150,22 +151,22 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi
const lng::TransHeader lngHeader = lng::parseHeader(stream); //throw ParsingError
assert(!lngHeader.languageName .empty());
assert(!lngHeader.translatorName.empty());
- assert(!lngHeader.localeName .empty());
+ assert(!lngHeader.locale .empty());
assert(!lngHeader.flagFile .empty());
- const wxLanguageInfo* lngInfo = wxLocale::FindLanguageInfo(utfTo<wxString>(lngHeader.localeName));
- assert(lngInfo && lngInfo->CanonicalName == utfTo<wxString>(lngHeader.localeName));
+ const wxLanguageInfo* lngInfo = wxLocale::FindLanguageInfo(utfTo<wxString>(lngHeader.locale));
+ assert(lngInfo && lngInfo->CanonicalName == utfTo<wxString>(lngHeader.locale));
if (lngInfo)
+ translations.push_back(
{
- TranslationInfo newEntry;
- newEntry.languageID = static_cast<wxLanguage>(lngInfo->Language);
- newEntry.languageName = utfTo<std::wstring>(lngHeader.languageName);
- newEntry.translatorName = utfTo<std::wstring>(lngHeader.translatorName);
- newEntry.languageFlag = lngHeader.flagFile;
- newEntry.lngFileName = fileName;
- newEntry.lngStream = std::move(stream);
- translations.push_back(std::move(newEntry));
- }
+ .languageID = static_cast<wxLanguage>(lngInfo->Language),
+ .locale = lngHeader.locale,
+ .languageName = utfTo<std::wstring>(lngHeader.languageName),
+ .translatorName = utfTo<std::wstring>(lngHeader.translatorName),
+ .languageFlag = lngHeader.flagFile,
+ .lngFileName = fileName,
+ .lngStream = std::move(stream),
+ });
}
catch (lng::ParsingError&) { assert(false); }
@@ -278,13 +279,13 @@ public:
wxMsgCatalog* LoadCatalog(const wxString& domain, const wxString& lang) override
{
+ //"lang" is NOT (exactly) what we return from GetAvailableTranslations(), but has a little "extra", e.g.: de_DE.WINDOWS-1252 or ar.WINDOWS-1252
auto extractIsoLangCode = [](wxString langCode)
{
langCode = beforeLast(langCode, L".", IfNotFoundReturn::all);
return beforeLast(langCode, L"_", IfNotFoundReturn::all);
};
- //"lang" is NOT (exactly) what we return from GetAvailableTranslations(), but has a little "extra", e.g.: de_DE.WINDOWS-1252 or ar.WINDOWS-1252
if (equalAsciiNoCase(extractIsoLangCode(lang), extractIsoLangCode(canonicalName_)))
return wxMsgCatalog::CreateFromData(wxScopedCharBuffer::CreateNonOwned(moBuf_.ref().c_str(), moBuf_.ref().size()), domain);
assert(false);
diff --git a/FreeFileSync/Source/localization.h b/FreeFileSync/Source/localization.h
index f7161541..3672e6f5 100644
--- a/FreeFileSync/Source/localization.h
+++ b/FreeFileSync/Source/localization.h
@@ -18,6 +18,7 @@ namespace fff
struct TranslationInfo
{
wxLanguage languageID = wxLANGUAGE_UNKNOWN;
+ std::string locale;
std::wstring languageName;
std::wstring translatorName;
std::string languageFlag;
diff --git a/FreeFileSync/Source/log_file.cpp b/FreeFileSync/Source/log_file.cpp
index 77817898..71255ba5 100644
--- a/FreeFileSync/Source/log_file.cpp
+++ b/FreeFileSync/Source/log_file.cpp
@@ -25,7 +25,7 @@ const int SEPARATION_LINE_LEN = 40;
std::string generateLogHeaderTxt(const ProcessSummary& s, const ErrorLog& log, int logPreviewFailsMax)
{
- const std::string tabSpace(4, ' '); //4: the only sensible space count for tabs
+ const auto tabSpace = utfTo<std::string>(TAB_SPACE);
std::string headerLine;
for (const std::wstring& jobName : s.jobNames)
diff --git a/FreeFileSync/Source/parse_lng.h b/FreeFileSync/Source/parse_lng.h
index 48a2dd81..7af4766d 100644
--- a/FreeFileSync/Source/parse_lng.h
+++ b/FreeFileSync/Source/parse_lng.h
@@ -28,7 +28,7 @@ struct TransHeader
{
std::string languageName; //display name: "English (UK)"
std::string translatorName; //"Zenju"
- std::string localeName; //ISO 639 language code + ISO 3166 country code, e.g. "en_GB", or "en_US"
+ std::string locale; //ISO 639 language code + (optional) ISO 3166 country code, e.g. "de", "en_GB", or "en_US"
std::string flagFile; //"england.png"
int pluralCount = 0; //2
std::string pluralDefinition; //"n == 1 ? 0 : 1"
@@ -166,8 +166,8 @@ enum class TokenType
langNameEnd,
transNameBegin,
transNameEnd,
- localeNameBegin,
- localeNameEnd,
+ localeBegin,
+ localeEnd,
flagFileBegin,
flagFileEnd,
pluralCountBegin,
@@ -223,8 +223,8 @@ private:
{TokenType::langNameEnd, "</language>"},
{TokenType::transNameBegin, "<translator>"},
{TokenType::transNameEnd, "</translator>"},
- {TokenType::localeNameBegin, "<locale>"},
- {TokenType::localeNameEnd, "</locale>"},
+ {TokenType::localeBegin, "<locale>"},
+ {TokenType::localeEnd, "</locale>"},
{TokenType::flagFileBegin, "<image>"},
{TokenType::flagFileEnd, "</image>"},
{TokenType::pluralCountBegin, "<plural_count>"},
@@ -373,10 +373,10 @@ public:
consumeToken(TokenType::text); //throw ParsingError
consumeToken(TokenType::transNameEnd); //
- consumeToken(TokenType::localeNameBegin); //throw ParsingError
- header.localeName = token().text;
+ consumeToken(TokenType::localeBegin); //throw ParsingError
+ header.locale = token().text;
consumeToken(TokenType::text); //throw ParsingError
- consumeToken(TokenType::localeNameEnd); //
+ consumeToken(TokenType::localeEnd); //
consumeToken(TokenType::flagFileBegin); //throw ParsingError
header.flagFile = token().text;
@@ -506,7 +506,7 @@ private:
throw ParsingError({L"Source text ends with an ellipsis \"...\", but translation does not", scn_.posRow(), scn_.posCol()});
//check for not-to-be-translated texts
- for (const char* fixedStr : {"FreeFileSync", "RealTimeSync", "ffs_gui", "ffs_batch", "ffs_tmp", "GlobalSettings.xml"})
+ for (const char* fixedStr : {"FreeFileSync", "RealTimeSync", "ffs_gui", "ffs_batch", "ffs_real", "ffs_tmp", "GlobalSettings.xml"})
if (contains(original, fixedStr) && !contains(translation, fixedStr))
throw ParsingError({replaceCpy<std::wstring>(L"Misspelled \"%x\" in translation", L"%x", utfTo<std::wstring>(fixedStr)), scn_.posRow(), scn_.posCol()});
@@ -757,9 +757,9 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h
out += header.translatorName;
out += tokens.text(TokenType::transNameEnd) + '\n';
- out += '\t' + tokens.text(TokenType::localeNameBegin);
- out += header.localeName;
- out += tokens.text(TokenType::localeNameEnd) + '\n';
+ out += '\t' + tokens.text(TokenType::localeBegin);
+ out += header.locale;
+ out += tokens.text(TokenType::localeEnd) + '\n';
out += '\t' + tokens.text(TokenType::flagFileBegin);
out += header.flagFile;
diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp
index 57e132af..68d4dd88 100644
--- a/FreeFileSync/Source/ui/batch_config.cpp
+++ b/FreeFileSync/Source/ui/batch_config.cpp
@@ -130,16 +130,17 @@ void BatchDialog::setConfig(const BatchDialogConfig& dlgCfg)
BatchDialogConfig BatchDialog::getConfig() const
{
- BatchDialogConfig dlgCfg = {};
-
- dlgCfg.ignoreErrors = m_checkBoxIgnoreErrors->GetValue();
-
- dlgCfg.batchExCfg.batchErrorHandling = m_radioBtnErrorDialogCancel->GetValue() ? BatchErrorHandling::cancel : BatchErrorHandling::showPopup;
- dlgCfg.batchExCfg.runMinimized = m_checkBoxRunMinimized->GetValue();
- dlgCfg.batchExCfg.autoCloseSummary = m_checkBoxAutoClose ->GetValue();
- dlgCfg.batchExCfg.postSyncAction = getEnumVal(enumPostSyncAction_, *m_choicePostSyncAction);
-
- return dlgCfg;
+ return
+ {
+ .batchExCfg
+ {
+ .batchErrorHandling = m_radioBtnErrorDialogCancel->GetValue() ? BatchErrorHandling::cancel : BatchErrorHandling::showPopup,
+ .runMinimized = m_checkBoxRunMinimized->GetValue(),
+ .autoCloseSummary = m_checkBoxAutoClose ->GetValue(),
+ .postSyncAction = getEnumVal(enumPostSyncAction_, *m_choicePostSyncAction),
+ },
+ .ignoreErrors = m_checkBoxIgnoreErrors->GetValue(),
+ };
}
diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp
index afb631b1..8161d13e 100644
--- a/FreeFileSync/Source/ui/cfg_grid.cpp
+++ b/FreeFileSync/Source/ui/cfg_grid.cpp
@@ -82,9 +82,8 @@ void ConfigView::addCfgFilesImpl(const std::vector<Zstring>& filePaths)
if (auto it = cfgList_.find(filePath);
it == cfgList_.end())
{
- Details detail = {};
+ Details detail{.lastUseIndex = ++lastUseIndexMax};
detail.cfgItem.cfgFilePath = filePath;
- detail.lastUseIndex = ++lastUseIndexMax;
std::tie(detail.name, detail.cfgType, detail.isLastRunCfg) = [&]
{
diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp
index 86d2bf28..6c8a1f4a 100644
--- a/FreeFileSync/Source/ui/file_grid.cpp
+++ b/FreeFileSync/Source/ui/file_grid.cpp
@@ -1727,8 +1727,8 @@ public:
//=> Next keyboard input on left does *not* emit focus change event, but still "scrollMaster" needs to change
//=> hook keyboard input instead of focus event:
grid.getMainWin().Bind(wxEVT_CHAR, handler);
- grid.getMainWin().Bind(wxEVT_KEY_UP, handler);
grid.getMainWin().Bind(wxEVT_KEY_DOWN, handler);
+ //grid.getMainWin().Bind(wxEVT_KEY_UP, handler); -> superfluous?
grid.getMainWin().Bind(wxEVT_LEFT_DOWN, handler);
grid.getMainWin().Bind(wxEVT_LEFT_DCLICK, handler);
diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp
index d53fcd8a..1ce778b3 100644
--- a/FreeFileSync/Source/ui/folder_selector.cpp
+++ b/FreeFileSync/Source/ui/folder_selector.cpp
@@ -202,7 +202,7 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event)
Zstring defaultFolderNative;
{
//make sure default folder exists: don't let folder picker hang on non-existing network share!
- auto folderExistsTimed = [waitEndTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const AbstractPath& folderPath)
+ auto folderAccessible = [waitEndTime = std::chrono::steady_clock::now() + FOLDER_SELECTED_EXISTENCE_CHECK_TIME_MAX](const AbstractPath& folderPath)
{
if (AFS::isNullPath(folderPath))
return false;
@@ -223,7 +223,7 @@ void FolderSelector::onSelectFolder(wxCommandEvent& event)
if (acceptsItemPathPhraseNative(folderPathPhrase)) //noexcept
{
const AbstractPath folderPath = createItemPathNative(folderPathPhrase);
- if (folderExistsTimed(folderPath))
+ if (folderAccessible(folderPath))
if (const Zstring& nativePath = getNativeItemPath(folderPath);
!nativePath.empty())
defaultFolderNative = nativePath;
diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp
index a30c063c..9ec3baf5 100644
--- a/FreeFileSync/Source/ui/gui_generated.cpp
+++ b/FreeFileSync/Source/ui/gui_generated.cpp
@@ -132,9 +132,9 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_panelTopButtons->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
wxBoxSizer* bSizer1791;
- bSizer1791 = new wxBoxSizer( wxVERTICAL );
+ bSizer1791 = new wxBoxSizer( wxHORIZONTAL );
- bSizerTopButtons = new wxBoxSizer( wxHORIZONTAL );
+ bSizer2941 = new wxBoxSizer( wxHORIZONTAL );
wxBoxSizer* bSizer261;
bSizer261 = new wxBoxSizer( wxHORIZONTAL );
@@ -142,14 +142,6 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer261->Add( 0, 0, 1, 0, 5 );
- m_bpButtonCmpConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- m_bpButtonCmpConfig->SetToolTip( _("dummy") );
-
- bSizer261->Add( m_bpButtonCmpConfig, 0, wxEXPAND, 5 );
-
-
- bSizer261->Add( 4, 0, 0, 0, 5 );
-
m_buttonCancel = new wxButton( m_panelTopButtons, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
m_buttonCancel->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
m_buttonCancel->Enable( false );
@@ -163,6 +155,11 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer261->Add( m_buttonCompare, 0, wxEXPAND, 5 );
+ m_bpButtonCmpConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
+ m_bpButtonCmpConfig->SetToolTip( _("dummy") );
+
+ bSizer261->Add( m_bpButtonCmpConfig, 0, wxEXPAND, 5 );
+
m_bpButtonCmpContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
bSizer261->Add( m_bpButtonCmpContext, 0, wxEXPAND, 5 );
@@ -170,10 +167,10 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer261->Add( 0, 0, 1, 0, 5 );
- bSizerTopButtons->Add( bSizer261, 1, wxEXPAND, 5 );
+ bSizer2941->Add( bSizer261, 1, wxEXPAND, 5 );
- bSizerTopButtons->Add( 5, 2, 0, 0, 5 );
+ bSizer2941->Add( 5, 0, 0, 0, 5 );
wxBoxSizer* bSizer199;
bSizer199 = new wxBoxSizer( wxHORIZONTAL );
@@ -193,10 +190,10 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer199->Add( 0, 0, 1, 0, 5 );
- bSizerTopButtons->Add( bSizer199, 0, wxEXPAND, 5 );
+ bSizer2941->Add( bSizer199, 0, wxEXPAND, 5 );
- bSizerTopButtons->Add( 5, 2, 0, 0, 5 );
+ bSizer2941->Add( 5, 0, 0, 0, 5 );
wxBoxSizer* bSizer262;
bSizer262 = new wxBoxSizer( wxHORIZONTAL );
@@ -204,20 +201,17 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer262->Add( 0, 0, 1, 0, 5 );
- m_bpButtonSyncConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- m_bpButtonSyncConfig->SetToolTip( _("dummy") );
-
- bSizer262->Add( m_bpButtonSyncConfig, 0, wxEXPAND, 5 );
-
-
- bSizer262->Add( 4, 0, 0, 0, 5 );
-
m_buttonSync = new zen::BitmapTextButton( m_panelTopButtons, wxID_ANY, _("Synchronize"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
m_buttonSync->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
m_buttonSync->SetToolTip( _("dummy") );
bSizer262->Add( m_buttonSync, 0, wxEXPAND, 5 );
+ m_bpButtonSyncConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
+ m_bpButtonSyncConfig->SetToolTip( _("dummy") );
+
+ bSizer262->Add( m_bpButtonSyncConfig, 0, wxEXPAND, 5 );
+
m_bpButtonSyncContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
bSizer262->Add( m_bpButtonSyncContext, 0, wxEXPAND, 5 );
@@ -225,10 +219,10 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer262->Add( 0, 0, 1, 0, 5 );
- bSizerTopButtons->Add( bSizer262, 1, wxEXPAND, 5 );
+ bSizer2941->Add( bSizer262, 1, wxEXPAND, 5 );
- bSizer1791->Add( bSizerTopButtons, 1, wxEXPAND, 5 );
+ bSizer1791->Add( bSizer2941, 1, wxALIGN_CENTER_VERTICAL, 5 );
m_panelTopButtons->SetSizer( bSizer1791 );
@@ -741,7 +735,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_panelConfig = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
m_panelConfig->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
- bSizerConfig = new wxBoxSizer( wxHORIZONTAL );
+ bSizerConfig = new wxBoxSizer( wxVERTICAL );
bSizerCfgHistoryButtons = new wxBoxSizer( wxHORIZONTAL );
@@ -793,21 +787,20 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
wxBoxSizer* bSizer174;
bSizer174 = new wxBoxSizer( wxVERTICAL );
- wxBoxSizer* bSizer1772;
- bSizer1772 = new wxBoxSizer( wxHORIZONTAL );
+ bSizerSaveAs = new wxBoxSizer( wxHORIZONTAL );
m_bpButtonSaveAs = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
m_bpButtonSaveAs->SetToolTip( _("dummy") );
- bSizer1772->Add( m_bpButtonSaveAs, 1, 0, 5 );
+ bSizerSaveAs->Add( m_bpButtonSaveAs, 1, 0, 5 );
m_bpButtonSaveAsBatch = new wxBitmapButton( m_panelConfig, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
m_bpButtonSaveAsBatch->SetToolTip( _("dummy") );
- bSizer1772->Add( m_bpButtonSaveAsBatch, 1, 0, 5 );
+ bSizerSaveAs->Add( m_bpButtonSaveAsBatch, 1, 0, 5 );
- bSizer174->Add( bSizer1772, 0, wxEXPAND, 5 );
+ bSizer174->Add( bSizerSaveAs, 0, wxEXPAND, 5 );
m_staticText97 = new wxStaticText( m_panelConfig, wxID_ANY, _("Save as..."), wxDefaultPosition, wxDefaultSize, 0 );
m_staticText97->Wrap( -1 );
@@ -817,11 +810,14 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizerCfgHistoryButtons->Add( bSizer174, 0, 0, 5 );
- bSizerConfig->Add( bSizerCfgHistoryButtons, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 );
+ bSizerConfig->Add( bSizerCfgHistoryButtons, 0, wxALIGN_CENTER_HORIZONTAL, 5 );
m_staticline81 = new wxStaticLine( m_panelConfig, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
bSizerConfig->Add( m_staticline81, 0, wxEXPAND|wxTOP, 5 );
+
+ bSizerConfig->Add( 10, 0, 0, 0, 5 );
+
m_gridCfgHistory = new zen::Grid( m_panelConfig, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL );
m_gridCfgHistory->SetScrollRate( 5, 5 );
bSizerConfig->Add( m_gridCfgHistory, 1, wxEXPAND, 5 );
@@ -843,62 +839,67 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 );
+ bSizerViewButtons = new wxBoxSizer( wxHORIZONTAL );
+
m_bpButtonViewType = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizerViewFilter->Add( m_bpButtonViewType, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );
+ bSizerViewButtons->Add( m_bpButtonViewType, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 );
- wxBoxSizer* bSizer300;
- bSizer300 = new wxBoxSizer( wxHORIZONTAL );
+
+ bSizerViewButtons->Add( 10, 10, 0, 0, 5 );
m_bpButtonShowExcluded = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowExcluded, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL|wxRIGHT, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowExcluded, 0, wxEXPAND, 5 );
+
+
+ bSizerViewButtons->Add( 10, 10, 0, 0, 5 );
m_bpButtonShowDeleteLeft = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowDeleteLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowDeleteLeft, 0, wxEXPAND, 5 );
m_bpButtonShowUpdateLeft = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowUpdateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowUpdateLeft, 0, wxEXPAND, 5 );
m_bpButtonShowCreateLeft = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowCreateLeft, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowCreateLeft, 0, wxEXPAND, 5 );
m_bpButtonShowLeftOnly = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowLeftOnly, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowLeftOnly, 0, wxEXPAND, 5 );
m_bpButtonShowLeftNewer = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowLeftNewer, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowLeftNewer, 0, wxEXPAND, 5 );
m_bpButtonShowEqual = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowEqual, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowEqual, 0, wxEXPAND, 5 );
m_bpButtonShowDoNothing = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowDoNothing, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowDoNothing, 0, wxEXPAND, 5 );
m_bpButtonShowDifferent = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowDifferent, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowDifferent, 0, wxEXPAND, 5 );
m_bpButtonShowRightNewer = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowRightNewer, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowRightNewer, 0, wxEXPAND, 5 );
m_bpButtonShowRightOnly = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowRightOnly, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowRightOnly, 0, wxEXPAND, 5 );
m_bpButtonShowCreateRight = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowCreateRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowCreateRight, 0, wxEXPAND, 5 );
m_bpButtonShowUpdateRight = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowUpdateRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowUpdateRight, 0, wxEXPAND, 5 );
m_bpButtonShowDeleteRight = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowDeleteRight, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowDeleteRight, 0, wxEXPAND, 5 );
m_bpButtonShowConflict = new zen::ToggleButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonShowConflict, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+ bSizerViewButtons->Add( m_bpButtonShowConflict, 0, wxEXPAND, 5 );
m_bpButtonViewFilterContext = new wxBitmapButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|0 );
- bSizer300->Add( m_bpButtonViewFilterContext, 0, wxEXPAND, 5 );
+ bSizerViewButtons->Add( m_bpButtonViewFilterContext, 0, wxEXPAND, 5 );
- bSizerViewFilter->Add( bSizer300, 0, wxALIGN_CENTER_VERTICAL, 5 );
+ bSizerViewFilter->Add( bSizerViewButtons, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 );
@@ -1134,20 +1135,20 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuCheckVersion ), this, m_menuItemCheckVersionNow->GetId());
m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuCheckVersionAutomatically ), this, m_menuItemCheckVersionAuto->GetId());
m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuAbout ), this, m_menuItemAbout->GetId());
- m_bpButtonCmpConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCmpSettings ), NULL, this );
- m_bpButtonCmpConfig->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onCompSettingsContextMouse ), NULL, this );
m_buttonCompare->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCompare ), NULL, this );
m_buttonCompare->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onCompSettingsContextMouse ), NULL, this );
+ m_bpButtonCmpConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCmpSettings ), NULL, this );
+ m_bpButtonCmpConfig->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onCompSettingsContextMouse ), NULL, this );
m_bpButtonCmpContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCompSettingsContext ), NULL, this );
m_bpButtonCmpContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onCompSettingsContextMouse ), NULL, this );
m_bpButtonFilter->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onConfigureFilter ), NULL, this );
m_bpButtonFilter->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onGlobalFilterContextMouse ), NULL, this );
m_bpButtonFilterContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onGlobalFilterContext ), NULL, this );
m_bpButtonFilterContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onGlobalFilterContextMouse ), NULL, this );
- m_bpButtonSyncConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSyncSettings ), NULL, this );
- m_bpButtonSyncConfig->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onSyncSettingsContextMouse ), NULL, this );
m_buttonSync->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onStartSync ), NULL, this );
m_buttonSync->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onSyncSettingsContextMouse ), NULL, this );
+ m_bpButtonSyncConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSyncSettings ), NULL, this );
+ m_bpButtonSyncConfig->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onSyncSettingsContextMouse ), NULL, this );
m_bpButtonSyncContext->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onSyncSettingsContext ), NULL, this );
m_bpButtonSyncContext->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::onSyncSettingsContextMouse ), NULL, this );
m_bpButtonAddPair->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onTopFolderPairAdd ), NULL, this );
@@ -1519,8 +1520,14 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
m_staticline331 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
bSizer159->Add( m_staticline331, 0, wxEXPAND, 5 );
+
+ bSizer159->Add( 0, 0, 1, 0, 5 );
+
bSizerCompMisc = new wxBoxSizer( wxVERTICAL );
+ m_staticline3311 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
+ bSizerCompMisc->Add( m_staticline3311, 0, wxEXPAND, 5 );
+
wxBoxSizer* bSizer2781;
bSizer2781 = new wxBoxSizer( wxHORIZONTAL );
@@ -1568,9 +1575,6 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
bSizerCompMisc->Add( bSizer2781, 0, wxEXPAND, 5 );
- m_staticline3311 = new wxStaticLine( m_panelComparisonSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
- bSizerCompMisc->Add( m_staticline3311, 0, wxEXPAND, 5 );
-
bSizer159->Add( bSizerCompMisc, 0, wxEXPAND, 5 );
@@ -1651,7 +1655,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
m_panelCompSettingsTab->SetSizer( bSizer275 );
m_panelCompSettingsTab->Layout();
bSizer275->Fit( m_panelCompSettingsTab );
- m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), false );
+ m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), true );
m_panelFilterSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
m_panelFilterSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
@@ -2489,7 +2493,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w
m_panelSyncSettingsTab->SetSizer( bSizer276 );
m_panelSyncSettingsTab->Layout();
bSizer276->Fit( m_panelSyncSettingsTab );
- m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), true );
+ m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), false );
bSizer190->Add( m_notebook, 1, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 5 );
diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h
index 8b47035a..6542e1a8 100644
--- a/FreeFileSync/Source/ui/gui_generated.h
+++ b/FreeFileSync/Source/ui/gui_generated.h
@@ -28,8 +28,8 @@ namespace zen { class BitmapTextButton; }
#include <wx/font.h>
#include <wx/colour.h>
#include <wx/settings.h>
-#include <wx/bmpbuttn.h>
#include <wx/button.h>
+#include <wx/bmpbuttn.h>
#include <wx/sizer.h>
#include <wx/panel.h>
#include <wx/stattext.h>
@@ -101,15 +101,15 @@ protected:
wxMenuItem* m_menuItemAbout;
wxBoxSizer* bSizerPanelHolder;
wxPanel* m_panelTopButtons;
- wxBoxSizer* bSizerTopButtons;
- wxBitmapButton* m_bpButtonCmpConfig;
+ wxBoxSizer* bSizer2941;
wxButton* m_buttonCancel;
zen::BitmapTextButton* m_buttonCompare;
+ wxBitmapButton* m_bpButtonCmpConfig;
wxBitmapButton* m_bpButtonCmpContext;
wxBitmapButton* m_bpButtonFilter;
wxBitmapButton* m_bpButtonFilterContext;
- wxBitmapButton* m_bpButtonSyncConfig;
zen::BitmapTextButton* m_buttonSync;
+ wxBitmapButton* m_bpButtonSyncConfig;
wxBitmapButton* m_bpButtonSyncContext;
wxPanel* m_panelDirectoryPairs;
wxStaticText* m_staticTextResolvedPathL;
@@ -170,6 +170,7 @@ protected:
wxStaticText* m_staticText95;
wxBitmapButton* m_bpButtonSave;
wxStaticText* m_staticText961;
+ wxBoxSizer* bSizerSaveAs;
wxBitmapButton* m_bpButtonSaveAs;
wxBitmapButton* m_bpButtonSaveAsBatch;
wxStaticText* m_staticText97;
@@ -178,6 +179,7 @@ protected:
wxPanel* m_panelViewFilter;
wxBoxSizer* bSizerViewFilter;
wxBitmapButton* m_bpButtonToggleLog;
+ wxBoxSizer* bSizerViewButtons;
zen::ToggleButton* m_bpButtonViewType;
zen::ToggleButton* m_bpButtonShowExcluded;
zen::ToggleButton* m_bpButtonShowDeleteLeft;
@@ -356,6 +358,7 @@ protected:
wxHyperlinkCtrl* m_hyperlink241;
wxStaticLine* m_staticline331;
wxBoxSizer* bSizerCompMisc;
+ wxStaticLine* m_staticline3311;
wxStaticBitmap* m_bitmapIgnoreErrors;
wxCheckBox* m_checkBoxIgnoreErrors;
wxCheckBox* m_checkBoxAutoRetry;
@@ -364,7 +367,6 @@ protected:
wxStaticText* m_staticTextAutoRetryDelay;
wxSpinCtrl* m_spinCtrlAutoRetryCount;
wxSpinCtrl* m_spinCtrlAutoRetryDelay;
- wxStaticLine* m_staticline3311;
wxStaticLine* m_staticline751;
wxBoxSizer* bSizerPerformance;
wxPanel* m_panel57;
diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp
index c6e8470c..7702cb6d 100644
--- a/FreeFileSync/Source/ui/gui_status_handler.cpp
+++ b/FreeFileSync/Source/ui/gui_status_handler.cpp
@@ -328,10 +328,7 @@ void StatusHandlerTemporaryPanel::onLocalKeyEvent(wxKeyEvent& event)
{
const int keyCode = event.GetKeyCode();
if (keyCode == WXK_ESCAPE)
- {
- wxCommandEvent dummy;
- onAbortCompare(dummy);
- }
+ return userRequestAbort();
event.Skip();
}
diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp
index aa686a8a..261b2085 100644
--- a/FreeFileSync/Source/ui/log_panel.cpp
+++ b/FreeFileSync/Source/ui/log_panel.cpp
@@ -468,62 +468,58 @@ void LogPanel::onGridButtonEvent(wxKeyEvent& event)
void LogPanel::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
{
- if (processingKeyEventHandler_) //avoid recursion
+ if (!processingKeyEventHandler_) //avoid recursion
{
- event.Skip();
- return;
- }
- processingKeyEventHandler_ = true;
- ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false);
-
-
- const int keyCode = event.GetKeyCode();
+ processingKeyEventHandler_ = true;
+ ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false);
- if (event.ControlDown())
- switch (keyCode)
- {
- case 'A':
- m_gridMessages->SetFocus();
- m_gridMessages->selectAllRows(GridEventPolicy::allow);
- return; // -> swallow event! don't allow default grid commands!
+ const int keyCode = event.GetKeyCode();
- //case 'C': -> already implemented by "Grid" class
- }
- else
- switch (keyCode)
- {
- //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:
- if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus
- m_gridMessages->IsEnabled())
- if (wxEvtHandler* evtHandler = m_gridMessages->getMainWin().GetEventHandler())
- {
- m_gridMessages->SetFocus();
+ if (event.ControlDown())
+ switch (keyCode)
+ {
+ case 'A':
+ m_gridMessages->SetFocus();
+ m_gridMessages->selectAllRows(GridEventPolicy::allow);
+ return; // -> swallow event! don't allow default grid commands!
- event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
- evtHandler->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it...
- event.Skip(false); //definitively handled now!
- return;
- }
- break;
- }
+ //case 'C': -> already implemented by "Grid" class
+ }
+ else
+ switch (keyCode)
+ {
+ //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:
+ if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus
+ m_gridMessages->IsEnabled())
+ if (wxEvtHandler* evtHandler = m_gridMessages->getMainWin().GetEventHandler())
+ {
+ m_gridMessages->SetFocus();
+ event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
+ evtHandler->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it...
+ event.Skip(false); //definitively handled now!
+ return;
+ }
+ break;
+ }
+ }
event.Skip();
}
diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp
index 2896d639..beea4555 100644
--- a/FreeFileSync/Source/ui/main_dlg.cpp
+++ b/FreeFileSync/Source/ui/main_dlg.cpp
@@ -313,7 +313,7 @@ void updateTopButton(wxBitmapButton& btn, const wxImage& img, const wxString& va
stackImages(btnIconImg, btnImg, ImageStackLayout::horizontal, ImageStackAlignment::center, fastFromDIP(5)) :
stackImages(btnImg, btnIconImg, ImageStackLayout::horizontal, ImageStackAlignment::center, fastFromDIP(5));
- wxSize btnSize = btnImg.GetSize() + wxSize(fastFromDIP(10), fastFromDIP(10)); //add border space
+ wxSize btnSize = btnImg.GetSize();
btnSize.x = std::max(btnSize.x, fastFromDIP(TOP_BUTTON_OPTIMAL_WIDTH_DIP));
btnImg = resizeCanvas(btnImg, btnSize, wxALIGN_CENTER);
@@ -411,6 +411,7 @@ void MainDialog::create(const Zstring& globalConfigFilePath,
const std::vector<Zstring>& referenceFiles,
bool startComparison)
{
+
const XmlGlobalSettings globSett = globalSettings ? *globalSettings : tryLoadGlobalConfig(globalConfigFilePath);
try
@@ -675,7 +676,10 @@ imgFileManagerSmall_([]
auiMgr_.AddPane(m_panelTopButtons,
wxAuiPaneInfo().Name(L"TopPanel").Layer(2).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false).
- PaneBorder(false).Gripper().MinSize(fastFromDIP(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight()));
+ PaneBorder(false).Gripper().
+ //BestSize(-1, m_panelTopButtons->GetSize().GetHeight() + fastFromDIP(10)).
+ MinSize(fastFromDIP(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(),
@@ -704,7 +708,7 @@ imgFileManagerSmall_([]
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(fastFromDIP(100), m_panelViewFilter->GetSize().y));
+ PaneBorder(false).Gripper().MinSize(fastFromDIP(80), m_panelViewFilter->GetSize().y));
m_panelConfig->GetSizer()->SetSizeHints(m_panelConfig); //~=Fit() + SetMinSize()
auiMgr_.AddPane(m_panelConfig,
@@ -854,8 +858,7 @@ imgFileManagerSmall_([]
{
m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, [this, panelWindow](wxCommandEvent&)
{
- wxAuiPaneInfo& paneInfo = this->auiMgr_.GetPane(panelWindow);
- paneInfo.Show();
+ this->auiMgr_.GetPane(panelWindow).Show();
this->auiMgr_.Update();
}, menuItem->GetId());
@@ -1921,40 +1924,46 @@ void MainDialog::enableGuiElements()
}
-namespace
-{
-void updateSizerOrientation(wxBoxSizer& sizer, wxWindow& window, double horizontalWeight)
-{
- const int newOrientation = window.GetSize().GetWidth() * horizontalWeight > window.GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window NOT sizer width!
- if (sizer.GetOrientation() != newOrientation)
- {
- sizer.SetOrientation(newOrientation);
- window.Layout();
- }
-}
-}
-
-
void MainDialog::onResizeTopButtonPanel(wxEvent& event)
{
- updateSizerOrientation(*bSizerTopButtons, *m_panelTopButtons, 0.5);
+ //TODO: shrink icons when out of space!?
event.Skip();
}
void MainDialog::onResizeConfigPanel(wxEvent& event)
{
- updateSizerOrientation(*bSizerConfig, *m_panelConfig, 0.5);
+ 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)
{
- //we need something more fancy for the statistics:
- const int newOrientation = m_panelViewFilter->GetSize().GetWidth() > m_panelViewFilter->GetSize().GetHeight() ? wxHORIZONTAL : wxVERTICAL; //check window NOT sizer width!
+ 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;
@@ -1963,8 +1972,6 @@ void MainDialog::onResizeViewPanel(wxEvent& event)
if (sizerChild->GetOrientation() != childOrient)
sizerChild->SetOrientation(childOrient);
- bSizerStatistics->SetOrientation(newOrientation);
- bSizerViewFilter->SetOrientation(newOrientation);
m_panelViewFilter->Layout();
m_panelStatistics->Layout();
}
@@ -2146,119 +2153,115 @@ void MainDialog::onGridKeyEvent(wxKeyEvent& event, Grid& grid, bool leftSide)
void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
{
- if (!localKeyEventsEnabled_)
+ if (localKeyEventsEnabled_) //avoid recursion
{
- event.Skip();
- return;
- }
- localKeyEventsEnabled_ = false; //avoid recursion
- ZEN_ON_SCOPE_EXIT(localKeyEventsEnabled_ = true);
+ localKeyEventsEnabled_ = false; //avoid recursion
+ ZEN_ON_SCOPE_EXIT(localKeyEventsEnabled_ = true);
+ const int keyCode = event.GetKeyCode();
- const int keyCode = event.GetKeyCode();
+ //CTRL + X
+ /* if (event.ControlDown())
+ switch (keyCode)
+ {
+ case 'F': //CTRL + F
+ showFindPanel();
+ return; //-> swallow event!
+ } */
- //CTRL + X
- /* if (event.ControlDown())
+ if (event.ControlDown())
switch (keyCode)
{
- case 'F': //CTRL + F
- showFindPanel();
+ 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!
- } */
+ }
- 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();
+ case WXK_F3:
+ case WXK_NUMPAD_F3:
+ startFindNext(!event.ShiftDown() /*searchAscending*/);
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:
- warn_static("F11 not working at all on macOS!")
- 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())
- if (wxEvtHandler* evtHandler = m_gridMainL->getMainWin().GetEventHandler())
- {
- m_gridMainL->SetFocus();
+ //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:
+ warn_static("F11 not working at all on macOS!")
+ setGridViewType(m_bpButtonViewType->isActive() ? GridViewType::difference : GridViewType::action);
+ return; //-> swallow event!
- event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
- evtHandler->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;
+ //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_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())
+ 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:
{
- if (auiMgr_.GetPane(m_panelLog).IsShown()) //else: let it "ding"
- return showLogPanel(false /*show*/);
+ 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())
+ if (wxEvtHandler* evtHandler = m_gridMainL->getMainWin().GetEventHandler())
+ {
+ m_gridMainL->SetFocus();
+
+ event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
+ evtHandler->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();
}
@@ -2778,9 +2781,9 @@ void MainDialog::onGridLabelContextRim(GridLabelClickEvent& event, bool leftSide
{
menu.addRadio(label, [sz, &setIconSize] { setIconSize(sz, true /*showIcons*/); }, globalCfg_.mainDlg.iconSize == sz, globalCfg_.mainDlg.showIcons);
};
- addSizeEntry(L" " + _("Small" ), GridIconSize::small );
- addSizeEntry(L" " + _("Medium"), GridIconSize::medium);
- addSizeEntry(L" " + _("Large" ), GridIconSize::large );
+ addSizeEntry(TAB_SPACE + _("Small" ), GridIconSize::small );
+ addSizeEntry(TAB_SPACE + _("Medium"), GridIconSize::medium);
+ addSizeEntry(TAB_SPACE + _("Large" ), GridIconSize::large );
//----------------------------------------------------------------------------------------------
auto setDefault = [&]
@@ -3074,27 +3077,15 @@ void MainDialog::onConfigSave(wxCommandEvent& event)
if (activeCfgFilePath.empty())
trySaveConfig(nullptr);
else
- try
- {
- switch (getXmlType(activeCfgFilePath)) //throw FileError
- {
- case XmlType::gui:
- trySaveConfig(&activeCfgFilePath);
- break;
- case XmlType::batch:
- trySaveBatchConfig(&activeCfgFilePath);
- break;
- case XmlType::global:
- case XmlType::other:
- showNotificationDialog(this, DialogInfoType::error,
- PopupDialogCfg().setDetailInstructions(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(activeCfgFilePath))));
- break;
- }
- }
- catch (const FileError& e)
- {
- showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
- }
+ {
+ 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(_("File %x does not contain a valid configuration."), L"%x", fmtPath(activeCfgFilePath))));
+ }
}
@@ -3105,7 +3096,7 @@ bool MainDialog::trySaveConfig(const Zstring* guiCfgPath) //"false": error/cance
if (guiCfgPath)
{
cfgFilePath = *guiCfgPath;
- assert(endsWith(cfgFilePath, Zstr(".ffs_gui")));
+ assert(endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_gui")));
}
else
{
@@ -3127,7 +3118,12 @@ bool MainDialog::trySaveConfig(const Zstring* guiCfgPath) //"false": error/cance
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (fileSelector.ShowModal() != wxID_OK)
return false;
- cfgFilePath = globalCfg_.mainDlg.config.lastSelectedFile = utfTo<Zstring>(fileSelector.GetPath());
+
+ 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 XmlGuiConfig guiCfg = getConfig();
@@ -3161,15 +3157,12 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchCfgPath) //"false": erro
Zstring referenceBatchFile;
if (batchCfgPath)
referenceBatchFile = *batchCfgPath;
- else if (!activeCfgFilePath.empty())
- if (getXmlType(activeCfgFilePath) == XmlType::batch) //throw FileError
- referenceBatchFile = activeCfgFilePath;
+ 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!
- }
+ //=> ignore warnings altogether: user has seen them already when loading the config file!
}
catch (const FileError& e)
{
@@ -3181,7 +3174,7 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchCfgPath) //"false": erro
if (batchCfgPath)
{
cfgFilePath = *batchCfgPath;
- assert(endsWith(cfgFilePath, Zstr(".ffs_batch")));
+ assert(endsWithAsciiNoCase(cfgFilePath, Zstr(".ffs_batch")));
}
else
{
@@ -3208,7 +3201,12 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchCfgPath) //"false": erro
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
if (fileSelector.ShowModal() != wxID_OK)
return false;
- cfgFilePath = globalCfg_.mainDlg.config.lastSelectedFile = utfTo<Zstring>(fileSelector.GetPath());
+
+ 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 XmlGuiConfig guiCfg = getConfig();
@@ -3252,24 +3250,14 @@ bool MainDialog::saveOldConfig() //"false": error/cancel
_("&Save"), _("Do&n't save")))
{
case QuestionButton2::yes: //save
- try
- {
- switch (getXmlType(activeCfgFilePath)) //throw FileError
- {
- case XmlType::gui:
- return trySaveConfig(&activeCfgFilePath); //"false": error/cancel
- case XmlType::batch:
- return trySaveBatchConfig(&activeCfgFilePath); //"false": error/cancel
- case XmlType::global:
- case XmlType::other:
- showNotificationDialog(this, DialogInfoType::error,
- PopupDialogCfg().setDetailInstructions(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(activeCfgFilePath))));
- return false;
- }
- }
- catch (const FileError& e)
+ 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(e.toString()));
+ showNotificationDialog(this, DialogInfoType::error,
+ PopupDialogCfg().setDetailInstructions(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(activeCfgFilePath))));
return false;
}
break;
@@ -4249,7 +4237,7 @@ void MainDialog::updateGui()
//*INDENT-ON*
}
- updateTopButton(*m_buttonCompare, loadImage("compare"), getVariantName(cmpVar), cmpVarIconName, false /*makeGrey*/);
+ updateTopButton(*m_buttonCompare, loadImage("compare"), getVariantName(cmpVar), cmpVarIconName, false /*makeGrey*/);
updateTopButton(*m_buttonSync, loadImage("start_sync"), getVariantName(syncVar), syncVarIconName, folderCmp_.empty());
m_panelTopButtons->Layout();
diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp
index 3ad876ac..58125051 100644
--- a/FreeFileSync/Source/ui/progress_indicator.cpp
+++ b/FreeFileSync/Source/ui/progress_indicator.cpp
@@ -944,7 +944,6 @@ void SyncProgressDialogImpl<TopLevelDialog>::onLocalKeyEvent(wxKeyEvent& event)
activeButton.Command(dummy); //simulate click
return;
}
- break;
}
event.Skip();
@@ -1645,7 +1644,6 @@ void SyncProgressDialogImpl<TopLevelDialog>::resumeFromSystray(bool userRequeste
updateStaticGui(); //restore Windows 7 task bar status (e.g. required in pause mode)
updateProgressGui(false /*allowYield*/); //restore Windows 7 task bar progress (e.g. required in pause mode)
-
if (userRequested)
{
if (parentFrame_)
diff --git a/FreeFileSync/Source/ui/progress_indicator.h b/FreeFileSync/Source/ui/progress_indicator.h
index 28b050ec..a8536df7 100644
--- a/FreeFileSync/Source/ui/progress_indicator.h
+++ b/FreeFileSync/Source/ui/progress_indicator.h
@@ -9,11 +9,8 @@
#include <functional>
#include <zen/error_log.h>
-//#include <zen/zstring.h>
#include <wx/frame.h>
-//#include "../config.h"
#include "../status_handler.h"
-//#include "../return_codes.h"
namespace fff
diff --git a/FreeFileSync/Source/ui/search_grid.cpp b/FreeFileSync/Source/ui/search_grid.cpp
index d26eb40e..cff99d12 100644
--- a/FreeFileSync/Source/ui/search_grid.cpp
+++ b/FreeFileSync/Source/ui/search_grid.cpp
@@ -16,10 +16,10 @@ using namespace fff;
namespace
{
template <bool respectCase>
-void normalizeForSeach(std::wstring& str);
+void normalizeForSearch(std::wstring& str);
template <> inline
-void normalizeForSeach<true /*respectCase*/>(std::wstring& str)
+void normalizeForSearch<true /*respectCase*/>(std::wstring& str)
{
for (wchar_t& c : str)
if (!isAsciiChar(c))
@@ -33,7 +33,7 @@ void normalizeForSeach<true /*respectCase*/>(std::wstring& str)
}
template <> inline
-void normalizeForSeach<false /*respectCase*/>(std::wstring& str)
+void normalizeForSearch<false /*respectCase*/>(std::wstring& str)
{
for (wchar_t& c : str)
if (!isAsciiChar(c))
@@ -53,14 +53,14 @@ template <bool respectCase>
class MatchFound
{
public:
- MatchFound(const std::wstring& textToFind) : textToFind_(textToFind)
+ explicit MatchFound(const std::wstring& textToFind) : textToFind_(textToFind)
{
- normalizeForSeach<respectCase>(textToFind_);
+ normalizeForSearch<respectCase>(textToFind_);
}
bool operator()(std::wstring&& phrase) const
{
- normalizeForSeach<respectCase>(phrase);
+ normalizeForSearch<respectCase>(phrase);
return contains(phrase, textToFind_);
}
diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp
index 72915145..14f78eb8 100644
--- a/FreeFileSync/Source/ui/sync_cfg.cpp
+++ b/FreeFileSync/Source/ui/sync_cfg.cpp
@@ -15,7 +15,6 @@
#include <wx+/choice_enum.h>
#include <wx+/image_tools.h>
#include <wx+/font_size.h>
-//#include <wx+/std_button_layout.h>
#include <wx+/popup_dlg.h>
#include <wx+/image_resources.h>
#include <wx+/window_tools.h>
@@ -25,7 +24,6 @@
#include "../base/norm_filter.h"
#include "../base/file_hierarchy.h"
#include "../base/icon_loader.h"
-//#include "../log_file.h"
#include "../afs/concrete.h"
#include "../base_tools.h"
@@ -38,6 +36,7 @@ using namespace fff;
namespace
{
const int CFG_DESCRIPTION_WIDTH_DIP = 230;
+const wchar_t arrowRight[] = L"\u2192"; //"RIGHTWARDS ARROW"
void initBitmapRadioButtons(const std::vector<std::pair<ToggleButton*, std::string /*imgName*/>>& buttons, bool alignLeft)
@@ -90,6 +89,113 @@ void initBitmapRadioButtons(const std::vector<std::pair<ToggleButton*, std::stri
}
}
+
+bool sanitizeFilter(FilterConfig& filterCfg, const std::vector<std::wstring>& folderDisplayPaths, wxWindow* parent)
+{
+ //include filter must not be empty!
+ if (trimCpy(filterCfg.includeFilter).empty())
+ filterCfg.includeFilter = FilterConfig().includeFilter; //no need to show error message, just correct user input
+
+
+ //replace full paths by relative ones: frequent user error => help out: https://freefilesync.org/forum/viewtopic.php?t=9225
+ auto normalizeForSearch = [](Zstring str)
+ {
+ //1. ignore Unicode normalization form 2. ignore case 3. normalize path separator
+ str = getUpperCase(str); //getUnicodeNormalForm() is implied by getUpperCase()
+
+ if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) std::replace(str.begin(), str.end(), Zstr('/'), FILE_NAME_SEPARATOR);
+ if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) std::replace(str.begin(), str.end(), Zstr('\\'), FILE_NAME_SEPARATOR);
+
+ return str;
+ };
+
+ std::vector<Zstring> folderPathsPf; //normalized + postfix path separator
+ {
+ const Zstring includeFilterNorm = normalizeForSearch(filterCfg.includeFilter);
+ const Zstring excludeFilterNorm = normalizeForSearch(filterCfg.excludeFilter);
+
+ for (const std::wstring& displayPath : folderDisplayPaths)
+ if (!displayPath.empty())
+ if (const Zstring pathNormPf = appendSeparator(normalizeForSearch(utfTo<Zstring>(displayPath)));
+ contains(includeFilterNorm, pathNormPf) || //perf!?
+ contains(excludeFilterNorm, pathNormPf)) //
+ folderPathsPf.push_back(pathNormPf);
+
+ removeDuplicates(folderPathsPf);
+ }
+
+ if (!folderPathsPf.empty())
+ {
+ std::vector<std::pair<Zstring /*from*/, Zstring /*to*/>> replacements;
+
+ auto replaceFullPaths = [&](Zstring& filterPhrase)
+ {
+ Zstring filterPhraseNew;
+ const Zchar* itFilterOrig = filterPhrase.begin();
+
+ split2(filterPhrase, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n'); }, //delimiters
+ [&](const Zchar* blockFirst, const Zchar* blockLast)
+ {
+ std::tie(blockFirst, blockLast) = trimCpy(blockFirst, blockLast, true /*fromLeft*/, true /*fromRight*/, [](Zchar c) { return isWhiteSpace(c); });
+ if (blockFirst != blockLast)
+ {
+ const Zstring phrase{blockFirst, blockLast};
+
+ for (const Zstring& pathNormPf : folderPathsPf)
+ if (startsWith(normalizeForSearch(phrase), pathNormPf))
+ {
+ //emulate a "normalized afterFirst()":
+ ptrdiff_t sepCount = std::count(pathNormPf.begin(), pathNormPf.end(), FILE_NAME_SEPARATOR);
+ assert(sepCount > 0);
+
+ for (auto it = phrase.begin(); it != phrase.end(); ++it)
+ if (*it == Zstr('/') ||
+ *it == Zstr('\\'))
+ if (--sepCount == 0)
+ {
+ const Zstring relPath(it, phrase.end()); //include first path separator
+
+ filterPhraseNew.append(itFilterOrig, blockFirst);
+ filterPhraseNew += relPath;
+ itFilterOrig = blockLast;
+
+ replacements.emplace_back(phrase, relPath);
+ return; //... to next block
+ }
+ throw std::logic_error("Contract violation! " + std::string(__FILE__) + ':' + numberTo<std::string>(__LINE__));
+ }
+ }
+ });
+ filterPhraseNew.append(itFilterOrig, filterPhrase.cend());
+
+ filterPhrase = filterPhraseNew;
+ };
+ replaceFullPaths(filterCfg.includeFilter);
+ replaceFullPaths(filterCfg.excludeFilter);
+
+ assert(!replacements.empty());
+ if (!replacements.empty())
+ {
+ std::wstring detailsMsg;
+ for (const auto& [from, to] : replacements)
+ detailsMsg += utfTo<std::wstring>(from) + L' ' + arrowRight + L' ' + utfTo<std::wstring>(to) + L'\n';
+ detailsMsg.pop_back();
+
+ switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg().
+ setMainInstructions(_("Each filter item must be a path relative to the base folders. The following changes are suggested:")).
+ setDetailInstructions(detailsMsg), _("&Change")))
+ {
+ case ConfirmationButton::accept: //change
+ break;
+
+ case ConfirmationButton::cancel:
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
//==========================================================================
class ConfigDialog : public ConfigDlgGenerated
@@ -256,6 +362,9 @@ private:
GlobalPairConfig globalPairCfg_;
std::vector<LocalPairConfig> localPairCfg_;
+ //display paths to fix filter if user pastes full folder paths
+ std::vector<std::wstring> folderDisplayPaths_;
+
int selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED;
static const int EMPTY_PAIR_INDEX_SELECTED = -2;
@@ -537,12 +646,18 @@ globalLogFolderPhrase_(globalLogFolderPhrase)
m_listBoxFolderPair->Append(_("All folder pairs"));
for (const LocalPairConfig& lpc : localPairCfg)
{
- std::wstring fpName = getShortDisplayNameForFolderPair(createAbstractPath(lpc.folderPathPhraseLeft ),
- createAbstractPath(lpc.folderPathPhraseRight));
+ const AbstractPath folderPathL= createAbstractPath(lpc.folderPathPhraseLeft);
+ const AbstractPath folderPathR= createAbstractPath(lpc.folderPathPhraseRight);
+
+ std::wstring fpName = getShortDisplayNameForFolderPair(folderPathL, folderPathR);
if (trimCpy(fpName).empty())
fpName = L"<" + _("empty") + L">";
- m_listBoxFolderPair->Append(L" " + fpName);
+ m_listBoxFolderPair->Append(TAB_SPACE + fpName);
+
+ //collect display paths for filter correction
+ if (!AFS::isNullPath(folderPathL)) folderDisplayPaths_.push_back(AFS::getDisplayPath(folderPathL));
+ if (!AFS::isNullPath(folderPathR)) folderDisplayPaths_.push_back(AFS::getDisplayPath(folderPathR));
}
if (!showMultipleCfgs)
@@ -553,13 +668,13 @@ globalLogFolderPhrase_(globalLogFolderPhrase)
//temporarily set main config as reference for window height calculations:
globalPairCfg_ = GlobalPairConfig();
- globalPairCfg_.syncCfg.directionCfg.var = SyncVariant::mirror; //
+ globalPairCfg_.syncCfg.directionCfg.var = SyncVariant::mirror; //
globalPairCfg_.syncCfg.handleDeletion = DeletionPolicy::versioning; //
globalPairCfg_.syncCfg.versioningFolderPhrase = Zstr("dummy"); //set tentatively for sync dir height calculation below
globalPairCfg_.syncCfg.versioningStyle = VersioningStyle::timestampFile; //
globalPairCfg_.syncCfg.versionMaxAgeDays = 30; //
globalPairCfg_.miscCfg.altLogFolderPathPhrase = Zstr("dummy"); //
- globalPairCfg_.miscCfg.emailNotifyAddress = "dummy"; //
+ globalPairCfg_.miscCfg.emailNotifyAddress = "dummy"; //
selectFolderPairConfig(-1);
@@ -705,7 +820,7 @@ std::optional<CompConfig> ConfigDialog::getCompConfig() const
CompConfig compCfg;
compCfg.compareVar = localCmpVar_;
- compCfg.handleSymlinks = !m_checkBoxSymlinksInclude->GetValue() ? SymLinkHandling::exclude : m_radioBtnSymlinksDirect->GetValue() ? SymLinkHandling::direct : SymLinkHandling::follow;
+ compCfg.handleSymlinks = !m_checkBoxSymlinksInclude->GetValue() ? SymLinkHandling::exclude : m_radioBtnSymlinksDirect->GetValue() ? SymLinkHandling::asLink : SymLinkHandling::follow;
compCfg.ignoreTimeShiftMinutes = fromTimeShiftPhrase(copyStringTo<std::wstring>(m_textCtrlTimeShift->GetValue()));
return compCfg;
@@ -731,7 +846,7 @@ void ConfigDialog::setCompConfig(const CompConfig* compCfg)
m_checkBoxSymlinksInclude->SetValue(true);
m_radioBtnSymlinksFollow->SetValue(true);
break;
- case SymLinkHandling::direct:
+ case SymLinkHandling::asLink:
m_checkBoxSymlinksInclude->SetValue(true);
m_radioBtnSymlinksDirect->SetValue(true);
break;
@@ -1497,7 +1612,7 @@ void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow)
}
else
{
- setCompConfig(get(localPairCfg_[selectedPairIndexToShow_].localCmpCfg ));
+ setCompConfig(get(localPairCfg_[selectedPairIndexToShow_].localCmpCfg));
setSyncConfig(get(localPairCfg_[selectedPairIndexToShow_].localSyncCfg));
setFilterConfig (localPairCfg_[selectedPairIndexToShow_].localFilter);
}
@@ -1508,9 +1623,9 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams)
{
assert(selectedPairIndexToShow_ == -1 || makeUnsigned(selectedPairIndexToShow_) < localPairCfg_.size());
- std::optional<CompConfig> compCfg = getCompConfig();
- std::optional<SyncConfig> syncCfg = getSyncConfig();
- FilterConfig filterCfg = getFilterConfig();
+ std::optional<CompConfig> compCfg = getCompConfig();
+ std::optional<SyncConfig> syncCfg = getSyncConfig();
+ FilterConfig filterCfg = getFilterConfig();
std::optional<MiscSyncConfig> miscCfg;
if (selectedPairIndexToShow_ < 0)
@@ -1519,9 +1634,13 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams)
//------- parameter validation (BEFORE writing output!) -------
if (validateParams)
{
- //parameter correction: include filter must not be empty!
- if (trimCpy(filterCfg.includeFilter).empty())
- filterCfg.includeFilter = FilterConfig().includeFilter; //no need to show error message, just correct user input
+ //parameter validation and correction:
+ if (!sanitizeFilter(filterCfg, folderDisplayPaths_, this))
+ {
+ m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::filter));
+ m_textCtrlExclude->SetFocus();
+ return false;
+ }
if (syncCfg && syncCfg->handleDeletion == DeletionPolicy::versioning)
{
diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp
index c51507f0..3e4f80cb 100644
--- a/FreeFileSync/Source/ui/version_check.cpp
+++ b/FreeFileSync/Source/ui/version_check.cpp
@@ -68,6 +68,12 @@ time_t getVersionCheckCurrentTime()
time_t now = std::time(nullptr);
return now;
}
+
+
+void openBrowserForDownload(wxWindow* parent)
+{
+ wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php");
+}
}
@@ -126,7 +132,7 @@ std::vector<std::pair<std::string, std::string>> geHttpPostParameters(wxWindow&
const OsVersion osv = getOsVersion();
params.emplace_back("os_version", numberTo<std::string>(osv.major) + "." + numberTo<std::string>(osv.minor));
- const char* osArch = BuildArch::program == BuildArch::bit32 ? "32" : "64";
+ const char* osArch = cpuArchName;
params.emplace_back("os_arch", osArch);
#if GTK_MAJOR_VERSION == 2
@@ -137,6 +143,17 @@ std::vector<std::pair<std::string, std::string>> geHttpPostParameters(wxWindow&
#error unknown GTK version!
#endif
+ const std::string ffsLang = []
+ {
+ const wxLanguage lang = getLanguage();
+
+ for (const TranslationInfo& ti : getAvailableTranslations())
+ if (ti.languageID == lang)
+ return ti.locale;
+ return std::string("zz");
+ }();
+ params.emplace_back("ffs_lang", ffsLang);
+
params.emplace_back("language", utfTo<std::string>(getIso639Language()));
params.emplace_back("country", utfTo<std::string>(getIso3166Country()));
@@ -157,7 +174,6 @@ void showUpdateAvailableDialog(wxWindow* parent, const std::string& onlineVersio
catch (const SysError& e) { updateDetailsMsg = _("Failed to retrieve update information.") + + L"\n\n" + e.toString(); }
- std::function<void()> openBrowserForDownload = [] { wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); };
switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg().
setIcon(loadImage("FreeFileSync", fastFromDIP(48))).
setTitle(_("Check for Program Updates")).
@@ -250,7 +266,7 @@ void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion)
setDetailInstructions(e.toString()), _("&Check"), _("&Retry")))
{
case ConfirmationButton2::accept:
- wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php");
+ openBrowserForDownload(&parent);
break;
case ConfirmationButton2::accept2: //retry
checkForUpdateNow(parent, lastOnlineVersion); //note: retry via recursion!!!
@@ -350,7 +366,7 @@ void fff::automaticUpdateCheckEval(wxWindow& parent, time_t& lastUpdateCheck, st
_("&Check"), _("&Retry")))
{
case ConfirmationButton2::accept:
- wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php");
+ openBrowserForDownload(&parent);
break;
case ConfirmationButton2::accept2: //retry
automaticUpdateCheckEval(parent, lastUpdateCheck, lastOnlineVersion,
diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h
index cebf68d0..eea2ae14 100644
--- a/FreeFileSync/Source/version/version.h
+++ b/FreeFileSync/Source/version/version.h
@@ -3,7 +3,7 @@
namespace fff
{
-const char ffsVersion[] = "11.23"; //internal linkage!
+const char ffsVersion[] = "11.24"; //internal linkage!
const char FFS_VERSION_SEPARATOR = '.';
}
diff --git a/wx+/dc.h b/wx+/dc.h
index e22820a7..48d0fe72 100644
--- a/wx+/dc.h
+++ b/wx+/dc.h
@@ -136,7 +136,7 @@ wxBitmap toScaledBitmap(const wxImage& img /*expected to be DPI-scaled!*/)
//all this shit just because wxDC::SetScaleFactor() is missing:
-inline
+inline
void setScaleFactor(wxDC& dc, double scale)
{
struct wxDcSurgeon : public wxDCImpl
diff --git a/wx+/graph.cpp b/wx+/graph.cpp
index eb9256f4..f9094386 100644
--- a/wx+/graph.cpp
+++ b/wx+/graph.cpp
@@ -232,7 +232,7 @@ void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Grap
//calculate intersection of polygon with half-plane
template <class Function, class Function2>
-void cutPoints(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut)
+void cutPoints(std::vector<CurvePoint>& curvePoints, std::vector<unsigned char>& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut)
{
assert(curvePoints.size() == oobMarker.size());
@@ -240,8 +240,8 @@ void cutPoints(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarke
auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; }; //test if point is start of an OOB line
- std::vector<CurvePoint> curvePointsTmp;
- std::vector<char> oobMarkerTmp;
+ std::vector<CurvePoint> curvePointsTmp;
+ std::vector<unsigned char> oobMarkerTmp;
curvePointsTmp.reserve(curvePoints.size()); //allocating memory for these containers is one
oobMarkerTmp .reserve(oobMarker .size()); //of the more expensive operations of Graph2D!
@@ -308,13 +308,13 @@ private:
const double y_;
};
-void cutPointsOutsideX(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarker, double minX, double maxX, bool doPolygonCut)
+void cutPointsOutsideX(std::vector<CurvePoint>& curvePoints, std::vector<unsigned char>& oobMarker, double minX, double maxX, bool doPolygonCut)
{
cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX), doPolygonCut);
cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX), doPolygonCut);
}
-void cutPointsOutsideY(std::vector<CurvePoint>& curvePoints, std::vector<char>& oobMarker, double minY, double maxY, bool doPolygonCut)
+void cutPointsOutsideY(std::vector<CurvePoint>& curvePoints, std::vector<unsigned char>& oobMarker, double minY, double maxY, bool doPolygonCut)
{
cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY), doPolygonCut);
cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY), doPolygonCut);
@@ -615,8 +615,8 @@ void Graph2D::render(wxDC& dc) const
double minY = attr_.minY ? *attr_.minY : std::numeric_limits<double>::infinity(); //automatic: ensure values are initialized by first curve
double maxY = attr_.maxY ? *attr_.maxY : -std::numeric_limits<double>::infinity(); //
- std::vector<std::vector<CurvePoint>> curvePoints(curves_.size());
- std::vector<std::vector<char>> oobMarker (curves_.size()); //effectively a std::vector<bool> marking points that start an out-of-bounds line
+ std::vector<std::vector<CurvePoint>> curvePoints(curves_.size());
+ std::vector<std::vector<unsigned char>> oobMarker (curves_.size()); //effectively a std::vector<bool> marking points that start an out-of-bounds line
for (size_t index = 0; index < curves_.size(); ++index)
{
diff --git a/wx+/grid.cpp b/wx+/grid.cpp
index a32de84e..5d2adc1a 100644
--- a/wx+/grid.cpp
+++ b/wx+/grid.cpp
@@ -164,7 +164,7 @@ void GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& te
if (extentTrunc.GetWidth() > rect.width)
{
- //unlike Windows Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfTo<std::wstring>("\xf0\xa4\xbd\x9c");
+ //unlike Windows Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideograph encodes to TWO wchar_t: utfTo<std::wstring>("\xf0\xa4\xbd\x9c");
size_t low = 0; //number of unicode chars!
size_t high = unicodeLength(text); //
if (high > 1)
@@ -285,7 +285,7 @@ public:
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent& event) { onMouseCaptureLost(event); });
Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); });
- Bind(wxEVT_KEY_UP, [this](wxKeyEvent& event) { onKeyUp (event); });
+ //Bind(wxEVT_KEY_UP, [this](wxKeyEvent& event) { onKeyUp (event); }); -> superfluous?
assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below
}
@@ -340,12 +340,6 @@ private:
event.Skip();
}
- void onKeyUp(wxKeyEvent& event)
- {
- if (!sendEventToParent(event)) //let parent collect all key events
- event.Skip();
- }
-
void onMouseWheel(wxMouseEvent& event)
{
/* MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated.
@@ -969,6 +963,17 @@ public:
colLabelWin_(colLabelWin)
{
Bind(EVENT_GRID_HAS_SCROLLED, [this](wxCommandEvent& event) { onRequestWindowUpdate(event); });
+
+ Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event)
+ {
+ if (event.GetKeyCode() == WXK_ESCAPE && activeSelection_) //allow Escape key to cancel active selection!
+ {
+ wxMouseCaptureLostEvent evt;
+ GetEventHandler()->ProcessEvent(evt); //better integrate into event handling rather than calling onMouseCaptureLost() directly!?
+ }
+ else
+ event.Skip();
+ });
}
~MainWin() { assert(!gridUpdatePending_); }
@@ -1099,6 +1104,13 @@ private:
void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
{
+ if (activeSelection_) //allow other mouse button to cancel active selection!
+ {
+ wxMouseCaptureLostEvent evt;
+ GetEventHandler()->ProcessEvent(evt);
+ return;
+ }
+
if (auto prov = refParent().getDataProvider())
{
evalMouseMovement(event.GetPosition()); //update highlight in obscure cases (e.g. right-click while other context menu is open)
diff --git a/wx+/grid.h b/wx+/grid.h
index 867abf35..3b3e76b7 100644
--- a/wx+/grid.h
+++ b/wx+/grid.h
@@ -10,7 +10,6 @@
#include <memory>
#include <numeric>
#include <optional>
-//#include <set>
#include <vector>
#include <zen/stl_tools.h>
#include <wx/scrolwin.h>
diff --git a/wx+/image_resources.cpp b/wx+/image_resources.cpp
index 36055f3f..58ae4d25 100644
--- a/wx+/image_resources.cpp
+++ b/wx+/image_resources.cpp
@@ -70,7 +70,7 @@ ImageHolder xbrzScale(int width, int height, const unsigned char* imageRgb, cons
}
-auto getScalerTask(const std::string& imageName, const wxImage& img, int hqScale, Protected<std::vector<std::pair<std::string, ImageHolder>>>& result)
+auto createScalerTask(const std::string& imageName, const wxImage& img, int hqScale, Protected<std::vector<std::pair<std::string, ImageHolder>>>& result)
{
assert(runningOnMainThread());
return [imageName,
@@ -97,7 +97,7 @@ public:
{
assert(runningOnMainThread());
imgKeeper_.push_back(img); //retain (ref-counted) wxImage so that the rgb/alpha pointers remain valid after passed to threads
- threadGroup_->run(getScalerTask(imageName, img, hqScale_, result_));
+ threadGroup_->run(createScalerTask(imageName, img, hqScale_, result_));
}
std::unordered_map<std::string, wxImage> waitAndGetResult()
@@ -125,7 +125,7 @@ private:
std::vector<wxImage> imgKeeper_;
Protected<std::vector<std::pair<std::string, ImageHolder>>> result_;
- using TaskType = FunctionReturnTypeT<decltype(&getScalerTask)>;
+ using TaskType = FunctionReturnTypeT<decltype(&createScalerTask)>;
std::optional<ThreadGroup<TaskType>> threadGroup_{ThreadGroup<TaskType>(std::max<int>(std::thread::hardware_concurrency(), 1), Zstr("xBRZ Scaler"))};
//hardware_concurrency() == 0 if "not computable or well defined"
};
@@ -192,11 +192,13 @@ ImageBuffer::ImageBuffer(const Zstring& zipPath) //throw FileError
else
assert(false);
}
- catch (FileError&) //fall back to folder
+ catch (FileError&) //fall back to folder: dev build (only!?)
{
const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none);
- if (dirAvailable(fallbackFolder)) //Debug build (only!?)
- traverseFolder(fallbackFolder, [&](const FileInfo& fi)
+ if (!itemStillExists(fallbackFolder)) //throw FileError
+ throw;
+
+ traverseFolder(fallbackFolder, [&](const FileInfo& fi)
{
if (endsWith(fi.fullPath, Zstr(".png")))
{
@@ -204,8 +206,6 @@ ImageBuffer::ImageBuffer(const Zstring& zipPath) //throw FileError
streams.emplace_back(fi.itemName, std::move(stream));
}
}, nullptr, nullptr, [](const std::wstring& errorMsg) { throw FileError(errorMsg); });
- else
- throw;
}
//--------------------------------------------------------------------
diff --git a/wx+/no_flicker.h b/wx+/no_flicker.h
index d8f2d6cd..7fa4ae23 100644
--- a/wx+/no_flicker.h
+++ b/wx+/no_flicker.h
@@ -85,20 +85,26 @@ void setTextWithUrls(wxRichTextCtrl& richCtrl, const wxString& newText)
urlStyle.SetTextColour(*wxBLUE);
urlStyle.SetFontUnderlined(true);
- for (const auto& [type, text] : blocks)
+ for (auto& [type, text] : blocks)
switch (type)
{
case BlockType::text:
+ if (endsWith(text, L"\n\n")) //bug: multiple newlines before a URL are condensed to only one;
+ //Why? fuck knows why! no such issue with double newlines *after* URL => hack this shit
+ text.RemoveLast().Append(ZERO_WIDTH_SPACE).Append(L'\n');
+
richCtrl.WriteText(text);
break;
case BlockType::url:
+ {
richCtrl.BeginStyle(urlStyle);
ZEN_ON_SCOPE_EXIT(richCtrl.EndStyle());
richCtrl.BeginURL(text);
ZEN_ON_SCOPE_EXIT(richCtrl.EndURL());
richCtrl.WriteText(text);
- break;
+ }
+ break;
}
if (std::any_of(blocks.begin(), blocks.end(), [](const auto& item) { return item.first == BlockType::url; }))
diff --git a/wx+/popup_dlg.cpp b/wx+/popup_dlg.cpp
index 0a4c75c0..dfa5494f 100644
--- a/wx+/popup_dlg.cpp
+++ b/wx+/popup_dlg.cpp
@@ -10,6 +10,7 @@
#include <wx/app.h>
#include <wx/display.h>
#include <wx/sound.h>
+#include "app_main.h"
#include "bitmap_button.h"
#include "no_flicker.h"
#include "font_size.h"
diff --git a/wx+/rtl.h b/wx+/rtl.h
index 42af52e1..80a75671 100644
--- a/wx+/rtl.h
+++ b/wx+/rtl.h
@@ -66,22 +66,22 @@ void drawBitmapRtlMirror(wxDC& dc, const wxImage& img, const wxRect& rect, int a
case wxLayout_RightToLeft:
if (rect.GetWidth() > 0 && rect.GetHeight() > 0)
- {
- if (!buffer || buffer->GetSize() != rect.GetSize()) //[!] since we do a mirror, width needs to match exactly!
- buffer.emplace(rect.GetSize());
+ {
+ if (!buffer || buffer->GetSize() != rect.GetSize()) //[!] since we do a mirror, width needs to match exactly!
+ buffer.emplace(rect.GetSize());
- if (buffer->GetScaleFactor() != dc.GetContentScaleFactor()) //needed here?
- buffer->SetScaleFactor(dc.GetContentScaleFactor()); //
+ if (buffer->GetScaleFactor() != dc.GetContentScaleFactor()) //needed here?
+ buffer->SetScaleFactor(dc.GetContentScaleFactor()); //
- wxMemoryDC memDc(*buffer); //copies scale factor from wxBitmap
- memDc.Blit(wxPoint(0, 0), rect.GetSize(), &dc, rect.GetTopLeft()); //blit in: background is mirrored due to memDc, dc having different layout direction!
+ wxMemoryDC memDc(*buffer); //copies scale factor from wxBitmap
+ memDc.Blit(wxPoint(0, 0), rect.GetSize(), &dc, rect.GetTopLeft()); //blit in: background is mirrored due to memDc, dc having different layout direction!
- impl::drawBitmapAligned(memDc, img, wxRect(0, 0, rect.width, rect.height), alignment);
- //note: we cannot simply use memDc.SetLayoutDirection(wxLayout_RightToLeft) due to some strange 1 pixel bug! 2022-04-04: maybe fixed in wxWidgets 3.1.6?
+ impl::drawBitmapAligned(memDc, img, wxRect(0, 0, rect.width, rect.height), alignment);
+ //note: we cannot simply use memDc.SetLayoutDirection(wxLayout_RightToLeft) due to some strange 1 pixel bug! 2022-04-04: maybe fixed in wxWidgets 3.1.6?
- dc.Blit(rect.GetTopLeft(), rect.GetSize(), &memDc, wxPoint(0, 0)); //blit out: mirror once again
- }
- break;
+ dc.Blit(rect.GetTopLeft(), rect.GetSize(), &memDc, wxPoint(0, 0)); //blit out: mirror once again
+ }
+ break;
case wxLayout_Default: //CAVEAT: wxPaintDC/wxMemoryDC on wxGTK/wxMAC does not implement SetLayoutDirection()!!! => GetLayoutDirection() == wxLayout_Default
if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft)
diff --git a/xBRZ/src/xbrz.cpp b/xBRZ/src/xbrz.cpp
index 65dfc47e..448a4b74 100644
--- a/xBRZ/src/xbrz.cpp
+++ b/xBRZ/src/xbrz.cpp
@@ -283,20 +283,18 @@ template <class ColorDistance>
FORCE_INLINE //detect blend direction
BlendResult preProcessCorners(const Kernel_4x4& ker, const xbrz::ScalerCfg& cfg) //result: E, F, H, I corners of "GradientType"
{
-
- BlendResult result = {};
-
if ((ker.e == ker.f &&
ker.h == ker.i) ||
(ker.e == ker.h &&
ker.f == ker.i))
- return result;
+ return {};
auto dist = [&](uint32_t pix1, uint32_t pix2) { return ColorDistance::dist(pix1, pix2, cfg.testAttribute); };
const double hf = dist(ker.g, ker.e) + dist(ker.e, ker.c) + dist(ker.k, ker.i) + dist(ker.i, ker.o) + cfg.centerDirectionBias * dist(ker.h, ker.f);
const double ei = dist(ker.d, ker.h) + dist(ker.h, ker.l) + dist(ker.b, ker.f) + dist(ker.f, ker.n) + cfg.centerDirectionBias * dist(ker.e, ker.i);
+ BlendResult result = {};
if (hf < ei) //test sample: 70% of values max(hf, ei) / min(hf, ei) are between 1.1 and 3.7 with median being 1.8
{
const bool dominantGradient = cfg.dominantDirectionThreshold * hf < ei;
diff --git a/zen/build_info.h b/zen/build_info.h
index b06c1302..86ff303c 100644
--- a/zen/build_info.h
+++ b/zen/build_info.h
@@ -26,6 +26,7 @@ enum class BuildArch
static_assert((BuildArch::program == BuildArch::bit32 ? 32 : 64) == sizeof(void*) * 8);
+//harmonize with os_arch enum in update_checks table:
constexpr const char* cpuArchName = BuildArch::program == BuildArch::bit32 ? "i686": "x86-64";
}
diff --git a/zen/file_access.cpp b/zen/file_access.cpp
index 6a62f671..2e119e87 100644
--- a/zen/file_access.cpp
+++ b/zen/file_access.cpp
@@ -70,7 +70,7 @@ std::optional<ItemType> zen::itemStillExists(const Zstring& itemPath) //throw Fi
try
{
traverseFolder(*parentPath,
- [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::file; },
+ [&](const FileInfo& fi) { if (fi.itemName == itemName) throw ItemType::file; }, //case-sensitive! itemPath must be normalized!
[&](const FolderInfo& fi) { if (fi.itemName == itemName) throw ItemType::folder; },
[&](const SymlinkInfo& si) { if (si.itemName == itemName) throw ItemType::symlink; },
[](const std::wstring& errorMsg) { throw FileError(errorMsg); });
@@ -233,7 +233,6 @@ void zen::removeDirectoryPlainRecursion(const Zstring& dirPath) //throw FileErro
namespace
{
-
/* Usage overview: (avoid circular pattern!)
moveAndRenameItem() --> moveAndRenameFileSub()
@@ -319,18 +318,20 @@ void setWriteTimeNative(const Zstring& itemPath, const timespec& modTime, ProcSy
=> utimens: https://github.com/coreutils/gnulib/blob/master/lib/utimens.c
touch: https://github.com/coreutils/coreutils/blob/master/src/touch.c
=> fdutimensat: https://github.com/coreutils/gnulib/blob/master/lib/fdutimensat.c */
- timespec newTimes[2] = {};
- newTimes[0].tv_sec = ::time(nullptr); //access time; don't use UTIME_NOW/UTIME_OMIT: more bugs! https://freefilesync.org/forum/viewtopic.php?t=1701
- newTimes[1] = modTime; //modification time
+ const timespec newTimes[2]
+ {
+ {.tv_sec = ::time(nullptr)}, //access time; don't use UTIME_NOW/UTIME_OMIT: more bugs! https://freefilesync.org/forum/viewtopic.php?t=1701
+ modTime,
+ };
//test: even modTime == 0 is correctly applied (no NOOP!) test2: same behavior for "utime()"
//hell knows why files on gvfs-mounted Samba shares fail to open(O_WRONLY) returning EOPNOTSUPP:
//https://freefilesync.org/forum/viewtopic.php?t=2803 => utimensat() works (but not for gvfs SFTP)
- if (::utimensat(AT_FDCWD, itemPath.c_str(), newTimes, procSl == ProcSymlink::direct ? AT_SYMLINK_NOFOLLOW : 0) == 0)
+ if (::utimensat(AT_FDCWD, itemPath.c_str(), newTimes, procSl == ProcSymlink::asLink ? AT_SYMLINK_NOFOLLOW : 0) == 0)
return;
try
{
- if (procSl == ProcSymlink::direct)
+ if (procSl == ProcSymlink::asLink)
try
{
if (getItemType(itemPath) == ItemType::symlink) //throw FileError
@@ -554,7 +555,7 @@ void zen::copySymlink(const Zstring& sourcePath, const Zstring& targetPath) //th
if (::lstat(sourcePath.c_str(), &sourceInfo) != 0)
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(sourcePath)), "lstat");
- setWriteTimeNative(targetPath, sourceInfo.st_mtim, ProcSymlink::direct); //throw FileError
+ setWriteTimeNative(targetPath, sourceInfo.st_mtim, ProcSymlink::asLink); //throw FileError
}
diff --git a/zen/file_access.h b/zen/file_access.h
index 17c47731..f6a02edc 100644
--- a/zen/file_access.h
+++ b/zen/file_access.h
@@ -29,12 +29,7 @@ using FileIndex = ino_t;
using FileTimeNative = timespec;
inline time_t nativeFileTimeToTimeT(const timespec& ft) { return ft.tv_sec; } //follow Windows Explorer and always round down!
-inline timespec timetToNativeFileTime(time_t utcTime)
-{
- timespec natTime = {};
- natTime.tv_sec = utcTime;
- return natTime;
-}
+inline timespec timetToNativeFileTime(time_t utcTime) { return {.tv_sec = utcTime}; }
enum class ItemType
{
@@ -44,15 +39,14 @@ enum class ItemType
};
//(hopefully) fast: does not distinguish between error/not existing
ItemType getItemType(const Zstring& itemPath); //throw FileError
-//execute potentially SLOW folder traversal but distinguish error/not existing
-// assumes: - base path still exists
-// - all child item path parts must correspond to folder traversal
+//execute potentially SLOW folder traversal but distinguish error/not existing:
+// - all child item path parts must correspond to folder traversal
// => we can conclude whether an item is *not* existing anymore by doing a *case-sensitive* name search => potentially SLOW!
std::optional<ItemType> itemStillExists(const Zstring& itemPath); //throw FileError
enum class ProcSymlink
{
- direct,
+ asLink,
follow
};
void setFileTime(const Zstring& filePath, time_t modTime, ProcSymlink procSl); //throw FileError
diff --git a/zen/file_path.cpp b/zen/file_path.cpp
index 716dd8de..f5c207f3 100644
--- a/zen/file_path.cpp
+++ b/zen/file_path.cpp
@@ -13,11 +13,12 @@ std::optional<PathComponents> zen::parsePathComponents(const Zstring& itemPath)
{
auto doParse = [&](int sepCountVolumeRoot, bool rootWithSep) -> std::optional<PathComponents>
{
+ assert(sepCountVolumeRoot > 0);
const Zstring itemPathPf = appendSeparator(itemPath); //simplify analysis of root without separator, e.g. \\server-name\share
- int sepCount = 0;
+
for (auto it = itemPathPf.begin(); it != itemPathPf.end(); ++it)
if (*it == FILE_NAME_SEPARATOR)
- if (++sepCount == sepCountVolumeRoot)
+ if (--sepCountVolumeRoot == 0)
{
Zstring rootPath(itemPathPf.begin(), rootWithSep ? it + 1 : it);
@@ -89,7 +90,7 @@ bool zen::isValidRelPath(const Zstring& relPath)
if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) if (contains(relPath, Zstr('\\'))) return false;
const Zchar doubleSep[] = {FILE_NAME_SEPARATOR, FILE_NAME_SEPARATOR, 0};
- return !startsWith(relPath, FILE_NAME_SEPARATOR)&& !endsWith(relPath, FILE_NAME_SEPARATOR)&&
+ return !startsWith(relPath, FILE_NAME_SEPARATOR) && !endsWith(relPath, FILE_NAME_SEPARATOR) &&
!contains(relPath, doubleSep);
}
diff --git a/zen/file_path.h b/zen/file_path.h
index 4a85514b..85af251d 100644
--- a/zen/file_path.h
+++ b/zen/file_path.h
@@ -40,7 +40,7 @@ std::weak_ordering compareNativePath(const Zstring& lhs, const Zstring& rhs);
inline bool equalNativePath(const Zstring& lhs, const Zstring& rhs) { return compareNativePath(lhs, rhs) == std::weak_ordering::equivalent; }
-struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return std::is_lt(compareNativePath(lhs, rhs)); } };
+struct LessNativePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNativePath(lhs, rhs) < 0; } };
//------------------------------------------------------------------------------------------
diff --git a/zen/file_traverser.h b/zen/file_traverser.h
index cb7782d6..11c3eaa0 100644
--- a/zen/file_traverser.h
+++ b/zen/file_traverser.h
@@ -17,7 +17,7 @@ struct FileInfo
Zstring itemName;
Zstring fullPath;
uint64_t fileSize = 0; //[bytes]
- time_t modTime = 0; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT
};
struct FolderInfo
@@ -30,7 +30,7 @@ struct SymlinkInfo
{
Zstring itemName;
Zstring fullPath;
- time_t modTime = 0; //number of seconds since Jan. 1st 1970 UTC
+ time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT
};
//- non-recursive
diff --git a/zen/format_unit.cpp b/zen/format_unit.cpp
index 2aa6e094..8b3fccfe 100644
--- a/zen/format_unit.cpp
+++ b/zen/format_unit.cpp
@@ -168,12 +168,27 @@ std::wstring zen::formatNumber(int64_t n)
std::wstring zen::formatUtcToLocalTime(time_t utcTime)
{
- auto errorMsg = [&] { return _("Error") + L" (time_t: " + numberTo<std::wstring>(utcTime) + L')'; };
+ auto fmtFallback = [utcTime] //don't take "no" for an answer!
+ {
+ if (const TimeComp tc = getUtcTime(utcTime);
+ tc != TimeComp())
+ {
+ wchar_t buf[128] = {}; //the only way to format abnormally large or invalid modTime: std::strftime() will fail!
+ if (const int rv = std::swprintf(buf, std::size(buf), L"%d-%02d-%02d %02d:%02d:%02d GMT", tc.year, tc.month, tc.day, tc.hour, tc.minute, tc.second);
+ 0 < rv && rv < std::ssize(buf))
+ return std::wstring(buf, rv);
+ }
+
+ return L"time_t = " + numberTo<std::wstring>(utcTime);
+ };
const TimeComp& loc = getLocalTime(utcTime); //returns TimeComp() on error
- std::wstring dateString = utfTo<std::wstring>(formatTime(Zstr("%x %X"), loc));
- return !dateString.empty() ? dateString : errorMsg();
+ /*const*/ std::wstring dateTimeFmt = utfTo<std::wstring>(formatTime(Zstr("%x %X"), loc));
+ if (dateTimeFmt.empty())
+ return fmtFallback();
+
+ return dateTimeFmt;
}
@@ -188,9 +203,9 @@ WeekDay impl::getFirstDayOfWeekImpl() //throw SysError
const char* firstDay = ::nl_langinfo(_NL_TIME_FIRST_WEEKDAY); //[1-Sunday, 7-Saturday]
ASSERT_SYSERROR(firstDay && 1 <= *firstDay && *firstDay <= 7);
- const int weekDayStartSunday = *firstDay;
- const int weekDayStartMonday = (weekDayStartSunday - 1 + 6) % 7; //+6 == -1 in Z_7
- // [0-Monday, 6-Sunday]
+ const int weekDayStartSunday = *firstDay; //[1-Sunday, 7-Saturday]
+ const int weekDayStartMonday = (weekDayStartSunday - 2 + 7) % 7; //[0-Monday, 6-Sunday] 7 == 0 in Z_7
+
return static_cast<WeekDay>(weekDayStartMonday);
}
diff --git a/zen/process_exec.cpp b/zen/process_exec.cpp
index 6b670508..df41a627 100644
--- a/zen/process_exec.cpp
+++ b/zen/process_exec.cpp
@@ -176,8 +176,7 @@ std::pair<int /*exit code*/, std::string> processExecuteImpl(const Zstring& file
const auto waitTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - now).count();
- timeval tv = {};
- tv.tv_sec = static_cast<long>(waitTimeMs / 1000);
+ timeval tv{.tv_sec = static_cast<long>(waitTimeMs / 1000)};
tv.tv_usec = static_cast<long>(waitTimeMs - tv.tv_sec * 1000) * 1000;
fd_set rfd = {}; //includes FD_ZERO
diff --git a/zen/resolve_path.cpp b/zen/resolve_path.cpp
index 357dab6a..99e2f6c6 100644
--- a/zen/resolve_path.cpp
+++ b/zen/resolve_path.cpp
@@ -9,7 +9,7 @@
#include "thread.h"
#include "file_access.h"
-#include <zen/sys_info.h>
+ #include <zen/sys_info.h>
// #include <stdlib.h> //getenv()
#include <unistd.h> //getuid()
#include <pwd.h> //getpwuid_r()
@@ -63,16 +63,16 @@ Zstring resolveRelativePath(const Zstring& relativePath)
https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html */
if (startsWith(pathTmp, "~/") || pathTmp == "~")
{
- try
- {
- const Zstring& homePath = getUserHome(); //throw FileError
+ try
+ {
+ const Zstring& homePath = getUserHome(); //throw FileError
if (startsWith(pathTmp, "~/"))
pathTmp = appendPath(homePath, pathTmp.c_str() + 2);
else //pathTmp == "~"
pathTmp = homePath;
- }
- catch (FileError&) {}
+ }
+ catch (FileError&) {}
//else: error! no further processing!
}
else
diff --git a/zen/socket.h b/zen/socket.h
index 5ece29f8..d9517bd8 100644
--- a/zen/socket.h
+++ b/zen/socket.h
@@ -33,11 +33,13 @@ class Socket //throw SysError
public:
Socket(const Zstring& server, const Zstring& serviceName) //throw SysError
{
- ::addrinfo hints = {};
- hints.ai_socktype = SOCK_STREAM; //we *do* care about this one!
- hints.ai_flags = AI_ADDRCONFIG; //save a AAAA lookup on machines that can't use the returned data anyhow
+ const addrinfo hints
+ {
+ .ai_flags = AI_ADDRCONFIG, //save a AAAA lookup on machines that can't use the returned data anyhow
+ .ai_socktype = SOCK_STREAM, //we *do* care about this one!
+ };
- ::addrinfo* servinfo = nullptr;
+ addrinfo* servinfo = nullptr;
ZEN_ON_SCOPE_EXIT(if (servinfo) ::freeaddrinfo(servinfo));
const int rcGai = ::getaddrinfo(server.c_str(), serviceName.c_str(), &hints, &servinfo);
diff --git a/zen/stl_tools.h b/zen/stl_tools.h
index 2726a09d..66af8551 100644
--- a/zen/stl_tools.h
+++ b/zen/stl_tools.h
@@ -68,10 +68,10 @@ template <class Iterator, class T, class CompLess>
Iterator binarySearch(Iterator first, Iterator last, const T& value, CompLess less);
//read-only variant of std::merge; input: two sorted ranges
-template <class Iterator, class FunctionLeftOnly, class FunctionBoth, class FunctionRightOnly>
+template <class Iterator, class FunctionLeftOnly, class FunctionBoth, class FunctionRightOnly, class Compare>
void mergeTraversal(Iterator first1, Iterator last1,
Iterator first2, Iterator last2,
- FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro);
+ FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro, Compare compare);
//why, oh why is there no std::optional<T>::get()???
template <class T> inline T* get( std::optional<T>& opt) { return opt ? &*opt : nullptr; }
@@ -255,31 +255,32 @@ BidirectionalIterator1 searchLast(const BidirectionalIterator1 first1, Bid
//---------------------------------------------------------------------------------------
//read-only variant of std::merge; input: two sorted ranges
-template <class Iterator, class FunctionLeftOnly, class FunctionBoth, class FunctionRightOnly> inline
-void mergeTraversal(Iterator first1, Iterator last1,
- Iterator first2, Iterator last2,
- FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro)
+template <class Iterator, class FunctionLeftOnly, class FunctionBoth, class FunctionRightOnly, class Compare> inline
+void mergeTraversal(Iterator firstL, Iterator lastL,
+ Iterator firstR, Iterator lastR,
+ FunctionLeftOnly lo, FunctionBoth bo, FunctionRightOnly ro, Compare compare)
{
- auto itL = first1;
- auto itR = first2;
+ auto itL = firstL;
+ auto itR = firstR;
- auto finishLeft = [&] { std::for_each(itL, last1, lo); };
- auto finishRight = [&] { std::for_each(itR, last2, ro); };
+ auto finishLeft = [&] { std::for_each(itL, lastL, lo); };
+ auto finishRight = [&] { std::for_each(itR, lastR, ro); };
- if (itL == last1) return finishRight();
- if (itR == last2) return finishLeft ();
+ if (itL == lastL) return finishRight();
+ if (itR == lastR) return finishLeft ();
for (;;)
- if (itL->first < itR->first)
+ if (const std::weak_ordering cmp = compare(*itL, *itR);
+ cmp < 0)
{
lo(*itL);
- if (++itL == last1)
+ if (++itL == lastL)
return finishRight();
}
- else if (itR->first < itL->first)
+ else if (cmp > 0)
{
ro(*itR);
- if (++itR == last2)
+ if (++itR == lastR)
return finishLeft();
}
else
@@ -287,8 +288,8 @@ void mergeTraversal(Iterator first1, Iterator last1,
bo(*itL, *itR);
++itL; //
++itR; //increment BOTH before checking for end of range!
- if (itL == last1) return finishRight();
- if (itR == last2) return finishLeft ();
+ if (itL == lastL) return finishRight();
+ if (itR == lastR) return finishLeft ();
//simplify loop by placing both EOB checks at the beginning? => slightly slower
}
}
diff --git a/zen/string_base.h b/zen/string_base.h
index ace870b9..e18a0f16 100644
--- a/zen/string_base.h
+++ b/zen/string_base.h
@@ -312,9 +312,10 @@ template <class Char, template <class> class SP> bool operator==(const Zb
template <class Char, template <class> class SP> bool operator==(const Zbase<Char, SP>& lhs, const Char* rhs);
template <class Char, template <class> class SP> inline bool operator==(const Char* lhs, const Zbase<Char, SP>& rhs) { return operator==(rhs, lhs); }
-template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs);
-template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Char* rhs);
-template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Char* lhs, const Zbase<Char, SP>& rhs);
+//follow convention + compare by unsigned char; alternative: std::lexicographical_compare_three_way + reinterpret_cast<const std::make_unsigned_t<Char>*>()
+template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs) { return compareString(lhs, rhs); }
+template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Char* rhs) { return compareString(lhs, rhs); }
+template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Char* lhs, const Zbase<Char, SP>& rhs) { return compareString(lhs, rhs); }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs) { return Zbase<Char, SP>(lhs) += rhs; }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const Char* rhs) { return Zbase<Char, SP>(lhs) += rhs; }
@@ -495,30 +496,6 @@ bool operator==(const Zbase<Char, SP>& lhs, const Char* rhs)
template <class Char, template <class> class SP> inline
-std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs)
-{
- return std::lexicographical_compare_three_way(lhs.begin(), lhs.end(), //respect embedded 0
- rhs.begin(), rhs.end()); //
-}
-
-
-template <class Char, template <class> class SP> inline
-std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Char* rhs)
-{
- return std::lexicographical_compare_three_way(lhs.begin(), lhs.end(), //respect embedded 0
- rhs, rhs + strLength(rhs));
-}
-
-
-template <class Char, template <class> class SP> inline
-std::strong_ordering operator<=>(const Char* lhs, const Zbase<Char, SP>& rhs)
-{
- return std::lexicographical_compare_three_way(lhs, lhs + strLength(lhs),
- rhs.begin(), rhs.end()); //respect embedded 0
-}
-
-
-template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::length() const
{
return SP<Char>::length(rawStr_);
diff --git a/zen/string_tools.h b/zen/string_tools.h
index d3f35ce8..cafff3d5 100644
--- a/zen/string_tools.h
+++ b/zen/string_tools.h
@@ -41,7 +41,7 @@ template <class S, class T> bool endsWithAsciiNoCase(const S& str, const T& post
template <class S, class T> bool equalString (const S& lhs, const T& rhs);
template <class S, class T> bool equalAsciiNoCase(const S& lhs, const T& rhs);
-//template <class S, class T> std::strong_ordering compareString(const S& lhs, const T& rhs);
+template <class S, class T> std::strong_ordering compareString(const S& lhs, const T& rhs);
template <class S, class T> std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs); //basic case-insensitive comparison (considering A-Z only!)
//STL container predicates for std::map, std::unordered_set/map
@@ -269,10 +269,12 @@ bool equalAsciiNoCase(const S& lhs, const T& rhs)
}
-#if 0
-//support embedded 0, unlike strncmp/wcsncmp:
+namespace impl
+{
+//support embedded 0 (unlike strncmp/wcsncmp) + compare unsigned[!] char
inline std::strong_ordering strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std:: memcmp(ptr1, ptr2, num) <=> 0; }
inline std::strong_ordering strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num) <=> 0; }
+}
template <class S, class T> inline
std::strong_ordering compareString(const S& lhs, const T& rhs)
@@ -280,13 +282,12 @@ std::strong_ordering compareString(const S& lhs, const T& rhs)
const size_t lhsLen = strLength(lhs);
const size_t rhsLen = strLength(rhs);
- //length check *after* strcmpWithNulls(): we DO care about natural ordering: e.g. for "compareString(getUpperCase(lhs), getUpperCase(rhs))"
+ //length check *after* strcmpWithNulls(): we DO care about natural ordering
if (const std::strong_ordering cmp = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen));
cmp != std::strong_ordering::equal)
return cmp;
return lhsLen <=> rhsLen;
}
-#endif
template <class S, class T> inline
@@ -587,7 +588,7 @@ struct CopyStringToString
T copy(const S& src) const
{
static_assert(!std::is_same_v<std::decay_t<S>, std::decay_t<T>>);
- return T(strBegin(src), strLength(src));
+ return {strBegin(src), strLength(src)};
}
};
@@ -626,11 +627,10 @@ S printNumber(const T& format, const Num& number) //format a single number using
#endif
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
- const int BUFFER_SIZE = 128;
- GetCharTypeT<S> buffer[BUFFER_SIZE]; //zero-initialize?
- const int charsWritten = impl::saferPrintf(buffer, BUFFER_SIZE, strBegin(format), number);
+ GetCharTypeT<S> buf[128]; //zero-initialize?
+ const int charsWritten = impl::saferPrintf(buf, std::size(buf), strBegin(format), number);
- return 0 < charsWritten && charsWritten < BUFFER_SIZE ? S(buffer, charsWritten) : S();
+ return 0 < charsWritten && charsWritten < std::ssize(buf) ? S(buf, charsWritten) : S();
}
@@ -944,7 +944,7 @@ Num hashString(const S& str)
struct StringHash
{
- using is_transparent = int; //allow heterogenous lookup!
+ using is_transparent = int; //enable heterogenous lookup!
template <class String>
size_t operator()(const String& str) const { return hashString<size_t>(str); }
@@ -953,7 +953,7 @@ struct StringHash
struct StringEqual
{
- using is_transparent = int; //allow heterogenous lookup!
+ using is_transparent = int; //enable heterogenous lookup!
template <class String1, class String2>
bool operator()(const String1& lhs, const String2& rhs) const { return equalString(lhs, rhs); }
@@ -963,7 +963,7 @@ struct StringEqual
struct LessAsciiNoCase
{
template <class String>
- bool operator()(const String& lhs, const String& rhs) const { return std::is_lt(compareAsciiNoCase(lhs, rhs)); }
+ bool operator()(const String& lhs, const String& rhs) const { return compareAsciiNoCase(lhs, rhs) < 0; }
};
diff --git a/zen/string_traits.h b/zen/string_traits.h
index 1a4f4740..31c8c12c 100644
--- a/zen/string_traits.h
+++ b/zen/string_traits.h
@@ -105,8 +105,8 @@ class StringTraits
public:
enum
{
- isStringClass = hasMemberType_value_type<CleanType> &&
- hasMember_c_str <CleanType> &&
+ isStringClass = hasMemberType_value_type<CleanType>&&
+ hasMember_c_str <CleanType>&&
hasMember_length <CleanType>
};
diff --git a/zen/sys_info.cpp b/zen/sys_info.cpp
index bc1bfe62..c57464bc 100644
--- a/zen/sys_info.cpp
+++ b/zen/sys_info.cpp
@@ -111,16 +111,20 @@ ComputerModel zen::getComputerModel() //throw FileError
{
auto tryGetInfo = [](const Zstring& filePath)
{
- if (!fileAvailable(filePath))
- return std::wstring();
try
{
const std::string stream = getFileContent(filePath, nullptr /*notifyUnbufferedIO*/); //throw FileError
return utfTo<std::wstring>(trimCpy(stream));
}
- catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError
+ catch (FileError&)
+ {
+ if (!itemStillExists(filePath)) //throw FileError
+ return std::wstring();
+
+ throw;
+ }
};
- cm.model = tryGetInfo("/sys/devices/virtual/dmi/id/product_name"); //throw SysError
+ cm.model = tryGetInfo("/sys/devices/virtual/dmi/id/product_name"); //throw FileError
cm.vendor = tryGetInfo("/sys/devices/virtual/dmi/id/sys_vendor"); //
//clean up:
diff --git a/zen/thread.h b/zen/thread.h
index 42fba281..abdc6da0 100644
--- a/zen/thread.h
+++ b/zen/thread.h
@@ -445,7 +445,7 @@ private:
activeCondition_ = cv;
}
- std::atomic<bool> stopRequested_{false}; //std:atomic is uninitialized by default!!!
+ std::atomic<bool> stopRequested_{false}; //std::atomic is uninitialized by default!!!
//"The default constructor is trivial: no initialization takes place other than zero initialization of static and thread-local objects."
std::condition_variable* activeCondition_ = nullptr;
diff --git a/zen/time.h b/zen/time.h
index c2c10fd5..376765be 100644
--- a/zen/time.h
+++ b/zen/time.h
@@ -83,30 +83,32 @@ std::tm toClibTimeComponents(const TimeComp& tc)
0 <= tc.minute && tc.minute <= 59 &&
0 <= tc.second && tc.second <= 61);
- std::tm ctc = {};
- ctc.tm_year = tc.year - 1900; //years since 1900
- ctc.tm_mon = tc.month - 1; //0-11
- ctc.tm_mday = tc.day; //1-31
- ctc.tm_hour = tc.hour; //0-23
- ctc.tm_min = tc.minute; //0-59
- ctc.tm_sec = tc.second; //0-60 (including leap second)
- ctc.tm_isdst = -1; //> 0 if DST is active, == 0 if DST is not active, < 0 if the information is not available
- //ctc.tm_wday
- //ctc.tm_yday
- return ctc;
+ return
+ {
+ .tm_sec = tc.second, //0-60 (including leap second)
+ .tm_min = tc.minute, //0-59
+ .tm_hour = tc.hour, //0-23
+ .tm_mday = tc.day, //1-31
+ .tm_mon = tc.month - 1, //0-11
+ .tm_year = tc.year - 1900, //years since 1900
+ .tm_isdst = -1, //> 0 if DST is active, == 0 if DST is not active, < 0 if the information is not available
+ //.tm_wday
+ //.tm_yday
+ };
}
inline
TimeComp toZenTimeComponents(const std::tm& ctc)
{
- TimeComp tc;
- tc.year = ctc.tm_year + 1900;
- tc.month = ctc.tm_mon + 1;
- tc.day = ctc.tm_mday;
- tc.hour = ctc.tm_hour;
- tc.minute = ctc.tm_min;
- tc.second = ctc.tm_sec;
- return tc;
+ return
+ {
+ .year = ctc.tm_year + 1900,
+ .month = ctc.tm_mon + 1,
+ .day = ctc.tm_mday,
+ .hour = ctc.tm_hour,
+ .minute = ctc.tm_min,
+ .second = ctc.tm_sec,
+ };
}
@@ -235,12 +237,12 @@ std::pair<time_t, bool /*success*/> localToTimeT(const TimeComp& tc) //convert l
const int cycles400 = numeric::intDivFloor(ctc.tm_year + 1900 - 1971/*[!]*/, 400); //see utcToTimeT()
//1971: ensures resulting time_t >= 0 after time zone, DST adaption, or std::mktime will fail on Windows!
- ctc.tm_year -= 400 * cycles400;
+ ctc.tm_year -= 400 * cycles400;
const time_t locTime = std::mktime(&ctc);
if (locTime == -1)
return {};
-
+
assert(locTime > 0);
return {locTime + secsPer400Years * cycles400, true};
}
diff --git a/zen/utf.h b/zen/utf.h
index 9c9cf7d1..ca231602 100644
--- a/zen/utf.h
+++ b/zen/utf.h
@@ -7,8 +7,6 @@
#ifndef UTF_H_01832479146991573473545
#define UTF_H_01832479146991573473545
-//#include <cstdint>
-//#include <iterator>
#include "string_tools.h" //copyStringTo
@@ -45,8 +43,8 @@ using CodePoint = uint32_t;
using Char16 = uint16_t;
using Char8 = uint8_t;
-const CodePoint LEAD_SURROGATE = 0xd800;
-const CodePoint TRAIL_SURROGATE = 0xdc00; //== LEAD_SURROGATE_MAX + 1
+const CodePoint LEAD_SURROGATE = 0xd800; //1101 1000 0000 0000 LEAD_SURROGATE_MAX = TRAIL_SURROGATE - 1
+const CodePoint TRAIL_SURROGATE = 0xdc00; //1101 1100 0000 0000
const CodePoint TRAIL_SURROGATE_MAX = 0xdfff;
const CodePoint REPLACEMENT_CHAR = 0xfffd;
@@ -62,31 +60,17 @@ void codePointToUtf16(CodePoint cp, Function writeOutput) //"writeOutput" is a u
if (cp < LEAD_SURROGATE)
writeOutput(static_cast<Char16>(cp));
else if (cp <= TRAIL_SURROGATE_MAX) //invalid code point
- codePointToUtf16(REPLACEMENT_CHAR, writeOutput); //resolves to 1-character utf16
- else if (cp < 0x10000)
+ writeOutput(static_cast<Char16>(REPLACEMENT_CHAR));
+ else if (cp <= 0xffff)
writeOutput(static_cast<Char16>(cp));
else if (cp <= CODE_POINT_MAX)
{
cp -= 0x10000;
writeOutput(static_cast<Char16>( LEAD_SURROGATE + (cp >> 10)));
- writeOutput(static_cast<Char16>(TRAIL_SURROGATE + (cp & 0x3ff)));
+ writeOutput(static_cast<Char16>(TRAIL_SURROGATE + (cp & 0b11'1111'1111)));
}
else //invalid code point
- codePointToUtf16(REPLACEMENT_CHAR, writeOutput); //resolves to 1-character utf16
-}
-
-
-inline
-size_t getUtf16Len(Char16 ch) //ch must be first code unit! returns 0 on error!
-{
- if (ch < LEAD_SURROGATE)
- return 1;
- else if (ch < TRAIL_SURROGATE)
- return 2;
- else if (ch <= TRAIL_SURROGATE_MAX)
- return 0; //unexpected trail surrogate!
- else
- return 1;
+ writeOutput(static_cast<Char16>(REPLACEMENT_CHAR));
}
@@ -102,17 +86,14 @@ public:
const Char16 ch = *it_++;
CodePoint cp = ch;
- switch (getUtf16Len(ch))
- {
- case 0: //invalid utf16 character
- cp = REPLACEMENT_CHAR;
- break;
- case 1:
- break;
- case 2:
- decodeTrail(cp);
- break;
- }
+
+ if (ch < LEAD_SURROGATE || ch > TRAIL_SURROGATE_MAX) //single Char16, no surrogates
+ ;
+ else if (ch < TRAIL_SURROGATE) //two Char16: lead and trail surrogates
+ decodeTrail(cp); //no range check needed: cp is inside [U+010000, U+10FFFF] by construction
+ else //unexpected trail surrogate
+ cp = REPLACEMENT_CHAR;
+
return cp;
}
@@ -141,46 +122,37 @@ private:
template <class Function> inline
void codePointToUtf8(CodePoint cp, Function writeOutput) //"writeOutput" is a unary function taking a Char8
{
- //https://en.wikipedia.org/wiki/UTF-8
- //assert(cp < LEAD_SURROGATE || TRAIL_SURROGATE_MAX < cp); //code points [0xd800, 0xdfff] are reserved for UTF-16 and *should* not be encoded in UTF-8
+ /* https://en.wikipedia.org/wiki/UTF-8
+ "high and low surrogate halves used by UTF-16 (U+D800 through U+DFFF) and
+ code points not encodable by UTF-16 (those after U+10FFFF) [...] must be treated as an invalid byte sequence" */
- if (cp < 0x80)
+ if (cp <= 0b111'1111)
writeOutput(static_cast<Char8>(cp));
- else if (cp < 0x800)
+ else if (cp <= 0b0111'1111'1111)
{
- writeOutput(static_cast<Char8>((cp >> 6 ) | 0xc0));
- writeOutput(static_cast<Char8>((cp & 0x3f) | 0x80));
+ writeOutput(static_cast<Char8>((cp >> 6) | 0b1100'0000)); //110x xxxx
+ writeOutput(static_cast<Char8>((cp & 0b11'1111) | 0b1000'0000)); //10xx xxxx
}
- else if (cp < 0x10000)
+ else if (cp <= 0b1111'1111'1111'1111)
{
- writeOutput(static_cast<Char8>( (cp >> 12 ) | 0xe0));
- writeOutput(static_cast<Char8>(((cp >> 6) & 0x3f) | 0x80));
- writeOutput(static_cast<Char8>( (cp & 0x3f) | 0x80));
+ if (LEAD_SURROGATE <= cp && cp <= TRAIL_SURROGATE_MAX) //[0xd800, 0xdfff]
+ codePointToUtf8(REPLACEMENT_CHAR, writeOutput);
+ else
+ {
+ writeOutput(static_cast<Char8>( (cp >> 12) | 0b1110'0000)); //1110 xxxx
+ writeOutput(static_cast<Char8>(((cp >> 6) & 0b11'1111) | 0b1000'0000)); //10xx xxxx
+ writeOutput(static_cast<Char8>( (cp & 0b11'1111) | 0b1000'0000)); //10xx xxxx
+ }
}
else if (cp <= CODE_POINT_MAX)
{
- writeOutput(static_cast<Char8>( (cp >> 18 ) | 0xf0));
- writeOutput(static_cast<Char8>(((cp >> 12) & 0x3f) | 0x80));
- writeOutput(static_cast<Char8>(((cp >> 6) & 0x3f) | 0x80));
- writeOutput(static_cast<Char8>( (cp & 0x3f) | 0x80));
+ writeOutput(static_cast<Char8>( (cp >> 18) | 0b1111'0000)); //1111 0xxx
+ writeOutput(static_cast<Char8>(((cp >> 12) & 0b11'1111) | 0b1000'0000)); //10xx xxxx
+ writeOutput(static_cast<Char8>(((cp >> 6) & 0b11'1111) | 0b1000'0000)); //10xx xxxx
+ writeOutput(static_cast<Char8>( (cp & 0b11'1111) | 0b1000'0000)); //10xx xxxx
}
else //invalid code point
- codePointToUtf8(REPLACEMENT_CHAR, writeOutput); //resolves to 3-byte utf8
-}
-
-
-inline
-size_t getUtf8Len(Char8 ch) //ch must be first code unit! returns 0 on error!
-{
- if (ch < 0x80)
- return 1;
- if (ch >> 5 == 0x6)
- return 2;
- if (ch >> 4 == 0xe)
- return 3;
- if (ch >> 3 == 0x1e)
- return 4;
- return 0; //invalid begin of UTF8 encoding
+ codePointToUtf8(REPLACEMENT_CHAR, writeOutput); //resolves to 3-byte UTF8
}
@@ -196,30 +168,34 @@ public:
const Char8 ch = *it_++;
CodePoint cp = ch;
- switch (getUtf8Len(ch))
+
+ if (ch < 0x80) //1 byte
+ ;
+ else if (ch >> 5 == 0b110) //2 bytes
{
- case 0: //invalid utf8 character
- cp = REPLACEMENT_CHAR;
- break;
- case 1:
- break;
- case 2:
- cp &= 0x1f;
- decodeTrail(cp);
- break;
- case 3:
- cp &= 0xf;
- if (decodeTrail(cp))
- decodeTrail(cp);
- break;
- case 4:
- cp &= 0x7;
- if (decodeTrail(cp))
- if (decodeTrail(cp))
- decodeTrail(cp);
- if (cp > CODE_POINT_MAX) cp = REPLACEMENT_CHAR;
- break;
+ cp &= 0b1'1111;
+ if (decodeTrail(cp))
+ if (cp <= 0b111'1111) //overlong encoding: "correct encoding of a code point uses only the minimum number of bytes required"
+ cp = REPLACEMENT_CHAR;
}
+ else if (ch >> 4 == 0b1110) //3 bytes
+ {
+ cp &= 0b1111;
+ if (decodeTrail(cp) && decodeTrail(cp))
+ if (cp <= 0b0111'1111'1111 ||
+ (LEAD_SURROGATE <= cp && cp <= TRAIL_SURROGATE_MAX)) //[0xd800, 0xdfff] are invalid code points
+ cp = REPLACEMENT_CHAR;
+ }
+ else if (ch >> 3 == 0b11110) //4 bytes
+ {
+ cp &= 0b111;
+ if (decodeTrail(cp) && decodeTrail(cp) && decodeTrail(cp))
+ if (cp <= 0b1111'1111'1111'1111 || cp > CODE_POINT_MAX)
+ cp = REPLACEMENT_CHAR;
+ }
+ else //invalid begin of UTF8 encoding
+ cp = REPLACEMENT_CHAR;
+
return cp;
}
@@ -229,9 +205,9 @@ private:
if (it_ != last_) //trail surrogate expected!
{
const Char8 ch = *it_;
- if (ch >> 6 == 0x2) //trail surrogate expected!
+ if (ch >> 6 == 0b10) //trail surrogate expected!
{
- cp = (cp << 6) + (ch & 0x3f);
+ cp = (cp << 6) + (ch & 0b11'1111);
++it_;
return true;
}
@@ -337,7 +313,9 @@ UtfString getUnicodeSubstring(const UtfString& str, size_t uniPosFirst, size_t u
assert(uniPosFirst <= uniPosLast && uniPosLast <= unicodeLength(str));
using namespace impl;
using CharType = GetCharTypeT<UtfString>;
+
UtfString output;
+ assert(uniPosFirst <= uniPosLast);
if (uniPosFirst >= uniPosLast) //optimize for empty range
return output;
@@ -357,6 +335,10 @@ UtfString getUnicodeSubstring(const UtfString& str, size_t uniPosFirst, size_t u
namespace impl
{
template <class TargetString, class SourceString> inline
+TargetString utfTo(const SourceString& str, std::true_type) { return copyStringTo<TargetString>(str); }
+
+
+template <class TargetString, class SourceString> inline
TargetString utfTo(const SourceString& str, std::false_type)
{
using CharSrc = GetCharTypeT<SourceString>;
@@ -371,10 +353,6 @@ TargetString utfTo(const SourceString& str, std::false_type)
return output;
}
-
-
-template <class TargetString, class SourceString> inline
-TargetString utfTo(const SourceString& str, std::true_type) { return copyStringTo<TargetString>(str); }
}
diff --git a/zen/zstring.cpp b/zen/zstring.cpp
index 76c0a81f..1e29e461 100644
--- a/zen/zstring.cpp
+++ b/zen/zstring.cpp
@@ -11,46 +11,44 @@
using namespace zen;
-Zstring getUnicodeNormalForm(const Zstring& str)
+Zstring getUnicodeNormalFormNonAscii(const Zstring& str)
{
- //fast pre-check:
- if (isAsciiString(str)) //perf: in the range of 3.5ns
- return str;
- static_assert(std::is_same_v<decltype(str), const Zbase<Zchar>&>, "god bless our ref-counting! => save output string memory consumption!");
-
//Example: const char* decomposed = "\x6f\xcc\x81";
// const char* precomposed = "\xc3\xb3";
+ assert(!isAsciiString(str));
+ assert(str.find(Zchar('\0')) == Zstring::npos); //don't expect embedded nulls!
+
try
{
gchar* outStr = ::g_utf8_normalize(str.c_str(), str.length(), G_NORMALIZE_DEFAULT_COMPOSE);
if (!outStr)
- throw SysError(formatSystemError("g_utf8_normalize(" + utfTo<std::string>(str) + ')', L"", L"Conversion failed."));
+ throw SysError(formatSystemError("g_utf8_normalize", L"", L"Conversion failed."));
ZEN_ON_SCOPE_EXIT(::g_free(outStr));
return outStr;
}
- catch ([[maybe_unused]] const SysError& e)
+ catch (const SysError& e)
{
- assert(false);
- return str;
+ throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Error normalizing string:" +
+ '\n' + utfTo<std::string>(str) + "\n\n" + utfTo<std::string>(e.toString()));
}
}
-Zstring getUpperCase(const Zstring& str)
+Zstring getUnicodeNormalForm(const Zstring& str)
{
- assert(str.find(Zchar('\0')) == Zstring::npos); //don't expect embedded nulls!
-
//fast pre-check:
if (isAsciiString(str)) //perf: in the range of 3.5ns
- {
- Zstring output = str;
- for (Zchar& c : output)
- c = asciiToUpper(c);
- return output;
- }
+ return str;
+ static_assert(std::is_same_v<decltype(str), const Zbase<Zchar>&>, "god bless our ref-counting! => save output string memory consumption!");
- Zstring strNorm = getUnicodeNormalForm(str);
+ return getUnicodeNormalFormNonAscii(str);
+}
+
+
+Zstring getUpperCaseNonAscii(const Zstring& str)
+{
+ Zstring strNorm = getUnicodeNormalFormNonAscii(str);
try
{
static_assert(sizeof(impl::CodePoint) == sizeof(gunichar));
@@ -64,11 +62,26 @@ Zstring getUpperCase(const Zstring& str)
return output;
}
- catch (SysError&)
+ catch (const SysError& e)
{
- assert(false);
- return str;
+ throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Error converting string to upper case:" +
+ '\n' + utfTo<std::string>(str) + "\n\n" + utfTo<std::string>(e.toString()));
+ }
+}
+
+
+Zstring getUpperCase(const Zstring& str)
+{
+ if (isAsciiString(str)) //fast path: in the range of 3.5ns
+ {
+ Zstring output = str;
+ for (Zchar& c : output) //identical to LCMapStringEx(), g_unichar_toupper(), CFStringUppercase() [verified!]
+ c = asciiToUpper(c); //
+ return output;
}
+ //else: slow path --------------------------------------
+
+ return getUpperCaseNonAscii(str);
}
@@ -91,10 +104,10 @@ std::weak_ordering compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char*
static_assert(sizeof(gunichar) == sizeof(impl::CodePoint));
+ //ordering: "to lower" converts to higher code points than "to upper"
const gunichar charL = ::g_unichar_toupper(*cpL); //note: tolower can be ambiguous, so don't use:
const gunichar charR = ::g_unichar_toupper(*cpR); //e.g. "Σ" (upper case) can be lower-case "ς" in the end of the word or "σ" in the middle.
if (charL != charR)
- //ordering: "to lower" converts to higher code points than "to upper"
return makeUnsigned(charL) <=> makeUnsigned(charR); //unsigned char-comparison is the convention!
}
}
@@ -107,78 +120,111 @@ std::weak_ordering compareNatural(const Zstring& lhs, const Zstring& rhs)
Windows: CompareString() already ignores NFD/NFC differences: nice...
Linux: g_unichar_toupper() can't ignore differences
macOS: CFStringCompare() considers differences */
-
- const Zstring& lhsNorm = getUnicodeNormalForm(lhs);
- const Zstring& rhsNorm = getUnicodeNormalForm(rhs);
-
- const char* strL = lhsNorm.c_str();
- const char* strR = rhsNorm.c_str();
-
- const char* const strEndL = strL + lhsNorm.size();
- const char* const strEndR = strR + rhsNorm.size();
- /* - compare strings after conceptually creating blocks of whitespace/numbers/text
- - implement strict weak ordering!
- - don't follow broken "strnatcasecmp": https://github.com/php/php-src/blob/master/ext/standard/strnatcmp.c
- 1. incorrect non-ASCII CI-comparison
- 2. incorrect bounds checks
- 3. incorrect trimming of *all* whitespace
- 4. arbitrary handling of leading 0 only at string begin
- 5. incorrect handling of whitespace following a number
- 6. code is a mess */
- for (;;)
+ try
{
- if (strL == strEndL || strR == strEndR)
- return (strL != strEndL) <=> (strR != strEndR); //"nothing" before "something"
- //note: "something" never would have been condensed to "nothing" further below => can finish evaluation here
-
- const bool wsL = isWhiteSpace(*strL);
- const bool wsR = isWhiteSpace(*strR);
- if (wsL != wsR)
- return !wsL <=> !wsR; //whitespace before non-ws!
- if (wsL)
- {
- ++strL, ++strR;
- while (strL != strEndL && isWhiteSpace(*strL)) ++strL;
- while (strR != strEndR && isWhiteSpace(*strR)) ++strR;
- continue;
- }
-
- const bool digitL = isDigit(*strL);
- const bool digitR = isDigit(*strR);
- if (digitL != digitR)
- return !digitL <=> !digitR; //numbers before chars!
- if (digitL)
+ const Zstring& lhsNorm = getUnicodeNormalForm(lhs);
+ const Zstring& rhsNorm = getUnicodeNormalForm(rhs);
+
+ const char* strL = lhsNorm.c_str();
+ const char* strR = rhsNorm.c_str();
+
+ const char* const strEndL = strL + lhsNorm.size();
+ const char* const strEndR = strR + rhsNorm.size();
+ /* - compare strings after conceptually creating blocks of whitespace/numbers/text
+ - implement strict weak ordering!
+ - don't follow broken "strnatcasecmp": https://github.com/php/php-src/blob/master/ext/standard/strnatcmp.c
+ 1. incorrect non-ASCII CI-comparison
+ 2. incorrect bounds checks
+ 3. incorrect trimming of *all* whitespace
+ 4. arbitrary handling of leading 0 only at string begin
+ 5. incorrect handling of whitespace following a number
+ 6. code is a mess */
+ for (;;)
{
- while (strL != strEndL && *strL == '0') ++strL;
- while (strR != strEndR && *strR == '0') ++strR;
+ if (strL == strEndL || strR == strEndR)
+ return (strL != strEndL) <=> (strR != strEndR); //"nothing" before "something"
+ //note: "something" never would have been condensed to "nothing" further below => can finish evaluation here
+
+ const bool wsL = isWhiteSpace(*strL);
+ const bool wsR = isWhiteSpace(*strR);
+ if (wsL != wsR)
+ return !wsL <=> !wsR; //whitespace before non-ws!
+ if (wsL)
+ {
+ ++strL, ++strR;
+ while (strL != strEndL && isWhiteSpace(*strL)) ++strL;
+ while (strR != strEndR && isWhiteSpace(*strR)) ++strR;
+ continue;
+ }
- int rv = 0;
- for (;; ++strL, ++strR)
+ const bool digitL = isDigit(*strL);
+ const bool digitR = isDigit(*strR);
+ if (digitL != digitR)
+ return !digitL <=> !digitR; //numbers before chars!
+ if (digitL)
{
- const bool endL = strL == strEndL || !isDigit(*strL);
- const bool endR = strR == strEndR || !isDigit(*strR);
- if (endL != endR)
- return !endL <=> !endR; //more digits means bigger number
- if (endL)
- break; //same number of digits
-
- if (rv == 0 && *strL != *strR)
- rv = *strL - *strR; //found first digit difference comparing from left
+ while (strL != strEndL && *strL == '0') ++strL;
+ while (strR != strEndR && *strR == '0') ++strR;
+
+ int rv = 0;
+ for (;; ++strL, ++strR)
+ {
+ const bool endL = strL == strEndL || !isDigit(*strL);
+ const bool endR = strR == strEndR || !isDigit(*strR);
+ if (endL != endR)
+ return !endL <=> !endR; //more digits means bigger number
+ if (endL)
+ break; //same number of digits
+
+ if (rv == 0 && *strL != *strR)
+ rv = *strL - *strR; //found first digit difference comparing from left
+ }
+ if (rv != 0)
+ return rv <=> 0;
+ continue;
}
- if (rv != 0)
- return rv <=> 0;
- continue;
+
+ //compare full junks of text: consider unicode encoding!
+ const char* textBeginL = strL++;
+ const char* textBeginR = strR++; //current char is neither white space nor digit at this point!
+ while (strL != strEndL && !isWhiteSpace(*strL) && !isDigit(*strL)) ++strL;
+ while (strR != strEndR && !isWhiteSpace(*strR) && !isDigit(*strR)) ++strR;
+
+ if (const std::weak_ordering cmp = compareNoCaseUtf8(textBeginL, strL - textBeginL, textBeginR, strR - textBeginR);
+ cmp != std::weak_ordering::equivalent)
+ return cmp;
}
- //compare full junks of text: consider unicode encoding!
- const char* textBeginL = strL++;
- const char* textBeginR = strR++; //current char is neither white space nor digit at this point!
- while (strL != strEndL && !isWhiteSpace(*strL) && !isDigit(*strL)) ++strL;
- while (strR != strEndR && !isWhiteSpace(*strR) && !isDigit(*strR)) ++strR;
+ }
+ catch (const SysError& e)
+ {
+ throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Error comparing strings:" + '\n' +
+ utfTo<std::string>(lhs) + '\n' + utfTo<std::string>(rhs) + "\n\n" + utfTo<std::string>(e.toString()));
+ }
+}
+
- if (const std::weak_ordering cmp = compareNoCaseUtf8(textBeginL, strL - textBeginL, textBeginR, strR - textBeginR);
- cmp != std::weak_ordering::equivalent)
- return cmp;
+std::weak_ordering compareNoCase(const Zstring& lhs, const Zstring& rhs)
+{
+ //fast path: no need for extra memory allocations => ~ 6x speedup
+ const size_t minSize = std::min(lhs.size(), rhs.size());
+
+ size_t i = 0;
+ for (; i < minSize; ++i)
+ {
+ const Zchar l = lhs[i];
+ const Zchar r = rhs[i];
+ if (!isAsciiChar(l) || !isAsciiChar(r))
+ goto slowPath; //=> let's NOT make assumptions how getUpperCase() compares "ASCII <=> non-ASCII"
+
+ const Zchar lUp = asciiToUpper(l); //
+ const Zchar rUp = asciiToUpper(r); //no surprises: emulate getUpperCase() [verified!]
+ if (lUp != rUp) //
+ return lUp <=> rUp; //
}
+ return lhs.size() <=> rhs.size();
+slowPath: //--------------------------------------
+ return compareNoCaseUtf8(lhs.c_str() + i, lhs.size() - i,
+ rhs.c_str() + i, rhs.size() - i);
}
diff --git a/zen/zstring.h b/zen/zstring.h
index bc7cfb06..70b9f448 100644
--- a/zen/zstring.h
+++ b/zen/zstring.h
@@ -39,7 +39,7 @@ Zstring getUnicodeNormalForm(const Zstring& str);
Zstring getUpperCase(const Zstring& str);
//------------------------------------------------------------------------------------------
-struct ZstringNorm //use as STL container key: avoid needless Unicode normalizations during std::map<>::find()
+struct ZstringNorm //use as STL container key: better than repeated Unicode normalizations during std::map<>::find()
{
/*explicit*/ ZstringNorm(const Zstring& str) : normStr(getUnicodeNormalForm(str)) {}
Zstring normStr;
@@ -51,7 +51,7 @@ template<> struct std::hash<ZstringNorm> { size_t operator()(const ZstringNorm&
//struct LessUnicodeNormal { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return getUnicodeNormalForm(lhs) < getUnicodeNormalForm(rhs); } };
//------------------------------------------------------------------------------------------
-struct ZstringNoCase //use as STL container key: avoid needless upper-case conversions during std::map<>::find()
+struct ZstringNoCase //use as STL container key: better than repeated upper-case conversions during std::map<>::find()
{
/*explicit*/ ZstringNoCase(const Zstring& str) : upperCase(getUpperCase(str)) {}
Zstring upperCase;
@@ -60,12 +60,18 @@ struct ZstringNoCase //use as STL container key: avoid needless upper-case conve
};
template<> struct std::hash<ZstringNoCase> { size_t operator()(const ZstringNoCase& str) const { return std::hash<Zstring>()(str.upperCase); } };
-inline bool equalNoCase(const Zstring& lhs, const Zstring& rhs) { return getUpperCase(lhs) == getUpperCase(rhs); }
+
+std::weak_ordering compareNoCase(const Zstring& lhs, const Zstring& rhs);
+
+inline
+bool equalNoCase(const Zstring& lhs, const Zstring& rhs) { return compareNoCase(lhs, rhs) == std::weak_ordering::equivalent; }
+//note: the "lhs.size() != rhs.size()" short-cut would require two isAsciiString() checks
+//=> generally SLOWER than starting comparison directly during first pass and breaking on first difference!
//------------------------------------------------------------------------------------------
std::weak_ordering compareNatural(const Zstring& lhs, const Zstring& rhs);
-struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return std::is_lt(compareNatural(lhs, rhs)); } };
+struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNatural(lhs, rhs) < 0; } };
//------------------------------------------------------------------------------------------
@@ -73,16 +79,18 @@ struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs)
const wchar_t EN_DASH = L'\u2013';
const wchar_t EM_DASH = L'\u2014';
const wchar_t* const SPACED_DASH = L" \u2014 "; //using 'EM DASH'
-const wchar_t LTR_MARK = L'\u200E'; //UTF-8: E2 80 8E
const wchar_t* const ELLIPSIS = L"\u2026"; //"..."
const wchar_t MULT_SIGN = L'\u00D7'; //fancy "x"
//const wchar_t NOBREAK_SPACE = L'\u00A0';
const wchar_t ZERO_WIDTH_SPACE = L'\u200B';
+const wchar_t LTR_MARK = L'\u200E'; //UTF-8: E2 80 8E
const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F https://www.w3.org/International/questions/qa-bidi-unicode-controls
-const wchar_t BIDI_DIR_ISOLATE_RTL = L'\u2067'; //UTF-8: E2 81 A7 => not working on Win 10
-const wchar_t BIDI_POP_DIR_ISOLATE = L'\u2069'; //UTF-8: E2 81 A9 => not working on Win 10
-const wchar_t BIDI_DIR_EMBEDDING_RTL = L'\u202B'; //UTF-8: E2 80 AB => not working on Win 10
-const wchar_t BIDI_POP_DIR_FORMATTING = L'\u202C'; //UTF-8: E2 80 AC => not working on Win 10
+//const wchar_t BIDI_DIR_ISOLATE_RTL = L'\u2067'; //=> not working on Win 10
+//const wchar_t BIDI_POP_DIR_ISOLATE = L'\u2069'; //=> not working on Win 10
+//const wchar_t BIDI_DIR_EMBEDDING_RTL = L'\u202B'; //=> not working on Win 10
+//const wchar_t BIDI_POP_DIR_FORMATTING = L'\u202C'; //=> not working on Win 10
+
+const wchar_t* const TAB_SPACE = L" "; //4: the only sensible space count for tabs
#endif //ZSTRING_H_73425873425789
bgstack15