summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Bugs.txt2
-rw-r--r--Changelog.txt9
-rw-r--r--FreeFileSync/Build/Resources/cacert.pem51
-rw-r--r--FreeFileSync/Source/RealTimeSync/application.cpp2
-rw-r--r--FreeFileSync/Source/application.cpp2
-rw-r--r--FreeFileSync/Source/base/algorithm.cpp49
-rw-r--r--FreeFileSync/Source/base/algorithm.h2
-rw-r--r--FreeFileSync/Source/base/cmp_filetime.h7
-rw-r--r--FreeFileSync/Source/base/comparison.cpp6
-rw-r--r--FreeFileSync/Source/base/comparison.h2
-rw-r--r--FreeFileSync/Source/base/file_hierarchy.cpp11
-rw-r--r--FreeFileSync/Source/base/file_hierarchy.h6
-rw-r--r--FreeFileSync/Source/base/synchronization.cpp130
-rw-r--r--FreeFileSync/Source/config.h2
-rw-r--r--FreeFileSync/Source/localization.cpp24
-rw-r--r--FreeFileSync/Source/localization.h1
-rw-r--r--FreeFileSync/Source/ui/file_grid.cpp296
-rw-r--r--FreeFileSync/Source/ui/main_dlg.cpp30
-rw-r--r--FreeFileSync/Source/version/version.h2
-rw-r--r--wx+/dc.h8
-rw-r--r--wx+/tooltip.cpp5
-rw-r--r--zen/i18n.h13
-rw-r--r--zen/process_exec.cpp2
-rw-r--r--zen/zstring.h13
24 files changed, 404 insertions, 271 deletions
diff --git a/Bugs.txt b/Bugs.txt
index 844b7d83..7a858099 100644
--- a/Bugs.txt
+++ b/Bugs.txt
@@ -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 = '.';
}
diff --git a/wx+/dc.h b/wx+/dc.h
index 619f6628..522e3bc8 100644
--- a/wx+/dc.h
+++ b/wx+/dc.h
@@ -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)
diff --git a/zen/i18n.h b/zen/i18n.h
index e858913a..28b8c087 100644
--- a/zen/i18n.h
+++ b/zen/i18n.h
@@ -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
bgstack15