diff options
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 Binary files differindex c7a5e698..7de873b1 100644 --- a/FreeFileSync/Build/Resources/Icons.zip +++ b/FreeFileSync/Build/Resources/Icons.zip diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex 8f6c9f21..ec222bbc 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip 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 = '.'; } @@ -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) @@ -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" @@ -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; @@ -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}; } @@ -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 |