diff options
24 files changed, 404 insertions, 271 deletions
@@ -5,7 +5,7 @@ the ones mentioned below. The remaining issues that are yet to be fixed are list ---------------- -| libcurl 8.4.0| +| libcurl 8.7.0| ---------------- __________________________________________________________________________________________________________ /lib/ftp.c diff --git a/Changelog.txt b/Changelog.txt index a03ca67d..0d8da3a9 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,12 @@ +FreeFileSync 13.5 [2024-04-01] +------------------------------ +Wrap file grid folder paths instead of truncate +Fixed sync operation arrows for RTL layout +Fixed FTP hang during connection (libcurl regression) +Consider user-defined file time tolerance for DB comparisons +Don't log folder pair paths if nothing to sync + + FreeFileSync 13.4 [2024-02-16] ------------------------------ Ignore leading/trailing space when matching file names diff --git a/FreeFileSync/Build/Resources/cacert.pem b/FreeFileSync/Build/Resources/cacert.pem index d8fda7d1..f78a6101 100644 --- a/FreeFileSync/Build/Resources/cacert.pem +++ b/FreeFileSync/Build/Resources/cacert.pem @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Dec 12 04:12:04 2023 GMT +## Certificate data from Mozilla as of: Mon Mar 11 15:25:27 2024 GMT ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates @@ -14,7 +14,7 @@ ## Just configure this file as the SSLCACertificateFile. ## ## Conversion done with mk-ca-bundle.pl version 1.29. -## SHA256: 1970dd65858925d68498d2356aea6d03f764422523c5887deca8ce3ba9e1f845 +## SHA256: 4d96bd539f4719e9ace493757afbe4a23ee8579de1c97fbebc50bba3c12e8c1e ## @@ -3532,3 +3532,50 @@ dVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670v64fG9PiO/yzcnMcmyiQ iRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17Org3bhzjlP1v9mxnhMUF6cKojawHhRUzN lM47ni3niAIi9G7oyOzWPPO5std3eqx7 -----END CERTIFICATE----- + +Telekom Security TLS ECC Root 2020 +================================== +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJE +RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJUZWxl +a29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIwMB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIz +NTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkg +R21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqG +SM49AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/OtdKPD/M1 +2kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDPf8iAC8GXs7s1J8nCG6NC +MEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6fMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZ +Mo7k+5Dck2TOrbRBR2Diz6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdU +ga/sf+Rn27iQ7t0l +-----END CERTIFICATE----- + +Telekom Security TLS RSA Root 2023 +================================== +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBjMQswCQYDVQQG +EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJU +ZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAyMDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMy +NzIzNTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJp +dHkgR21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9cUD/h3VC +KSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHVcp6R+SPWcHu79ZvB7JPP +GeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMAU6DksquDOFczJZSfvkgdmOGjup5czQRx +UX11eKvzWarE4GC+j4NSuHUaQTXtvPM6Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWo +l8hHD/BeEIvnHRz+sTugBTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9 +FIS3R/qy8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73Jco4v +zLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg8qKrBC7m8kwOFjQg +rIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8rFEz0ciD0cmfHdRHNCk+y7AO+oML +KFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7S +WWO/gLCMk3PLNaaZlSJhZQNg+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUtqeXgj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQpGv7qHBFfLp+ +sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm9S3ul0A8Yute1hTWjOKWi0Fp +kzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErwM807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy +/SKE8YXJN3nptT+/XOR0so8RYgDdGGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4 +mZqTuXNnQkYRIer+CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtz +aL1txKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+w6jv/naa +oqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aKL4x35bcF7DvB7L6Gs4a8 +wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+ljX273CXE2whJdV/LItM3z7gLfEdxquVeE +HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 +o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index 39daab05..02ed1511 100644 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -204,7 +204,7 @@ int Application::OnExit() } -wxLayoutDirection Application::GetLayoutDirection() const { return fff::getLayoutDirection(); } +wxLayoutDirection Application::GetLayoutDirection() const { return languageLayoutIsRtl() ? wxLayout_RightToLeft : wxLayout_LeftToRight; } int Application::OnRun() diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index d479fb22..597836b2 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -238,7 +238,7 @@ int Application::OnExit() } -wxLayoutDirection Application::GetLayoutDirection() const { return getLayoutDirection(); } +wxLayoutDirection Application::GetLayoutDirection() const { return languageLayoutIsRtl() ? wxLayout_RightToLeft : wxLayout_LeftToRight; } int Application::OnRun() diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index 5526f0f9..d78e8767 100644 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -218,7 +218,8 @@ bool fff::allElementsEqual(const FolderComparison& folderCmp) namespace { template <SelectSide side> inline -CudAction compareDbEntry(const FilePair& file, const InSyncFile* dbFile, const std::vector<unsigned int>& ignoreTimeShiftMinutes, bool renamedOrMoved) +CudAction compareDbEntry(const FilePair& file, const InSyncFile* dbFile, unsigned int fileTimeTolerance, + const std::vector<unsigned int>& ignoreTimeShiftMinutes, bool renamedOrMoved) { if (file.isEmpty<side>()) return dbFile ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; @@ -227,8 +228,7 @@ CudAction compareDbEntry(const FilePair& file, const InSyncFile* dbFile, const s const InSyncDescrFile& descrDb = selectParam<side>(dbFile->left, dbFile->right); - return sameFileTime(file.getLastWriteTime<side>(), descrDb.modTime, FAT_FILE_TIME_PRECISION_SEC, ignoreTimeShiftMinutes) && - //- we're not interested in "fileTimeTolerance"! + return sameFileTime(file.getLastWriteTime<side>(), descrDb.modTime, fileTimeTolerance, ignoreTimeShiftMinutes) && //- we do *not* consider file ID, but only *user-visual* changes. E.g. user moving data to some other medium should not be considered a change! file.getFileSize<side>() == dbFile->fileSize ? CudAction::noChange : CudAction::update; @@ -237,7 +237,7 @@ CudAction compareDbEntry(const FilePair& file, const InSyncFile* dbFile, const s //check whether database entry is in sync considering *current* comparison settings inline -bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, int fileTimeTolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) +bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, unsigned int fileTimeTolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) { switch (compareVar) { @@ -263,7 +263,8 @@ bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, int fileTi //check whether database entry and current item match: *irrespective* of current comparison settings template <SelectSide side> inline -CudAction compareDbEntry(const SymlinkPair& symlink, const InSyncSymlink* dbSymlink, const std::vector<unsigned int>& ignoreTimeShiftMinutes, bool renamedOrMoved) +CudAction compareDbEntry(const SymlinkPair& symlink, const InSyncSymlink* dbSymlink, unsigned int fileTimeTolerance, + const std::vector<unsigned int>& ignoreTimeShiftMinutes, bool renamedOrMoved) { if (symlink.isEmpty<side>()) return dbSymlink ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; @@ -272,14 +273,14 @@ CudAction compareDbEntry(const SymlinkPair& symlink, const InSyncSymlink* dbSyml const InSyncDescrLink& descrDb = selectParam<side>(dbSymlink->left, dbSymlink->right); - return sameFileTime(symlink.getLastWriteTime<side>(), descrDb.modTime, FAT_FILE_TIME_PRECISION_SEC, ignoreTimeShiftMinutes) ? + return sameFileTime(symlink.getLastWriteTime<side>(), descrDb.modTime, fileTimeTolerance, ignoreTimeShiftMinutes) ? CudAction::noChange : CudAction::update; } //check whether database entry is in sync considering *current* comparison settings inline -bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, int fileTimeTolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) +bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, unsigned int fileTimeTolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) { switch (compareVar) { @@ -534,7 +535,7 @@ private: } const CompareVariant cmpVar_; - const int fileTimeTolerance_; + const unsigned int fileTimeTolerance_; const std::vector<unsigned int> ignoreTimeShiftMinutes_; std::vector<FilePair*> filesL_; //collection of *all* file items (with non-null filePrint) @@ -649,8 +650,8 @@ private: } return false; }(); - const CudAction changeL = compareDbEntry<SelectSide::left >(file, dbEntryL, ignoreTimeShiftMinutes_, renamedOrMoved); - const CudAction changeR = compareDbEntry<SelectSide::right>(file, dbEntryR, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeL = compareDbEntry<SelectSide::left >(file, dbEntryL, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeR = compareDbEntry<SelectSide::right>(file, dbEntryR, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); setSyncDirForChange(file, changeL, changeR); } @@ -686,8 +687,8 @@ private: return symlink.setSyncDirConflict(txtDbNotInSync_); const bool renamedOrMoved = cat == SYMLINK_RENAMED; - const CudAction changeL = compareDbEntry<SelectSide::left >(symlink, dbEntryL, ignoreTimeShiftMinutes_, renamedOrMoved); - const CudAction changeR = compareDbEntry<SelectSide::right>(symlink, dbEntryR, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeL = compareDbEntry<SelectSide::left >(symlink, dbEntryL, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeR = compareDbEntry<SelectSide::right>(symlink, dbEntryR, fileTimeTolerance_, ignoreTimeShiftMinutes_, renamedOrMoved); setSyncDirForChange(symlink, changeL, changeR); } @@ -800,7 +801,7 @@ private: const DirectionByChange dirs_; const CompareVariant cmpVar_; - const int fileTimeTolerance_; + const unsigned int fileTimeTolerance_; const std::vector<unsigned int> ignoreTimeShiftMinutes_; }; } @@ -1206,7 +1207,7 @@ std::optional<PathDependency> fff::getPathDependency(const AbstractPath& itemPat relDirPath = appendPath(relDirPath, itemName); }); - return PathDependency{leftParent ? itemPathL : itemPathR, relDirPath}; + return PathDependency{leftParent ? itemPathL : itemPathR, relDirPath}; } } } @@ -1215,18 +1216,18 @@ std::optional<PathDependency> fff::getPathDependency(const AbstractPath& itemPat std::optional<PathDependency> fff::getFolderPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL, - const AbstractPath& folderPathR, const PathFilter& filterR) + const AbstractPath& folderPathR, const PathFilter& filterR) { if (std::optional<PathDependency> pd = getPathDependency(folderPathL, folderPathR)) - { - const PathFilter& filterP = pd->itemPathParent == folderPathL ? filterL : filterR; - //if there's a dependency, check if the sub directory is (fully) excluded via filter - //=> easy to check but still insufficient in general: - // - one folder may have a *.txt include-filter, the other a *.lng include filter => no dependencies, but "childItemMightMatch = true" below! - // - user may have manually excluded the conflicting items or changed the filter settings without running a re-compare - bool childItemMightMatch = true; - if (pd->relPath.empty() || filterP.passDirFilter(pd->relPath, &childItemMightMatch) || childItemMightMatch) - return pd; + { + const PathFilter& filterP = pd->itemPathParent == folderPathL ? filterL : filterR; + //if there's a dependency, check if the sub directory is (fully) excluded via filter + //=> easy to check but still insufficient in general: + // - one folder may have a *.txt include-filter, the other a *.lng include filter => no dependencies, but "childItemMightMatch = true" below! + // - user may have manually excluded the conflicting items or changed the filter settings without running a re-compare + bool childItemMightMatch = true; + if (pd->relPath.empty() || filterP.passDirFilter(pd->relPath, &childItemMightMatch) || childItemMightMatch) + return pd; } return {}; } diff --git a/FreeFileSync/Source/base/algorithm.h b/FreeFileSync/Source/base/algorithm.h index 71f20b57..ae4d28d3 100644 --- a/FreeFileSync/Source/base/algorithm.h +++ b/FreeFileSync/Source/base/algorithm.h @@ -46,7 +46,7 @@ struct PathDependency }; std::optional<PathDependency> getPathDependency(const AbstractPath& itemPathL, const AbstractPath& itemPathR); std::optional<PathDependency> getFolderPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL, - const AbstractPath& folderPathR, const PathFilter& filterR); + const AbstractPath& folderPathR, const PathFilter& filterR); //manual copy to alternate folder: void copyToAlternateFolder(const std::vector<const FileSystemObject*>& selectionL, //all pointers need to be bound and !isEmpty<side>! diff --git a/FreeFileSync/Source/base/cmp_filetime.h b/FreeFileSync/Source/base/cmp_filetime.h index c148c6b0..bd7fb4fb 100644 --- a/FreeFileSync/Source/base/cmp_filetime.h +++ b/FreeFileSync/Source/base/cmp_filetime.h @@ -14,10 +14,9 @@ namespace fff { inline -bool sameFileTime(time_t lhs, time_t rhs, int tolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) +bool sameFileTime(time_t lhs, time_t rhs, /*unsigned*/ int tolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) { - if (tolerance < 0) //:= unlimited tolerance by convention! - return true; + assert(tolerance >= 0); if (lhs < rhs) std::swap(lhs, rhs); @@ -71,7 +70,7 @@ inline const time_t oneYearFromNow = std::time(nullptr) + 365 * 24 * 3600; inline -TimeResult compareFileTime(time_t lhs, time_t rhs, int tolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) +TimeResult compareFileTime(time_t lhs, time_t rhs, unsigned int tolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) { assert(oneYearFromNow != 0); if (sameFileTime(lhs, rhs, tolerance, ignoreTimeShiftMinutes)) //last write time may differ by up to 2 seconds (NTFS vs FAT32) diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 3d448f39..a5347c9b 100644 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -187,7 +187,7 @@ class ComparisonBuffer { public: ComparisonBuffer(const FolderStatus& folderStatus, - int fileTimeTolerance, + unsigned int fileTimeTolerance, ProcessCallback& callback) : fileTimeTolerance_(fileTimeTolerance), folderStatus_(folderStatus), @@ -221,7 +221,7 @@ private: return BaseFolderStatus::notExisting; }; - const int fileTimeTolerance_; + const unsigned int fileTimeTolerance_; const FolderStatus& folderStatus_; std::map<DirectoryKey, DirectoryValue> folderBuffer_; //contains entries for *all* scanned folders! ProcessCallback& cb_; @@ -1055,7 +1055,7 @@ SharedRef<BaseFolderPair> ComparisonBuffer::performComparison(const ResolvedFold FolderComparison fff::compare(WarningDialogs& warnings, - int fileTimeTolerance, + unsigned int fileTimeTolerance, const AFS::RequestPasswordFun& requestPassword /*throw X*/, bool runWithBackgroundPriority, bool createDirLocks, diff --git a/FreeFileSync/Source/base/comparison.h b/FreeFileSync/Source/base/comparison.h index cca22eb5..903a8274 100644 --- a/FreeFileSync/Source/base/comparison.h +++ b/FreeFileSync/Source/base/comparison.h @@ -48,7 +48,7 @@ std::vector<FolderPairCfg> extractCompareCfg(const MainConfiguration& mainCfg); //FFS core routine: output.size() == fpCfgList.size() or 0 on fatal error FolderComparison compare(WarningDialogs& warnings, - int fileTimeTolerance, + unsigned int fileTimeTolerance, const AFS::RequestPasswordFun& requestPassword /*throw X*/, bool runWithBackgroundPriority, bool createDirLocks, diff --git a/FreeFileSync/Source/base/file_hierarchy.cpp b/FreeFileSync/Source/base/file_hierarchy.cpp index 8945d705..d293042f 100644 --- a/FreeFileSync/Source/base/file_hierarchy.cpp +++ b/FreeFileSync/Source/base/file_hierarchy.cpp @@ -514,6 +514,11 @@ std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) { const SyncOperation op = fsObj.getSyncOperation(); + const std::wstring rightArrowDown = languageLayoutIsRtl() ? + std::wstring() + RTL_MARK + LEFT_ARROW_ANTICLOCK : + std::wstring() + LTR_MARK + RIGHT_ARROW_CURV_DOWN; + //Windows bug: RIGHT_ARROW_CURV_DOWN rendering and extent calculation is buggy (see wx+\tooltip.cpp) => need LTR mark! + auto generateFooter = [&] { if (fsObj.hasEquivalentItemNames()) @@ -528,7 +533,7 @@ std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) if (dir == SyncDirection::left) std::swap(itemNameNew, itemNameOld); - return L'\n' + fmtPath(itemNameOld) + L' ' + RIGHT_ARROW_CURV_DOWN + L'\n' + fmtPath(itemNameNew); + return L'\n' + fmtPath(itemNameOld) + L' ' + rightArrowDown + L'\n' + fmtPath(itemNameNew); } else return L'\n' + @@ -574,10 +579,10 @@ std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) (beforeLast(relPathFrom, FILE_NAME_SEPARATOR, IfNotFoundReturn::none) == beforeLast(relPathTo, FILE_NAME_SEPARATOR, IfNotFoundReturn::none) ? //detected pure "rename" - fmtPath(getItemName(relPathFrom)) + L' ' + RIGHT_ARROW_CURV_DOWN + L'\n' + //show file name only + fmtPath(getItemName(relPathFrom)) + L' ' + rightArrowDown + L'\n' + //show file name only fmtPath(getItemName(relPathTo)) : //"move" or "move + rename" - fmtPath(relPathFrom) + L' ' + RIGHT_ARROW_CURV_DOWN + L'\n' + + fmtPath(relPathFrom) + L' ' + rightArrowDown + L'\n' + fmtPath(relPathTo)); } break; diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h index e5abe11b..78d5b4c0 100644 --- a/FreeFileSync/Source/base/file_hierarchy.h +++ b/FreeFileSync/Source/base/file_hierarchy.h @@ -293,7 +293,7 @@ public: BaseFolderStatus folderStatusRight, const FilterRef& filter, CompareVariant cmpVar, - int fileTimeTolerance, + unsigned int fileTimeTolerance, const std::vector<unsigned int>& ignoreTimeShiftMinutes) : ContainerObject(*this), //trust that ContainerObject knows that *this is not yet fully constructed! filter_(filter), cmpVar_(cmpVar), fileTimeTolerance_(fileTimeTolerance), ignoreTimeShiftMinutes_(ignoreTimeShiftMinutes), @@ -308,7 +308,7 @@ public: //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_; } + unsigned int getFileTimeTolerance() const { return fileTimeTolerance_; } const std::vector<unsigned int>& getIgnoredTimeShift() const { return ignoreTimeShiftMinutes_; } void flip() override; @@ -319,7 +319,7 @@ private: const FilterRef filter_; //filter used while scanning directory: represents sub-view of actual files! const CompareVariant cmpVar_; - const int fileTimeTolerance_; + const unsigned int fileTimeTolerance_; const std::vector<unsigned int> ignoreTimeShiftMinutes_; BaseFolderStatus folderStatusLeft_; diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 8bcc37c2..48a08a44 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -1961,7 +1961,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) else assert(false); -#if 0 //changing file time without copying content is not justified after CompareVariant::size finds "equal" files! similar issue with CompareVariant::timeSize and FileTimeTolerance == -1 +#if 0 //changing file time without copying content is not justified after CompareVariant::size finds "equal" files! //Bonus: some devices don't support setting (precise) file times anyway, e.g. FAT or MTP! if (file.getLastWriteTime<sideTrg>() != file.getLastWriteTime<sideSrc>()) //- no need to call sameFileTime() or respect 2 second FAT/FAT32 precision in this comparison @@ -2798,10 +2798,10 @@ break2: if (pd->itemPathParent == folderPath) //if versioning folder is a subfolder of a base folder if (!pd->relPath.empty()) //this can be fixed via an exclude filter { - assert(pd->itemPathParent == folderPath); //otherwise: what the fuck!? + assert(pd->itemPathParent == folderPath); //otherwise: what the fuck!? shouldExclude = true; - msg += std::wstring() + L'\n' + - L"⇒ " + _("Exclude:") + L" \t" + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); + msg += std::wstring() + L'\n' + + L"⇒ " + _("Exclude:") + L" \t" + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); } } } @@ -2872,11 +2872,6 @@ break2: continue; //------------------------------------------------------------------------------------------ - callback.logMessage(_("Synchronizing folder pair:") + L' ' + getVariantNameWithSymbol(folderPairCfg.syncVar) + L'\n' + //throw X - TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::left >()) + L'\n' + - TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::right>()), PhaseCallback::MsgType::info); - //------------------------------------------------------------------------------------------ - //checking a second time: 1. a long time may have passed since syncing the previous folder pairs! // 2. expected to be run directly *before* createBaseFolder()! if (!checkBaseFolderStatus<SelectSide::left >(baseFolder, callback) || @@ -2890,9 +2885,7 @@ break2: continue; //------------------------------------------------------------------------------------------ - //execute synchronization recursively - - //update database even when sync is cancelled: + //update database even when sync is cancelled (or "nothing to sync"): auto guardDbSave = makeGuard<ScopeGuardRunMode::onFail>([&] { if (folderPairCfg.saveSyncDB) @@ -2900,64 +2893,73 @@ break2: callbackNoThrow); }); - //guarantee removal of invalid entries (where element is empty on both sides) - ZEN_ON_SCOPE_EXIT(baseFolder.removeDoubleEmpty()); - - bool copyPermissionsFp = false; - tryReportingError([&] + //------------------------------------------------------------------------------------------ + //execute synchronization recursively + if (getCUD(folderPairStat) > 0) { - copyPermissionsFp = copyFilePermissions && //copy permissions only if asked for and supported by *both* sides! - AFS::supportPermissionCopy(baseFolder.getAbstractPath<SelectSide::left>(), - baseFolder.getAbstractPath<SelectSide::right>()); //throw FileError - }, callback); //throw X + callback.logMessage(_("Synchronizing folder pair:") + L' ' + getVariantNameWithSymbol(folderPairCfg.syncVar) + L'\n' + //throw X + TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::left >()) + L'\n' + + TAB_SPACE + AFS::getDisplayPath(baseFolder.getAbstractPath<SelectSide::right>()), PhaseCallback::MsgType::info); - const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase); - - DeletionHandler delHandlerL(baseFolder.getAbstractPath<SelectSide::left>(), - recyclerMissingReportOnce, - warnings.warnRecyclerMissing, - folderPairCfg.handleDeletion, - versioningFolderPath, - folderPairCfg.versioningStyle, - std::chrono::system_clock::to_time_t(syncStartTime)); - - DeletionHandler delHandlerR(baseFolder.getAbstractPath<SelectSide::right>(), - recyclerMissingReportOnce, - warnings.warnRecyclerMissing, - folderPairCfg.handleDeletion, - versioningFolderPath, - folderPairCfg.versioningStyle, - std::chrono::system_clock::to_time_t(syncStartTime)); - - //always (try to) clean up, even if synchronization is aborted! - auto guardDelCleanup = makeGuard<ScopeGuardRunMode::onFail>([&] - { - delHandlerL.tryCleanup(callbackNoThrow); - delHandlerR.tryCleanup(callbackNoThrow); - }); + //guarantee removal of invalid entries (where element is empty on both sides) + ZEN_ON_SCOPE_EXIT(baseFolder.removeDoubleEmpty()); + bool copyPermissionsFp = false; + tryReportingError([&] + { + copyPermissionsFp = copyFilePermissions && //copy permissions only if asked for and supported by *both* sides! + AFS::supportPermissionCopy(baseFolder.getAbstractPath<SelectSide::left>(), + baseFolder.getAbstractPath<SelectSide::right>()); //throw FileError + }, callback); //throw X + + const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase); + + DeletionHandler delHandlerL(baseFolder.getAbstractPath<SelectSide::left>(), + recyclerMissingReportOnce, + warnings.warnRecyclerMissing, + folderPairCfg.handleDeletion, + versioningFolderPath, + folderPairCfg.versioningStyle, + std::chrono::system_clock::to_time_t(syncStartTime)); + + DeletionHandler delHandlerR(baseFolder.getAbstractPath<SelectSide::right>(), + recyclerMissingReportOnce, + warnings.warnRecyclerMissing, + folderPairCfg.handleDeletion, + versioningFolderPath, + folderPairCfg.versioningStyle, + std::chrono::system_clock::to_time_t(syncStartTime)); + + //always (try to) clean up, even if synchronization is aborted! + auto guardDelCleanup = makeGuard<ScopeGuardRunMode::onFail>([&] + { + delHandlerL.tryCleanup(callbackNoThrow); + delHandlerR.tryCleanup(callbackNoThrow); + }); - FolderPairSyncer::SyncCtx syncCtx = - { - verifyCopiedFiles, copyPermissionsFp, failSafeFileCopy, - delHandlerL, delHandlerR, - }; - FolderPairSyncer::runSync(syncCtx, baseFolder, callback); - //(try to gracefully) clean up temporary Recycle Bin folders and versioning - delHandlerL.tryCleanup(callback); //throw X - delHandlerR.tryCleanup(callback); // - guardDelCleanup.dismiss(); + FolderPairSyncer::SyncCtx syncCtx = + { + verifyCopiedFiles, copyPermissionsFp, failSafeFileCopy, + delHandlerL, delHandlerR, + }; + FolderPairSyncer::runSync(syncCtx, baseFolder, callback); - if (folderPairCfg.handleDeletion == DeletionVariant::versioning && - folderPairCfg.versioningStyle != VersioningStyle::replace) - versionLimitFolders.insert( - { - versioningFolderPath, - folderPairCfg.versionMaxAgeDays, - folderPairCfg.versionCountMin, - folderPairCfg.versionCountMax - }); + //(try to gracefully) clean up temporary Recycle Bin folders and versioning + delHandlerL.tryCleanup(callback); //throw X + delHandlerR.tryCleanup(callback); // + guardDelCleanup.dismiss(); + + if (folderPairCfg.handleDeletion == DeletionVariant::versioning && + folderPairCfg.versioningStyle != VersioningStyle::replace) + versionLimitFolders.insert( + { + versioningFolderPath, + folderPairCfg.versionMaxAgeDays, + folderPairCfg.versionCountMin, + folderPairCfg.versionCountMax + }); + } //(try to gracefully) write database file if (folderPairCfg.saveSyncDB) diff --git a/FreeFileSync/Source/config.h b/FreeFileSync/Source/config.h index 6193effd..b62b5c35 100644 --- a/FreeFileSync/Source/config.h +++ b/FreeFileSync/Source/config.h @@ -150,7 +150,7 @@ struct XmlGlobalSettings bool copyLockedFiles = false; //safer default: avoid copies of partially written files bool copyFilePermissions = false; - int fileTimeTolerance = zen::FAT_FILE_TIME_PRECISION_SEC; //max. allowed file time deviation; < 0 means unlimited tolerance; default 2s: FAT vs NTFS + unsigned int fileTimeTolerance = zen::FAT_FILE_TIME_PRECISION_SEC; //default 2s: FAT vs NTFS bool runWithBackgroundPriority = false; bool createLockFile = true; bool verifyFileCopy = false; diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index 0ca0d841..22eab503 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -23,7 +23,7 @@ namespace class FFSTranslation : public TranslationHandler { public: - explicit FFSTranslation(const std::string& lngStream); //throw lng::ParsingError, plural::ParsingError + FFSTranslation(const std::string& lngStream, bool haveRtlLayout); //throw lng::ParsingError, plural::ParsingError std::wstring translate(const std::wstring& text) const override { @@ -47,6 +47,8 @@ public: return replaceCpy(std::abs(n) == 1 ? singular : plural, L"%x", formatNumber(n)); //fallback } + bool layoutIsRtl() const override { return haveRtlLayout_; } + private: using Translation = std::unordered_map<std::wstring, std::wstring>; //hash_map is 15% faster than std::map on GCC using TranslationPlural = std::map<std::pair<std::wstring, std::wstring>, std::vector<std::wstring>>; @@ -54,10 +56,12 @@ private: Translation transMapping_; //map original text |-> translation TranslationPlural transMappingPl_; std::unique_ptr<plural::PluralForm> pluralParser_; //bound! + const bool haveRtlLayout_; }; -FFSTranslation::FFSTranslation(const std::string& lngStream) //throw lng::ParsingError, plural::ParsingError +FFSTranslation::FFSTranslation(const std::string& lngStream, bool haveRtlLayout) ://throw lng::ParsingError, plural::ParsingError + haveRtlLayout_(haveRtlLayout) { lng::TransHeader header; lng::TranslationMap transUtf; @@ -300,7 +304,6 @@ private: std::vector<TranslationInfo> globalTranslations; wxLanguage globalLang = wxLANGUAGE_UNKNOWN; -wxLayoutDirection globalLayoutDir = wxLayout_Default; } @@ -346,7 +349,6 @@ void fff::localizationCleanup() { #if 0 //good place for clean up rather than some time during static destruction: is this an actual benefit??? globalLang = wxLANGUAGE_UNKNOWN; - globalLayoutDir = wxLayout_Default; setTranslator(nullptr); @@ -382,7 +384,11 @@ void fff::setLanguage(wxLanguage lng) //throw FileError else try { - setTranslator(std::make_unique<FFSTranslation>(lngStream)); //throw lng::ParsingError, plural::ParsingError + bool haveRtlLayout = false; + if (const wxLanguageInfo* selLngInfo = wxUILocale::GetLanguageInfo(lng)) + haveRtlLayout = selLngInfo->LayoutDirection == wxLayout_RightToLeft; + + setTranslator(std::make_unique<FFSTranslation>(lngStream, haveRtlLayout)); //throw lng::ParsingError, plural::ParsingError } catch (const lng::ParsingError& e) { @@ -400,12 +406,6 @@ void fff::setLanguage(wxLanguage lng) //throw FileError globalLang = lng; - if (const wxLanguageInfo* selLngInfo = wxUILocale::GetLanguageInfo(lng)) - globalLayoutDir = selLngInfo->LayoutDirection; - else - globalLayoutDir = wxLayout_LeftToRight; - - //add translation for wxWidgets-internal strings: std::map<std::string, std::wstring> transMapping = { @@ -435,5 +435,3 @@ wxLanguage fff::getDefaultLanguage() wxLanguage fff::getLanguage() { return globalLang; } - -wxLayoutDirection fff::getLayoutDirection() { return globalLayoutDir; } diff --git a/FreeFileSync/Source/localization.h b/FreeFileSync/Source/localization.h index f52db495..5eaf5ec3 100644 --- a/FreeFileSync/Source/localization.h +++ b/FreeFileSync/Source/localization.h @@ -29,7 +29,6 @@ const std::vector<TranslationInfo>& getAvailableTranslations(); wxLanguage getDefaultLanguage(); wxLanguage getLanguage(); -wxLayoutDirection getLayoutDirection(); void setLanguage(wxLanguage lng); //throw FileError diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index 9c8293fe..c53a174d 100644 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -308,7 +308,8 @@ struct SharedComponents //...between left, center, and right grids NavigationMarker navMarker; std::unique_ptr<GridEventManager> evtMgr; GridViewType gridViewType = GridViewType::action; - std::unordered_map<std::wstring, wxSize> compExtentsBuf_; //buffer expensive wxDC::GetTextExtent() calls! + std::unordered_map<std::wstring, wxSize, StringHash, StringEqual> compExtentsBuf_; //buffer expensive wxDC::GetTextExtent() calls! + //StringHash, StringEqual => heterogenous lookup by std::wstring_view }; //######################################################################################################## @@ -351,7 +352,7 @@ public: const FileSystemObject* getFsObject(size_t row) const { return getDataView().getFsObject(row); } - const wxSize& getTextExtentBuffered(wxDC& dc, const std::wstring& text) + const wxSize& getTextExtentBuffered(wxDC& dc, const std::wstring_view& text) { auto& compExtentsBuf = sharedComp_.ref().compExtentsBuf_; //- only used for parent path names and file names on view => should not grow "too big" @@ -359,10 +360,47 @@ public: auto it = compExtentsBuf.find(text); if (it == compExtentsBuf.end()) - it = compExtentsBuf.emplace(text, dc.GetTextExtent(text)).first; + it = compExtentsBuf.emplace(text, dc.GetTextExtent(copyStringTo<wxString>(text))).first; return it->second; } + //- trim while leaving path components intact + //- *always* returns at least one component, even if > maxWidth + size_t getPathTrimmedSize(wxDC& dc, const std::wstring_view& itemPath, int maxWidth) + { + if (itemPath.size() <= 1) + return itemPath.size(); + + std::vector<std::wstring_view> subComp; + + //split path by components, but skip slash at beginning or end + for (auto it = itemPath.begin() + 1; it != itemPath.end() - 1; ++it) + if (*it == L'/' || + *it == L'\\') + subComp.push_back(makeStringView(itemPath.begin(), it)); + + subComp.push_back(itemPath); + + if (maxWidth <= 0) + return subComp[0].size(); + + size_t low = 0; + size_t high = subComp.size(); + + for (;;) + { + if (high - low == 1) + return subComp[low].size(); + + const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1" + + if (getTextExtentBuffered(dc, subComp[middle]).GetWidth() <= maxWidth) + low = middle; + else + high = middle; + } + } + private: size_t getRowCount() const override { return getDataView().rowsOnView(); } @@ -550,7 +588,9 @@ private: //---------------------------------------------------------------------------------- const wxRect rectLine(rect.x, rect.y + rect.height - dipToWxsize(1), rect.width, dipToWxsize(1)); - clearArea(dc, rectLine, row == pdi.groupLastRow - 1 /*last group item*/ ? + clearArea(dc, rectLine, row == pdi.groupLastRow - 1 || //last group item + (pdi.fsObj == pdi.folderGroupObj && //folder item => distinctive separation color against subsequent file items + itemPathFormat_ != ItemPathFormat::name) ? getColorGridLine() : getDefaultBackgroundColorAlternating(pdi.groupIdx % 2 != 0)); } @@ -572,18 +612,18 @@ private: if (itemNamesWidth < 0) { itemNamesWidth = 0; - const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + //const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; std::vector<int> itemWidths; for (size_t row2 = pdi.groupFirstRow; row2 < pdi.groupLastRow; ++row2) if (const FileSystemObject* fsObj = getDataView().getFsObject(row2)) if (itemPathFormat_ == ItemPathFormat::name || fsObj != pdi.folderGroupObj) - { +#if 0 //render same layout even when items don't exist if (fsObj->isEmpty<side>()) itemNamesWidth = ellipsisWidth; else +#endif itemWidths.push_back(getTextExtentBuffered(dc, utfTo<std::wstring>(fsObj->getItemName<side>())).x); - } if (!itemWidths.empty()) { @@ -601,17 +641,15 @@ private: } - struct GroupRenderLayout + struct GroupRowLayout { + std::wstring groupParentPart; //... if distributed over multiple rows, otherswise full group parent folder + std::wstring groupName; //only filled for first row of a group std::wstring itemName; - std::wstring groupName; - std::wstring groupParentFolder; - size_t groupFirstRow; - bool stackedGroupRender; int groupParentWidth; int groupNameWidth; }; - GroupRenderLayout getGroupRenderLayout(wxDC& dc, size_t row, const FileView::PathDrawInfo& pdi, int maxWidth) + GroupRowLayout getGroupRowLayout(wxDC& dc, size_t row, const FileView::PathDrawInfo& pdi, int maxWidth) { assert(pdi.fsObj); @@ -619,14 +657,15 @@ private: const int iconSize = getIconManager().getIconWxsize(); //-------------------------------------------------------------------- - const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; + const int arrowRightDownWidth = getTextExtentBuffered(dc, rightArrowDown_).x; const int groupItemNamesWidth = getGroupItemNamesWidth(dc, pdi); //-------------------------------------------------------------------- //exception for readability: top row is always group start! const size_t groupFirstRow = std::max<size_t>(pdi.groupFirstRow, refGrid().getRowAtWinPos(0)); - const bool multiItemGroup = pdi.groupLastRow - groupFirstRow > 1; + const size_t groupLineCount = pdi.groupLastRow - groupFirstRow; std::wstring itemName; if (itemPathFormat_ == ItemPathFormat::name || //hack: show folder name in item colum since groupName/groupParentFolder are unused! @@ -660,11 +699,23 @@ private: break; } - //path components should follow the app layout direction and are NOT a single piece of text! - //caveat: add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer" - assert(!contains(groupParentFolder, slashBidi_) && !contains(groupParentFolder, bslashBidi_)); - replace(groupParentFolder, L'/', slashBidi_); - replace(groupParentFolder, L'\\', bslashBidi_); + if (!groupParentFolder.empty()) + { + const wchar_t pathSep = [&] + { + for (auto it = groupParentFolder.end(); it != groupParentFolder.begin();) //reverse iteration: 1. check 2. decrement 3. evaluate + { + --it; // + + if (*it == L'/' || + *it == L'\\') + return *it; + } + return static_cast<wchar_t>(FILE_NAME_SEPARATOR); + }(); + if (!endsWith(groupParentFolder, pathSep)) //visual hint that this is a parent folder only + groupParentFolder += pathSep; // + } /* group details: single row ________________________ ___________________________________ _____________________________________________________ @@ -672,12 +723,15 @@ private: ------------------------ ----------------------------------- ----------------------------------------------------- group details: stacked - _____________________________________________________ _____________________________________________________ - | <right-aligned> (gap | icon | gap | group name) | | | (gap | icon) | gap | item name | <- group name on first row - |---------------------------------------------------| | (2x gap | vline) |--------------------------------| - | (gap | group parent_/\ | wide gap) | | | (gap | icon) | gap | item name | <- group parent on second - ----------------------------------------------------- ----------------------------------------------------- */ - bool stackedGroupRender = false; + __________________________________ ___________________________________ ___________________________________________________ + | gap | group parent, part 1 | ⤵️ | <right-aligned> | (gap | icon | gap | group name) | | | (gap | icon) | gap | item name | + |-------------------------------------------------------------------------------------| | 2x gap | vline |--------------------------------| + | gap | group parent, part n | | | (gap | icon) | gap | item name | + --------------------------------------------------------------------------------------- --------------------------------------------------- + + -> group name on first row + -> parent name distributed over multiple rows, if needed */ + int groupParentWidth = groupParentFolder.empty() ? 0 : (gapSize_ + getTextExtentBuffered(dc, groupParentFolder).x); int groupNameWidth = groupName.empty() ? 0 : (gapSize_ + iconSize + gapSize_ + getTextExtentBuffered(dc, groupName).x); @@ -688,84 +742,95 @@ private: int groupItemsWidth = groupSepWidth + (drawFileIcons ? gapSize_ + iconSize : 0) + gapSize_ + groupItemNamesWidth; const int groupItemsMinWidth = groupSepWidth + (drawFileIcons ? gapSize_ + iconSize : 0) + gapSize_ + ellipsisWidth; - //not enough space? => collapse + std::wstring groupParentPart; + if (row == groupFirstRow) + groupParentPart = groupParentFolder; + + //not enough space? => trim or render on multiple rows if (int excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; excessWidth > 0) { - if (multiItemGroup && !groupParentFolder.empty() && !groupName.empty()) - { - //1. render group components on two rows - stackedGroupRender = true; + const bool stackedGroupRender = !groupParentFolder.empty() && groupLineCount > 1; //group parent details on multiple rows - //add Unicode arrow to indicate that path was split - groupParentFolder += L'\u2934'; //Right Arrow Curving Up + //1. shrink group parent + if (!groupParentFolder.empty()) + { + const int groupParentMinWidth = stackedGroupRender && !groupName.empty() ? 0 : gapSize_ + ellipsisWidth; - const int groupParentMinWidth = gapSize_ + ellipsisWidth + gapSizeWide_; - groupParentWidth = gapSize_ + getTextExtentBuffered(dc, groupParentFolder).x + gapSizeWide_; + groupParentWidth = std::max(groupParentWidth - excessWidth, groupParentMinWidth); + excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; + } - int groupStackWidth = std::max(groupParentWidth, groupNameWidth); - excessWidth = groupStackWidth + groupItemsWidth - maxWidth; + if (excessWidth > 0) + { + //2. shrink item rendering + groupItemsWidth = std::max(groupItemsWidth - excessWidth, groupItemsMinWidth); + excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; if (excessWidth > 0) - { - //2. shrink group stack (group parent only) - if (groupParentWidth > groupNameWidth) - { - groupStackWidth = groupParentWidth = std::max({groupParentWidth - excessWidth, groupNameWidth, groupParentMinWidth}); - excessWidth = groupStackWidth + groupItemsWidth - maxWidth; - } - if (excessWidth > 0) - { - //3. shrink item rendering - groupItemsWidth = std::max(groupItemsWidth - excessWidth, groupItemsMinWidth); - excessWidth = groupStackWidth + groupItemsWidth - maxWidth; - - if (excessWidth > 0) - { - //4. shrink group stack - groupStackWidth = std::max({groupStackWidth - excessWidth, groupNameMinWidth, groupParentMinWidth}); - - groupParentWidth = std::min(groupParentWidth, groupStackWidth); - groupNameWidth = std::min(groupNameWidth, groupStackWidth); - } - } - } + //3. shrink group name + if (!groupName.empty()) + groupNameWidth = std::max(groupNameWidth - excessWidth, groupNameMinWidth); } - else //group details on single row + + if (stackedGroupRender) { - //1. shrink group parent - if (!groupParentFolder.empty()) + size_t comp1Len = getPathTrimmedSize(dc, groupParentFolder, groupParentWidth - gapSize_ - arrowRightDownWidth); + + if (!groupName.empty() && + getTextExtentBuffered(dc, makeStringView(groupParentFolder.begin(), comp1Len)).x > groupParentWidth - gapSize_ - arrowRightDownWidth) + comp1Len = 0; //exception: never truncate parent component on first row, but move to second row instead + + if (row == groupFirstRow) { - const int groupParentMinWidth = gapSize_ + ellipsisWidth; - groupParentWidth = std::max(groupParentWidth - excessWidth, groupParentMinWidth); - excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; + groupParentPart = groupParentFolder.substr(0, comp1Len); + + if (comp1Len != 0 && comp1Len != groupParentFolder.size()) + groupParentPart += rightArrowDown_; } - if (excessWidth > 0) + else { - //2. shrink item rendering - groupItemsWidth = std::max(groupItemsWidth - excessWidth, groupItemsMinWidth); - excessWidth = groupParentWidth + groupNameWidth + groupItemsWidth - maxWidth; + size_t compPos = comp1Len; - if (excessWidth > 0) - //3. shrink group name - if (!groupName.empty()) - groupNameWidth = std::max(groupNameWidth - excessWidth, groupNameMinWidth); + for (size_t i = groupFirstRow + 1; ; ++i) + { + const size_t compLen = getPathTrimmedSize(dc, makeStringView(groupParentFolder.begin() + compPos, groupParentFolder.end()), + groupParentWidth + groupNameWidth - gapSize_ - arrowRightDownWidth); + if (row == i) + { + groupParentPart = compPos + compLen == groupParentFolder.size() || + row == pdi.groupLastRow - 1 ? //not enough rows to show all parent folder components? + groupParentFolder.substr(compPos) : //=> append to last row => will be truncated with ellipsis + groupParentFolder.substr(compPos, compLen) + rightArrowDown_; + break; + } + compPos += compLen; + + if (compPos == groupParentFolder.size()) + break; + } } } } + //path components should follow the app layout direction and are NOT a single piece of text! + //caveat: - add Bidi support only during rendering and not in getValue() or AFS::getDisplayPath(): e.g. support "open file in Explorer" + // - add *after* getPathTrimmedSize(), otherwise LTR-mark can be confused for path component, e.g. "<LTR>/home" would be two components! + assert(!contains(groupParentPart, slashBidi_) && !contains(groupParentPart, bslashBidi_)); + replace(groupParentPart, L'/', slashBidi_); + replace(groupParentPart, L'\\', bslashBidi_); + return { - itemName, - groupName, - groupParentFolder, - groupFirstRow, - stackedGroupRender, - groupParentWidth, - groupNameWidth, + std::move(groupParentPart), + row == groupFirstRow ? std::move(groupName) : std::wstring{}, + std::move(itemName), + row == groupFirstRow ? groupParentWidth : groupParentWidth + groupNameWidth, + row == groupFirstRow ? groupNameWidth : 0, }; } + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override { //----------------------------------------------- @@ -882,36 +947,26 @@ private: }; //------------------------------------------------------------------------- - const auto& [itemName, + const auto& [groupParentPart, groupName, - groupParentFolder, - groupFirstRow, - stackedGroupRender, + itemName, groupParentWidth, - groupNameWidth] = getGroupRenderLayout(dc, row, pdi, rectTmp.width); + groupNameWidth] = getGroupRowLayout(dc, row, pdi, rectTmp.width); wxRect rectGroup, rectGroupParent, rectGroupName; rectGroup = rectGroupParent = rectGroupName = rectTmp; + rectGroup .width = groupParentWidth + groupNameWidth; rectGroupParent.width = groupParentWidth; rectGroupName .width = groupNameWidth; + rectGroupName.x += groupParentWidth; - if (stackedGroupRender) - { - rectGroup.width = std::max(groupParentWidth, groupNameWidth); - rectGroupName.x += rectGroup.width - groupNameWidth; //right-align - } - else //group details on single row - { - rectGroup.width = groupParentWidth + groupNameWidth; - rectGroupName.x += groupParentWidth; - } rectTmp.x += rectGroup.width; rectTmp.width -= rectGroup.width; wxRect rectGroupItems = rectTmp; - if (itemName.empty()) //expand group name to include (empty) item area + if (itemName.empty()) //expand group name to include unused item area (e.g. bigger selection border) { rectGroupName.width += rectGroupItems.width; rectGroupItems.width = 0; @@ -921,7 +976,7 @@ private: { //clear background below parent path => harmonize with renderRowBackgound() wxDCTextColourChanger textColorGroup(dc); - if ((!groupParentFolder.empty() || !groupName.empty()) && + if (rectGroup.width > 0 && (!enabled || !selected)) { wxRect rectGroupBack = rectGroup; @@ -936,22 +991,18 @@ private: //accessibility: always set *both* foreground AND background colors! } - if (!groupParentFolder.empty() && - (( stackedGroupRender && row == groupFirstRow + 1) || - (!stackedGroupRender && row == groupFirstRow)) && - (groupName.empty() || !pdi.folderGroupObj->isEmpty<side>())) //don't show for missing folders + if (!groupParentPart.empty() && + (!pdi.folderGroupObj || !pdi.folderGroupObj->isEmpty<side>())) //don't show for missing folders { tryDrawNavMarker(rectGroupParent); wxRect rectGroupParentText = rectGroupParent; rectGroupParentText.x += gapSize_; - rectGroupParentText.width -= stackedGroupRender ? gapSize_ + gapSizeWide_ : gapSize_; - - drawCellText(dc, rectGroupParentText, groupParentFolder, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupParentFolder)); + rectGroupParentText.width -= gapSize_; + drawCellText(dc, rectGroupParentText, groupParentPart, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &getTextExtentBuffered(dc, groupParentPart)); } - if (!groupName.empty() && - row == groupFirstRow) + if (!groupName.empty()) { wxRect rectGroupNameBack = rectGroupName; @@ -996,7 +1047,7 @@ private: if (!itemName.empty()) { //draw group/items separation line - if (!groupParentFolder.empty() || !groupName.empty()) + if (rectGroup.width > 0) { rectGroupItems.x += 2 * gapSize_; rectGroupItems.width -= 2 * gapSize_; @@ -1099,18 +1150,15 @@ private: if (const FileView::PathDrawInfo pdi = getDataView().getDrawInfo(row); pdi.fsObj) { - const auto& [itemName, + const auto& [groupParentPart, groupName, - groupParentFolder, - groupFirstRow, - stackedGroupRender, + itemName, groupParentWidth, - groupNameWidth] = getGroupRenderLayout(dc, row, pdi, cellWidth); + groupNameWidth] = getGroupRowLayout(dc, row, pdi, cellWidth); - if (!groupName.empty() && row == groupFirstRow && pdi.fsObj != pdi.folderGroupObj) + if (!groupName.empty() && pdi.fsObj != pdi.folderGroupObj) { - const int groupNameCellBeginX = (stackedGroupRender ? std::max(groupParentWidth, groupNameWidth) - groupNameWidth : //right-aligned - groupParentWidth); //group details on single row + const int groupNameCellBeginX = groupParentWidth; if (groupNameCellBeginX <= cellRelativePosX && cellRelativePosX < groupNameCellBeginX + groupNameWidth + 2 * gapSize_ /*include gap before vline*/) return static_cast<HoverArea>(HoverAreaGroup::groupName); @@ -1133,16 +1181,13 @@ private: /* ________________________ ___________________________________ _____________________________________________________ | (gap | group parent) | | (gap | icon | gap | group name) | | (2x gap | vline) | (gap | icon) | gap | item name | ------------------------ ----------------------------------- ----------------------------------------------------- */ - const auto& [itemName, + const auto& [groupParentPart, groupName, - groupParentFolder, - groupFirstRow, - stackedGroupRender, + itemName, groupParentWidth, - groupNameWidth] = getGroupRenderLayout(dc, row, pdi, insanelyHugeWidth); - assert(!stackedGroupRender); + groupNameWidth] = getGroupRowLayout(dc, row, pdi, insanelyHugeWidth); - const int groupSepWidth = (groupParentFolder.empty() && groupName.empty()) ? 0 : (2 * gapSize_ + dipToWxsize(1)); + const int groupSepWidth = groupParentWidth + groupNameWidth <= 0 ? 0 : (2 * gapSize_ + dipToWxsize(1)); const int fileIconWidth = getIconManager().getIconBuffer() ? gapSize_ + getIconManager().getIconWxsize() : 0; const int ellipsisWidth = getTextExtentBuffered(dc, ELLIPSIS).x; const int itemWidth = itemName.empty() ? 0 : @@ -1296,6 +1341,11 @@ private: const std::wstring bslashBidi_ = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? RTL_MARK : LTR_MARK) + std::wstring(L"\\"); //no need for LTR/RTL marks on both sides: text follows main direction if slash is between two strong characters with different directions + const std::wstring rightArrowDown_ = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? + std::wstring() + RTL_MARK + LEFT_ARROW_ANTICLOCK : + std::wstring() + LTR_MARK + RIGHT_ARROW_CURV_DOWN; + //Windows bug: RIGHT_ARROW_CURV_DOWN rendering and extent calculation is buggy (see wx+\tooltip.cpp) => need LTR mark! + std::vector<int> groupItemNamesWidthBuf_; //buffer! groupItemNamesWidths essentially only depends on (groupIdx, side) uint64_t viewUpdateIdLast_ = 0; // }; diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index fc7078ea..91ebdd7f 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -719,8 +719,7 @@ imgFileManagerSmall_([] wxAuiPaneInfo().Name(L"TopPanel").Layer(2).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false). PaneBorder(false).Gripper(). //BestSize(-1, m_panelTopButtons->GetSize().GetHeight() + dipToWxsize(10)). - MinSize(dipToWxsize(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight()) - ); + MinSize(dipToWxsize(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight())); //note: min height is calculated incorrectly by wxAuiManager if panes with and without caption are in the same row => use smaller min-size auiMgr_.AddPane(compareStatus_->getAsWindow(), @@ -734,7 +733,7 @@ imgFileManagerSmall_([] /* yes, m_panelDirectoryPairs's min height is overwritten in updateGuiForFolderPair(), but the default height might be wrong after increasing text size (Win10 Settings -> Accessibility -> Text size), e.g. to 150%: auiMgr_.LoadPerspective will load a too small "dock_size", so m_panelTopLeft/m_panelTopCenter will have squashed height */ - MinSize(dipToWxsize(100), m_panelDirectoryPairs->GetSize().y)); + MinSize(dipToWxsize(100), m_panelDirectoryPairs->GetSize().y).CloseButton(false)); m_panelSearch->GetSizer()->SetSizeHints(m_panelSearch); //~=Fit() + SetMinSize() auiMgr_.AddPane(m_panelSearch, @@ -3338,6 +3337,8 @@ void MainDialog::updateUnsavedCfgStatus() const bool allowSave = haveUnsavedCfg || activeConfigFiles_.size() > 1; + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + if (m_bpButtonSave->IsEnabled() != allowSave || !m_bpButtonSave->GetBitmap().IsOk()) //support polling { setImage(*m_bpButtonSave, allowSave ? loadImage("cfg_save") : makeBrightGrey(loadImage("cfg_save"))); @@ -3349,10 +3350,13 @@ void MainDialog::updateUnsavedCfgStatus() wxString title; if (haveUnsavedCfg) title += L'*'; - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalNativePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); - + bool showingConfigName = true; if (!activeCfgFilePath.empty()) - title += utfTo<wxString>(activeCfgFilePath); + { + title += extractJobName(activeCfgFilePath); + if (const std::optional<Zstring>& parentPath = getParentFolderPath(activeCfgFilePath)) + title += L" [" + utfTo<wxString>(*parentPath) + L']'; + } else if (activeConfigFiles_.size() > 1) { for (const std::wstring& jobName : getJobNames()) @@ -3361,12 +3365,12 @@ void MainDialog::updateUnsavedCfgStatus() title.resize(title.size() - 3); } else - { - title += L"FreeFileSync " + utfTo<std::wstring>(ffsVersion); - //if (!haveUnsavedCfg) - title += SPACED_DASH + _("Folder Comparison and Synchronization"); - } + showingConfigName = false; + + if (showingConfigName) + title += SPACED_DASH; + title += L"FreeFileSync " + utfTo<std::wstring>(ffsVersion); try { if (runningElevated()) //throw FileError @@ -3374,6 +3378,10 @@ void MainDialog::updateUnsavedCfgStatus() } catch (FileError&) { assert(false); } + if (!showingConfigName) + title += SPACED_DASH + _("Folder Comparison and Synchronization"); + + SetTitle(title); //macOS-only: diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 4fb89a75..1747bf72 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "13.4"; //internal linkage! +const char ffsVersion[] = "13.5"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } @@ -195,8 +195,12 @@ public: } else { - dc_.SetClippingRegion(r); - clippingAreas_.emplace(&dc_, r); + //caveat: actual clipping region is smaller when rect is not fully inside the DC + //=> ensure consistency for validateClippingBuffer() + const wxRect tmp = getIntersection(r, wxRect(dc.GetSize())); + + dc_.SetClippingRegion(tmp); + clippingAreas_.emplace(&dc_, tmp); } } diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp index 01b5ead4..14df955c 100644 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -75,10 +75,9 @@ void Tooltip::show(const wxString& text, wxPoint mousePos, const wxImage* img) if (txtChanged) { lastUsedText_ = text; - { tipWindow_->staticTextMain_->SetLabelText(text); - tipWindow_->staticTextMain_->Wrap(dipToWxsize(600)); - } + + tipWindow_->staticTextMain_->Wrap(dipToWxsize(600)); } if (imgChanged || txtChanged) @@ -35,6 +35,8 @@ struct TranslationHandler virtual std::wstring translate(const std::wstring& text) const = 0; //simple translation virtual std::wstring translate(const std::wstring& singular, const std::wstring& plural, int64_t n) const = 0; + virtual bool layoutIsRtl() const = 0; //right-to-left? e.g. Hebrew, Arabic + private: TranslationHandler (const TranslationHandler&) = delete; TranslationHandler& operator=(const TranslationHandler&) = delete; @@ -50,8 +52,6 @@ std::shared_ptr<const TranslationHandler> getTranslator(); - - //######################## implementation ############################## namespace impl { @@ -101,6 +101,15 @@ std::wstring translate(const std::wstring& singular, const std::wstring& plural, //fallback: return replaceCpy(std::abs(n64) == 1 ? singular : plural, L"%x", formatNumber(n)); } + + +inline +bool languageLayoutIsRtl() +{ + if (std::shared_ptr<const TranslationHandler> t = getTranslator()) + return t->layoutIsRtl(); + return false; +} } #endif //I18_N_H_3843489325044253425456 diff --git a/zen/process_exec.cpp b/zen/process_exec.cpp index 89df9f8b..46c04eb5 100644 --- a/zen/process_exec.cpp +++ b/zen/process_exec.cpp @@ -69,7 +69,7 @@ std::pair<int /*exit code*/, std::string> processExecuteImpl(const Zstring& file const int fdTempFile = ::open(tempFilePath.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, S_IRUSR | S_IWUSR); //0600 if (fdTempFile == -1) - THROW_LAST_SYS_ERROR("open"); + THROW_LAST_SYS_ERROR("open(" + utfTo<std::string>(tempFilePath) + ")"); auto guardTmpFile = makeGuard<ScopeGuardRunMode::onExit>([&] { ::close(fdTempFile); }); //"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE diff --git a/zen/zstring.h b/zen/zstring.h index bf7ac526..00b1c86e 100644 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -79,11 +79,11 @@ struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring& rhs) //------------------------------------------------------------------------------------------ //common Unicode characters -const wchar_t EN_DASH = L'\u2013'; -const wchar_t EM_DASH = L'\u2014'; +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* const ELLIPSIS = L"\u2026"; //"..." -const wchar_t MULT_SIGN = L'\u00D7'; //fancy "x" +const wchar_t* const ELLIPSIS = L"\u2026"; //… +const wchar_t MULT_SIGN = L'\u00D7'; //× const wchar_t NOBREAK_SPACE = L'\u00A0'; const wchar_t ZERO_WIDTH_SPACE = L'\u200B'; @@ -96,7 +96,10 @@ const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F https://www.w3.org/Interna //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 RIGHT_ARROW_CURV_DOWN = L'\u2935'; //Right Arrow Curving Down +const wchar_t RIGHT_ARROW_CURV_DOWN = L'\u2935'; //Right Arrow Curving Down: ⤵ +//Windows bug: rendered differently depending on presence of e.g. LTR_MARK! +//there is no "Left Arrow Curving Down" => WTF => better than nothing: +const wchar_t LEFT_ARROW_ANTICLOCK = L'\u2B8F'; //Anticlockwise Triangle-Headed Top U-Shaped Arrow: ⮏ const wchar_t* const TAB_SPACE = L" "; //4: the only sensible space count for tabs |