diff options
64 files changed, 3709 insertions, 2624 deletions
diff --git a/Changelog.txt b/Changelog.txt index 50d6e0af..551fd1d2 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,16 @@ +FreeFileSync 13.0 [2023-09-12] +------------------------------ +Rename (multiple) files manually (F2 key) +Configure individual directions for DB-based sync +Detect moved files with "Update" sync variant (requires sync.ffs_db files) +Update variant: Do not restore files that were deleted on target +Distinguish file renames from file moves and simplify grid display +Fixed ERROR_NOT_SUPPORTED when copying files with NTFS extended attributes +Fixed error during process initialization while connecting with quick launch +Avoid redundant file reopen when setting file times during copy +Set working directory to match FFS configuration file when double-clicking (Linux) + + FreeFileSync 12.5 [2023-07-21] ------------------------------ Merge logs of individual steps (comparison, manual operation, sync) diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip Binary files differindex 12822e10..dc23279f 100644 --- a/FreeFileSync/Build/Resources/Icons.zip +++ b/FreeFileSync/Build/Resources/Icons.zip diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex 8a828e86..3437d30b 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip diff --git a/FreeFileSync/Build/Resources/cacert.pem b/FreeFileSync/Build/Resources/cacert.pem index 6b93dc34..9551dfd8 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 May 30 03:12:04 2023 GMT +## Certificate data from Mozilla as of: Tue Aug 22 03:12:04 2023 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: c47475103fb05bb562bbadff0d1e72346b03236154e1448a6ca191b740f83507 +## SHA256: 0ff137babc6a5561a9cfbe9f29558972e5b528202681b7d3803d03a3e82922bd ## @@ -3222,55 +3222,6 @@ AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR -----END CERTIFICATE----- -E-Tugra Global Root CA RSA v3 -============================= ------BEGIN CERTIFICATE----- -MIIF8zCCA9ugAwIBAgIUDU3FzRYilZYIfrgLfxUGNPt5EDQwDQYJKoZIhvcNAQELBQAwgYAxCzAJ -BgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVncmEgRUJHIEEuUy4xHTAb -BgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290 -IENBIFJTQSB2MzAeFw0yMDAzMTgwOTA3MTdaFw00NTAzMTIwOTA3MTdaMIGAMQswCQYDVQQGEwJU -UjEPMA0GA1UEBxMGQW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRF -LVR1Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBSU0Eg -djMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiZvCJt3J77gnJY9LTQ91ew6aEOErx -jYG7FL1H6EAX8z3DeEVypi6Q3po61CBxyryfHUuXCscxuj7X/iWpKo429NEvx7epXTPcMHD4QGxL -sqYxYdE0PD0xesevxKenhOGXpOhL9hd87jwH7eKKV9y2+/hDJVDqJ4GohryPUkqWOmAalrv9c/SF -/YP9f4RtNGx/ardLAQO/rWm31zLZ9Vdq6YaCPqVmMbMWPcLzJmAy01IesGykNz709a/r4d+ABs8q -QedmCeFLl+d3vSFtKbZnwy1+7dZ5ZdHPOrbRsV5WYVB6Ws5OUDGAA5hH5+QYfERaxqSzO8bGwzrw -bMOLyKSRBfP12baqBqG3q+Sx6iEUXIOk/P+2UNOMEiaZdnDpwA+mdPy70Bt4znKS4iicvObpCdg6 -04nmvi533wEKb5b25Y08TVJ2Glbhc34XrD2tbKNSEhhw5oBOM/J+JjKsBY04pOZ2PJ8QaQ5tndLB -eSBrW88zjdGUdjXnXVXHt6woq0bM5zshtQoK5EpZ3IE1S0SVEgpnpaH/WwAH0sDM+T/8nzPyAPiM -bIedBi3x7+PmBvrFZhNb/FAHnnGGstpvdDDPk1Po3CLW3iAfYY2jLqN4MpBs3KwytQXk9TwzDdbg -h3cXTJ2w2AmoDVf3RIXwyAS+XF1a4xeOVGNpf0l0ZAWMowIDAQABo2MwYTAPBgNVHRMBAf8EBTAD -AQH/MB8GA1UdIwQYMBaAFLK0ruYt9ybVqnUtdkvAG1Mh0EjvMB0GA1UdDgQWBBSytK7mLfcm1ap1 -LXZLwBtTIdBI7zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAImocn+M684uGMQQ -gC0QDP/7FM0E4BQ8Tpr7nym/Ip5XuYJzEmMmtcyQ6dIqKe6cLcwsmb5FJ+Sxce3kOJUxQfJ9emN4 -38o2Fi+CiJ+8EUdPdk3ILY7r3y18Tjvarvbj2l0Upq7ohUSdBm6O++96SmotKygY/r+QLHUWnw/q -ln0F7psTpURs+APQ3SPh/QMSEgj0GDSz4DcLdxEBSL9htLX4GdnLTeqjjO/98Aa1bZL0SmFQhO3s -SdPkvmjmLuMxC1QLGpLWgti2omU8ZgT5Vdps+9u1FGZNlIM7zR6mK7L+d0CGq+ffCsn99t2HVhjY -sCxVYJb6CH5SkPVLpi6HfMsg2wY+oF0Dd32iPBMbKaITVaA9FCKvb7jQmhty3QUBjYZgv6Rn7rWl -DdF/5horYmbDB7rnoEgcOMPpRfunf/ztAmgayncSd6YAVSgU7NbHEqIbZULpkejLPoeJVF3Zr52X -nGnnCv8PWniLYypMfUeUP95L6VPQMPHF9p5J3zugkaOj/s1YzOrfr28oO6Bpm4/srK4rVJ2bBLFH -IK+WEj5jlB0E5y67hscMmoi/dkfv97ALl2bSRM9gUgfh1SxKOidhd8rXj+eHDjD/DLsE4mHDosiX -YY60MGo8bcIHX0pzLz/5FooBZu+6kcpSV3uu1OYP3Qt6f4ueJiDPO++BcYNZ ------END CERTIFICATE----- - -E-Tugra Global Root CA ECC v3 -============================= ------BEGIN CERTIFICATE----- -MIICpTCCAiqgAwIBAgIUJkYZdzHhT28oNt45UYbm1JeIIsEwCgYIKoZIzj0EAwMwgYAxCzAJBgNV -BAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVncmEgRUJHIEEuUy4xHTAbBgNV -BAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENB -IEVDQyB2MzAeFw0yMDAzMTgwOTQ2NThaFw00NTAzMTIwOTQ2NThaMIGAMQswCQYDVQQGEwJUUjEP -MA0GA1UEBxMGQW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1 -Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBFQ0MgdjMw -djAQBgcqhkjOPQIBBgUrgQQAIgNiAASOmCm/xxAeJ9urA8woLNheSBkQKczLWYHMjLiSF4mDKpL2 -w6QdTGLVn9agRtwcvHbB40fQWxPa56WzZkjnIZpKT4YKfWzqTTKACrJ6CZtpS5iB4i7sAnCWH/31 -Rs7K3IKjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU/4Ixcj75xGZsrTie0bBRiKWQ -zPUwHQYDVR0OBBYEFP+CMXI++cRmbK04ntGwUYilkMz1MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO -PQQDAwNpADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/67W4W -Aie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFxvmjkI6TZraE3 ------END CERTIFICATE----- - Security Communication RootCA3 ============================== -----BEGIN CERTIFICATE----- @@ -3361,3 +3312,140 @@ SR9BIgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK++kpRuDCK W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8g UXOQwKhbYdDFUDn9hf7B43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== -----END CERTIFICATE----- + +Sectigo Public Server Authentication Root E46 +============================================= +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH +QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2 +ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5 +WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0 +aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr +gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0 +NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud +DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH +lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U +SAGKcw== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root R46 +============================================= +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1 +OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k +1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf +GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP +FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu +ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz +Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A +wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF +plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ +EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW +6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI +IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp +E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4 +exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M +0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI +84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m +pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd +Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b +E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm +J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +SSL.com TLS RSA Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQG +EwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBSU0Eg +Um9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloXDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMC +VVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u +9nTPL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OYt6/wNr/y +7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0insS657Lb85/bRi3pZ7Qcac +oOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3PnxEX4MN8/HdIGkWCVDi1FW24IBydm5M +R7d1VVm0U3TZlMZBrViKMWYPHqIbKUBOL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDG +D6C1vBdOSHtRwvzpXGk3R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEW +TO6Af77wdr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS+YCk +8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYSd66UNHsef8JmAOSq +g+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoGAtUjHBPW6dvbxrB6y3snm/vg1UYk +7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2fgTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsu +N+7jhHonLs0ZNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsMQtfhWsSWTVTN +j8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvfR4iyrT7gJ4eLSYwfqUdYe5by +iB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjU +o3KUQyxi4U5cMj29TH0ZR6LDSeeWP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqo +ENjwuSfr98t67wVylrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7Egkaib +MOlqbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2wAgDHbICi +vRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3qr5nsLFR+jM4uElZI7xc7 +P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sjiMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB0 +9+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +SSL.com TLS ECC Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBFQ0MgUm9v +dCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMx +GDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWy +JGYmacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFNSeR7T5v1 +5wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSJjy+j6CugFFR7 +81a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NWuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGG +MAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w +7deedWo1dlJF4AIxAMeNb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5 +Zn6g6g== +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA ECC TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4wLAYDVQQDDCVB +dG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQswCQYD +VQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3Mg +VHJ1c3RlZFJvb3QgUm9vdCBDQSBFQ0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYT +AkRFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6K +DP/XtXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4AjJn8ZQS +b+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2KCXWfeBmmnoJsmo7jjPX +NtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwW5kp85wxtolrbNa9d+F851F+ +uDrNozZffPc8dz7kUK2o59JZDCaOMDtuCCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGY +a3cpetskz2VAv9LcjBHo9H1/IISpQuQo +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA RSA TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBMMS4wLAYDVQQD +DCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQsw +CQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0 +b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNV +BAYTAkRFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BB +l01Z4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYvYe+W/CBG +vevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZkmGbzSoXfduP9LVq6hdK +ZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDsGY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt +0xU6kGpn8bRrZtkh68rZYnxGEFzedUlnnkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVK +PNe0OwANwI8f4UDErmwh3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMY +sluMWuPD0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzygeBY +Br3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8ANSbhqRAvNncTFd+ +rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezBc6eUWsuSZIKmAMFwoW4sKeFYV+xa +fJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lIpw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUdEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0G +CSqGSIb3DQEBDAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPso0UvFJ/1TCpl +Q3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJqM7F78PRreBrAwA0JrRUITWX +AdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuywxfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9G +slA9hGCZcbUztVdF5kJHdWoOsAgMrr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2Vkt +afcxBPTy+av5EzH4AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9q +TFsR0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuYo7Ey7Nmj +1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5dDTedk+SKlOxJTnbPP/l +PqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcEoji2jbDwN/zIIX8/syQbPYtuzE2wFg2W +HYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index 4a94d1cf..6fc74c37 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -9,7 +9,6 @@ #include <functional> #include <chrono> -//#include <variant> #include <zen/file_error.h> #include <zen/file_path.h> #include <zen/serialize.h> //InputStream/OutputStream support buffered stream concept @@ -261,6 +260,9 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //already existing: undefined behavior! (e.g. fail/overwrite) static void moveAndRenameItem(const AbstractPath& pathFrom, const AbstractPath& pathTo); //throw FileError, ErrorMoveUnsupported + static std::wstring generateMoveErrorMsg(const AbstractPath& pathFrom, const AbstractPath& pathTo) { return pathFrom.afsDevice.ref().generateMoveErrorMsg(pathFrom.afsPath, pathTo); } + + //Note: it MAY happen that copyFileTransactional() leaves temp files behind, e.g. temporary network drop. // => clean them up at an appropriate time (automatically set sync directions to delete them). They have the following ending: static inline constexpr ZstringView TEMP_FILE_ENDING = Zstr(".ffs_tmp"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! @@ -351,6 +353,21 @@ protected: FileCopyResult copyFileAsStream(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, ErrorFileLocked, X const AbstractPath& targetPath, const zen::IoCallback& notifyUnbufferedIO /*throw X*/) const; + + std::wstring generateMoveErrorMsg(const AfsPath& pathFrom, const AbstractPath& pathTo) const + { + using namespace zen; + + if (getParentPath(pathFrom) == getParentPath(pathTo.afsPath)) //pure "rename" + return replaceCpy(replaceCpy(_("Cannot rename %x to %y."), + L"%x", fmtPath(getDisplayPath(pathFrom))), + L"%y", fmtPath(getItemName(pathTo))); + else //"move" or "move + rename" + return trimCpy(replaceCpy(replaceCpy(_("Cannot move %x to %y."), + L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), + L"%y", L'\n' + fmtPath(getDisplayPath(pathTo)))); + } + private: virtual std::optional<Zstring> getNativeItemPath(const AfsPath& itemPath) const { return {}; }; @@ -532,9 +549,7 @@ void AbstractFileSystem::moveAndRenameItem(const AbstractPath& pathFrom, const A using namespace zen; if (typeid(pathFrom.afsDevice.ref()) != typeid(pathTo.afsDevice.ref())) - throw ErrorMoveUnsupported(replaceCpy(replaceCpy(_("Cannot move file %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), - L"%y", L'\n' + fmtPath(getDisplayPath(pathTo))), _("Operation not supported between different devices.")); + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); //already existing: undefined behavior! (e.g. fail/overwrite) pathFrom.afsDevice.ref().moveAndRenameItemForSameAfsType(pathFrom.afsPath, pathTo); //throw FileError, ErrorMoveUnsupported diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index 283ca3ec..d72daf92 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -2119,7 +2119,7 @@ struct OutputStreamFtp : public AFS::OutputStreamImpl /* is setting modtime after closing the file handle a pessimization? FTP: no: could set modtime via CURLOPT_POSTQUOTE (but this would internally trigger an extra round-trip anyway!) */ } - catch (const FileError& e) { result.errorModTime = FileError(e.toString()); /*avoid slicing*/ } + catch (const FileError& e) { result.errorModTime = e; /*might slice derived class?*/ } return result; } @@ -2463,13 +2463,8 @@ private: // FileZilla Server: CURLE_QUOTE_ERROR: QUOT command failed with 553 file exists void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported { - auto generateErrorMsg = [&] { return replaceCpy(replaceCpy(_("Cannot move file %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), - L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))); - }; - if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) - throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); try { @@ -2489,7 +2484,7 @@ private: } catch (const SysError& e) { - throw FileError(generateErrorMsg(), e.toString()); + throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); } } diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index 25c0255b..e6215b63 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -3774,13 +3774,8 @@ private: //=> actual behavior: 1. fails or 2. creates duplicate (unlikely) void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported { - auto generateErrorMsg = [&] { return replaceCpy(replaceCpy(_("Cannot move file %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), - L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))); - }; - if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) - throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); //note: moving files within account works, e.g. between My Drive <-> shared drives // BUT: not supported by our model with separate GdriveFileStates; e.g. how to handle complexity of a moved folder (tree)? try @@ -3837,7 +3832,7 @@ private: fileState.all().notifyMoveAndRename(aai.stateDelta, itemId, parentIdFrom, parentIdTo, itemNameNew); }); } - catch (const SysError& e) { throw FileError(generateErrorMsg(), e.toString()); } + catch (const SysError& e) { throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); } } bool supportsPermissions(const AfsPath& folderPath) const override { return false; } //throw FileError diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index 46b2bb4d..cbcce134 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -375,20 +375,18 @@ struct OutputStreamNative : public AFS::OutputStreamImpl AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X { AFS::FinalizeResult result; - if (modTime_) - result.filePrint = getNativeFileInfo(fileOut_).filePrint; //throw FileError - fileOut_.close(); //throw FileError + result.filePrint = getNativeFileInfo(fileOut_).filePrint; //throw FileError + fileOut_.close(); //throw FileError + /* is setting modtime after closing the file handle a pessimization? + no, needed for functional correctness, see file_access.cpp::copyNewFile() for macOS/Linux */ try { if (modTime_) setFileTime(fileOut_.getFilePath(), *modTime_, ProcSymlink::follow); //throw FileError - /* is setting modtime after closing the file handle a pessimization? - no, needed for functional correctness, see file_access.cpp */ } - catch (const FileError& e) { result.errorModTime = FileError(e.toString()); /*avoid slicing*/ } - + catch (const FileError& e) { result.errorModTime = e; /*might slice derived class?*/ } return result; } @@ -626,10 +624,8 @@ private: //perf test: detecting different volumes by path is ~30 times faster than having ::MoveFileEx() fail with ERROR_NOT_SAME_DEVICE (6µs vs 190µs) //=> maybe we can even save some actual I/O in some cases? if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) - throw ErrorMoveUnsupported(replaceCpy(replaceCpy(_("Cannot move file %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), - L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))), - _("Operation not supported between different devices.")); + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); + initComForThread(); //throw FileError const Zstring nativePathTarget = static_cast<const NativeFileSystem&>(pathTo.afsDevice.ref()).getNativePath(pathTo.afsPath); diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index 4787b668..795b5a01 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -1820,13 +1820,8 @@ private: //=> actual behavior: fail with obscure LIBSSH2_FX_FAILURE error void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported { - auto generateErrorMsg = [&] { return replaceCpy(replaceCpy(_("Cannot move file %x to %y."), - L"%x", L'\n' + fmtPath(getDisplayPath(pathFrom))), - L"%y", L'\n' + fmtPath(AFS::getDisplayPath(pathTo))); - }; - if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent) - throw ErrorMoveUnsupported(generateErrorMsg(), _("Operation not supported between different devices.")); + throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices.")); try { @@ -1849,7 +1844,7 @@ private: } catch (const SysError& e) //libssh2_sftp_rename_ex reports generic LIBSSH2_FX_FAILURE if target is already existing! { - throw FileError(generateErrorMsg(), e.toString()); + throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString()); } } diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index 272fed83..baaf660f 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -418,7 +418,7 @@ void Application::onEnterEventLoop() else { XmlGuiConfig guiCfg; - guiCfg.mainCfg.syncCfg.directionCfg.var = SyncVariant::mirror; + guiCfg.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::mirror); replaceDirectories(guiCfg.mainCfg); //throw FileError diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index 5ff1cfbc..6bd2d203 100644 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -34,21 +34,21 @@ void fff::swapGrids(const MainConfiguration& mainCfg, FolderComparison& folderCm namespace { //visitFSObjectRecursively? nope, see premature end of traversal in processFolder() -class SetSyncDirectionByConfig +class SetSyncDirViaDifferences { public: - static void execute(const DirectionSet& dirCfgIn, ContainerObject& hierObj) { SetSyncDirectionByConfig(dirCfgIn).recurse(hierObj); } + static void execute(const DirectionByDiff& dirs, ContainerObject& conObj) { SetSyncDirViaDifferences(dirs).recurse(conObj); } private: - SetSyncDirectionByConfig(const DirectionSet& dirCfgIn) : dirCfg_(dirCfgIn) {} + SetSyncDirViaDifferences(const DirectionByDiff& dirs) : dirs_(dirs) {} - void recurse(ContainerObject& hierObj) const + void recurse(ContainerObject& conObj) const { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) processFile(file); - for (SymlinkPair& link : hierObj.refSubLinks()) + for (SymlinkPair& link : conObj.refSubLinks()) processLink(link); - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) processFolder(folder); } @@ -57,38 +57,49 @@ private: const CompareFileResult cat = file.getCategory(); //##################### schedule old temporary files for deletion #################### - if (cat == FILE_LEFT_SIDE_ONLY && endsWith(file.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) + if (cat == FILE_LEFT_ONLY && endsWith(file.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) return file.setSyncDir(SyncDirection::left); - else if (cat == FILE_RIGHT_SIDE_ONLY && endsWith(file.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) + else if (cat == FILE_RIGHT_ONLY && endsWith(file.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) return file.setSyncDir(SyncDirection::right); //#################################################################################### switch (cat) { - case FILE_LEFT_SIDE_ONLY: - file.setSyncDir(dirCfg_.exLeftSideOnly); + case FILE_EQUAL: + //file.setSyncDir(SyncDirection::none); break; - case FILE_RIGHT_SIDE_ONLY: - file.setSyncDir(dirCfg_.exRightSideOnly); + case FILE_RENAMED: + if (dirs_.leftNewer == dirs_.rightNewer) + file.setSyncDir(dirs_.leftNewer); //treat "rename" like a "file update" + else + file.setSyncDirConflict(txtDiffName_); break; - case FILE_RIGHT_NEWER: - file.setSyncDir(dirCfg_.rightNewer); + case FILE_LEFT_ONLY: + file.setSyncDir(dirs_.leftOnly); + break; + case FILE_RIGHT_ONLY: + file.setSyncDir(dirs_.rightOnly); break; case FILE_LEFT_NEWER: - file.setSyncDir(dirCfg_.leftNewer); + file.setSyncDir(dirs_.leftNewer); break; - case FILE_DIFFERENT_CONTENT: - file.setSyncDir(dirCfg_.different); + case FILE_RIGHT_NEWER: + file.setSyncDir(dirs_.rightNewer); break; - case FILE_CONFLICT: - case FILE_DIFFERENT_METADATA: //use setting from "conflict/cannot categorize" - if (dirCfg_.conflict == SyncDirection::none) - file.setSyncDirConflict(file.getCatExtraDescription()); //take over category conflict + case FILE_TIME_INVALID: + if (dirs_.leftNewer == dirs_.rightNewer) //e.g. "Mirror" sync variant + file.setSyncDir(dirs_.leftNewer); else - file.setSyncDir(dirCfg_.conflict); + file.setSyncDirConflict(file.getCategoryCustomDescription()); break; - case FILE_EQUAL: - file.setSyncDir(SyncDirection::none); + case FILE_DIFFERENT_CONTENT: + if (dirs_.leftNewer == dirs_.rightNewer) + file.setSyncDir(dirs_.leftNewer); + else + file.setSyncDirConflict(txtDiffContent_); + break; + case FILE_CONFLICT: + file.setSyncDirConflict(file.getCategoryCustomDescription()); //take over category conflict: allow *manual* resolution only! break; } } @@ -97,30 +108,41 @@ private: { switch (symlink.getLinkCategory()) { - case SYMLINK_LEFT_SIDE_ONLY: - symlink.setSyncDir(dirCfg_.exLeftSideOnly); + case SYMLINK_EQUAL: + //symlink.setSyncDir(SyncDirection::none); + break; + case SYMLINK_RENAMED: + if (dirs_.leftNewer == dirs_.rightNewer) + symlink.setSyncDir(dirs_.leftNewer); + else + symlink.setSyncDirConflict(txtDiffName_); break; - case SYMLINK_RIGHT_SIDE_ONLY: - symlink.setSyncDir(dirCfg_.exRightSideOnly); + case SYMLINK_LEFT_ONLY: + symlink.setSyncDir(dirs_.leftOnly); + break; + case SYMLINK_RIGHT_ONLY: + symlink.setSyncDir(dirs_.rightOnly); break; case SYMLINK_LEFT_NEWER: - symlink.setSyncDir(dirCfg_.leftNewer); + symlink.setSyncDir(dirs_.leftNewer); break; case SYMLINK_RIGHT_NEWER: - symlink.setSyncDir(dirCfg_.rightNewer); + symlink.setSyncDir(dirs_.rightNewer); break; - case SYMLINK_CONFLICT: - case SYMLINK_DIFFERENT_METADATA: //use setting from "conflict/cannot categorize" - if (dirCfg_.conflict == SyncDirection::none) - symlink.setSyncDirConflict(symlink.getCatExtraDescription()); //take over category conflict + case SYMLINK_TIME_INVALID: + if (dirs_.leftNewer == dirs_.rightNewer) + symlink.setSyncDir(dirs_.leftNewer); else - symlink.setSyncDir(dirCfg_.conflict); + symlink.setSyncDirConflict(symlink.getCategoryCustomDescription()); break; case SYMLINK_DIFFERENT_CONTENT: - symlink.setSyncDir(dirCfg_.different); + if (dirs_.leftNewer == dirs_.rightNewer) + symlink.setSyncDir(dirs_.leftNewer); + else + symlink.setSyncDirConflict(txtDiffContent_); break; - case SYMLINK_EQUAL: - symlink.setSyncDir(SyncDirection::none); + case SYMLINK_CONFLICT: + symlink.setSyncDirConflict(symlink.getCategoryCustomDescription()); //take over category conflict: allow *manual* resolution only! break; } } @@ -130,50 +152,56 @@ private: const CompareDirResult cat = folder.getDirCategory(); //########### schedule abandoned temporary recycle bin directory for deletion ########## - if (cat == DIR_LEFT_SIDE_ONLY && endsWith(folder.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) + if (cat == DIR_LEFT_ONLY && endsWith(folder.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::left, folder); // - else if (cat == DIR_RIGHT_SIDE_ONLY && endsWith(folder.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) + else if (cat == DIR_RIGHT_ONLY && endsWith(folder.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::right, folder); //don't recurse below! //####################################################################################### switch (cat) { - case DIR_LEFT_SIDE_ONLY: - folder.setSyncDir(dirCfg_.exLeftSideOnly); + case DIR_EQUAL: + //folder.setSyncDir(SyncDirection::none); break; - case DIR_RIGHT_SIDE_ONLY: - folder.setSyncDir(dirCfg_.exRightSideOnly); + case DIR_RENAMED: + if (dirs_.leftNewer == dirs_.rightNewer) + folder.setSyncDir(dirs_.leftNewer); + else + folder.setSyncDirConflict(txtDiffName_); break; - case DIR_EQUAL: - folder.setSyncDir(SyncDirection::none); + case DIR_LEFT_ONLY: + folder.setSyncDir(dirs_.leftOnly); + break; + case DIR_RIGHT_ONLY: + folder.setSyncDir(dirs_.rightOnly); break; case DIR_CONFLICT: - case DIR_DIFFERENT_METADATA: //use setting from "conflict/cannot categorize" - if (dirCfg_.conflict == SyncDirection::none) - folder.setSyncDirConflict(folder.getCatExtraDescription()); //take over category conflict - else - folder.setSyncDir(dirCfg_.conflict); + folder.setSyncDirConflict(folder.getCategoryCustomDescription()); //take over category conflict: allow *manual* resolution only! break; } recurse(folder); } - const DirectionSet dirCfg_; + const DirectionByDiff dirs_; + const Zstringc txtDiffName_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + + _("The items have different names, but it's unknown which side was renamed.")); + const Zstringc txtDiffContent_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + + _("The items have different content, but it's unknown which side has changed.")); }; //--------------------------------------------------------------------------------------------------------------- //test if non-equal items exist in scanned data -bool allItemsCategoryEqual(const ContainerObject& hierObj) +bool allItemsCategoryEqual(const ContainerObject& conObj) { - return std::all_of(hierObj.refSubFiles().begin(), hierObj.refSubFiles().end(), + return std::all_of(conObj.refSubFiles().begin(), conObj.refSubFiles().end(), [](const FilePair& file) { return file.getCategory() == FILE_EQUAL; })&& - std::all_of(hierObj.refSubLinks().begin(), hierObj.refSubLinks().end(), + std::all_of(conObj.refSubLinks().begin(), conObj.refSubLinks().end(), [](const SymlinkPair& symlink) { return symlink.getLinkCategory() == SYMLINK_EQUAL; })&& - std::all_of(hierObj.refSubFolders().begin(), hierObj.refSubFolders().end(), [](const FolderPair& folder) + std::all_of(conObj.refSubFolders().begin(), conObj.refSubFolders().end(), [](const FolderPair& folder) { return folder.getDirCategory() == DIR_EQUAL && allItemsCategoryEqual(folder); //short-circuit behavior! }); @@ -190,19 +218,20 @@ bool fff::allElementsEqual(const FolderComparison& folderCmp) namespace { template <SelectSide side> inline -bool matchesDbEntry(const FilePair& file, const InSyncFile* dbFile, const std::vector<unsigned int>& ignoreTimeShiftMinutes) +CudAction compareDbEntry(const FilePair& file, const InSyncFile* dbFile, const std::vector<unsigned int>& ignoreTimeShiftMinutes, bool renamedOrMoved) { if (file.isEmpty<side>()) - return !dbFile; + return dbFile ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; else if (!dbFile) - return false; + return (renamedOrMoved ? CudAction::update : CudAction::create); const InSyncDescrFile& descrDb = selectParam<side>(dbFile->left, dbFile->right); - return //we're not interested in "fileTimeTolerance" here! - sameFileTime(file.getLastWriteTime<side>(), descrDb.modTime, FAT_FILE_TIME_PRECISION_SEC, ignoreTimeShiftMinutes) && - file.getFileSize<side>() == dbFile->fileSize; - //note: we do *not* consider file ID here, but are only interested in *visual* changes. Consider user moving data to some other medium, this is not a change! + return sameFileTime(file.getLastWriteTime<side>(), descrDb.modTime, FAT_FILE_TIME_PRECISION_SEC, ignoreTimeShiftMinutes) && + //- we're not interested in "fileTimeTolerance"! + //- 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; } @@ -215,15 +244,15 @@ bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, int fileTi case CompareVariant::timeSize: if (dbFile.cmpVar == CompareVariant::content) return true; //special rule: this is certainly "good enough" for CompareVariant::timeSize! - //case-sensitive short name match is a database invariant! + //case-sensitive file name match is a database invariant! return sameFileTime(dbFile.left.modTime, dbFile.right.modTime, fileTimeTolerance, ignoreTimeShiftMinutes); case CompareVariant::content: - //case-sensitive short name match is a database invariant! + //case-sensitive file name match is a database invariant! return dbFile.cmpVar == CompareVariant::content; //in contrast to comparison, we don't care about modification time here! - case CompareVariant::size: //file size/case-sensitive short name always matches on both sides for an "in-sync" database entry + case CompareVariant::size: //file size/case-sensitive file name always matches on both sides for an "in-sync" database entry return true; } assert(false); @@ -234,16 +263,17 @@ 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 -bool matchesDbEntry(const SymlinkPair& symlink, const InSyncSymlink* dbSymlink, const std::vector<unsigned int>& ignoreTimeShiftMinutes) +CudAction compareDbEntry(const SymlinkPair& symlink, const InSyncSymlink* dbSymlink, const std::vector<unsigned int>& ignoreTimeShiftMinutes, bool renamedOrMoved) { if (symlink.isEmpty<side>()) - return !dbSymlink; + return dbSymlink ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; else if (!dbSymlink) - return false; + return (renamedOrMoved ? CudAction::update : CudAction::create); 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, FAT_FILE_TIME_PRECISION_SEC, ignoreTimeShiftMinutes) ? + CudAction::noChange : CudAction::update; } @@ -257,12 +287,12 @@ bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, int fil if (dbLink.cmpVar == CompareVariant::content || dbLink.cmpVar == CompareVariant::size) return true; //special rule: this is already "good enough" for CompareVariant::timeSize! - //case-sensitive short name match is a database invariant! + //case-sensitive symlink name match is a database invariant! return sameFileTime(dbLink.left.modTime, dbLink.right.modTime, fileTimeTolerance, ignoreTimeShiftMinutes); case CompareVariant::content: case CompareVariant::size: //== categorized by content! see comparison.cpp, ComparisonBuffer::compareBySize() - //case-sensitive short name match is a database invariant! + //case-sensitive symlink name match is a database invariant! return dbLink.cmpVar == CompareVariant::content || dbLink.cmpVar == CompareVariant::size; } assert(false); @@ -273,18 +303,21 @@ bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, int fil //check whether database entry and current item match: *irrespective* of current comparison settings template <SelectSide side> inline -bool matchesDbEntry(const FolderPair& folder, const InSyncFolder* dbFolder) +CudAction compareDbEntry(const FolderPair& folder, const InSyncFolder* dbFolder, bool renamedOrMoved) { - const bool haveDbEntry = dbFolder && dbFolder->status != InSyncFolder::DIR_STATUS_STRAW_MAN; - return haveDbEntry == !folder.isEmpty<side>(); + if (folder.isEmpty<side>()) + return dbFolder ? (renamedOrMoved ? CudAction::update: CudAction::delete_) : CudAction::noChange; + else if (!dbFolder) + return (renamedOrMoved ? CudAction::update : CudAction::create); + + return CudAction::noChange; } inline bool stillInSync(const InSyncFolder& dbFolder) { - //case-sensitive short name match is a database invariant! - //InSyncFolder::DIR_STATUS_STRAW_MAN considered + //case-sensitive folder name match is a database invariant! return true; } @@ -293,7 +326,11 @@ bool stillInSync(const InSyncFolder& dbFolder) class DetectMovedFiles { public: - static void execute(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) { DetectMovedFiles(baseFolder, dbFolder); } + static void execute(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) + { + DetectMovedFiles(baseFolder, dbFolder); + baseFolder.removeDoubleEmpty(); //see findAndSetMovePair() + } private: DetectMovedFiles(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) : @@ -311,12 +348,14 @@ private: detectMovePairs(dbFolder); } - void recurse(ContainerObject& hierObj, const InSyncFolder* dbFolderL, const InSyncFolder* dbFolderR) + void recurse(ContainerObject& conObj, const InSyncFolder* dbFolderL, const InSyncFolder* dbFolderR) { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) { - const AFS::FingerPrint filePrintL = file.getFilePrint<SelectSide::left >(); - const AFS::FingerPrint filePrintR = file.getFilePrint<SelectSide::right>(); + file.setMoveRef(nullptr); //discard remnants from previous move detection and start fresh (e.g. consider manual folder rename) + + const AFS::FingerPrint filePrintL = file.isEmpty<SelectSide::left >() ? 0 : file.getFilePrint<SelectSide::left >(); + const AFS::FingerPrint filePrintR = file.isEmpty<SelectSide::right>() ? 0 : file.getFilePrint<SelectSide::right>(); if (filePrintL != 0) filesL_.push_back(&file); //collect *all* prints for uniqueness check! if (filePrintR != 0) filesR_.push_back(&file); // @@ -331,19 +370,19 @@ private: }; if (const CompareFileResult cat = file.getCategory(); - cat == FILE_LEFT_SIDE_ONLY) + cat == FILE_LEFT_ONLY) { if (const InSyncFile* dbEntry = getDbEntry(dbFolderL, file.getItemName<SelectSide::left>())) exLeftOnlyByPath_.emplace(dbEntry, &file); } - else if (cat == FILE_RIGHT_SIDE_ONLY) + else if (cat == FILE_RIGHT_ONLY) { if (const InSyncFile* dbEntry = getDbEntry(dbFolderR, file.getItemName<SelectSide::right>())) exRightOnlyByPath_.emplace(dbEntry, &file); } } - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) { auto getDbEntry = [](const InSyncFolder* dbFolder, const ZstringNorm& folderName) -> const InSyncFolder* { @@ -391,7 +430,7 @@ private: } //collect unique file prints for files existing on one side only: - constexpr CompareFileResult oneSideOnlyTag = side == SelectSide::left ? FILE_LEFT_SIDE_ONLY : FILE_RIGHT_SIDE_ONLY; + constexpr CompareFileResult oneSideOnlyTag = side == SelectSide::left ? FILE_LEFT_ONLY : FILE_RIGHT_ONLY; for (FilePair* file : files) if (file->getCategory() == oneSideOnlyTag) @@ -455,17 +494,42 @@ private: if (FilePair* fileRightOnly = getAssocFilePair<SelectSide::right>(dbFile)) if (sameSizeAndDate<SelectSide::right>(*fileRightOnly, dbFile)) { - assert((!fileLeftOnly ->getMoveRef() && - !fileRightOnly->getMoveRef()) || - (fileLeftOnly ->getMoveRef() == fileRightOnly->getId() && - fileRightOnly->getMoveRef() == fileLeftOnly ->getId())); - - if (fileLeftOnly ->getMoveRef() == nullptr && //needless check!? file prints are unique in this context! - fileRightOnly->getMoveRef() == nullptr) // + if (fileLeftOnly ->getMoveRef() == nullptr && //needless checks? (file prints are unique in this context) + fileRightOnly->getMoveRef() == nullptr && // + fileLeftOnly ->getCategory() == FILE_LEFT_ONLY && //is it possible we could get conflicting matches!? + fileRightOnly->getCategory() == FILE_RIGHT_ONLY) //=> likely 'yes', but only in obscure cases + //--------------- found a match --------------- { - fileLeftOnly ->setMoveRef(fileRightOnly->getId()); //found a pair, mark it! - fileRightOnly->setMoveRef(fileLeftOnly ->getId()); // + //move pair is just a 'rename' => combine: + if (&fileLeftOnly->parent() == &fileRightOnly->parent()) + { + fileLeftOnly->setSyncedTo<SelectSide::right>(fileLeftOnly->getFileSize<SelectSide::left>(), + fileRightOnly->getLastWriteTime<SelectSide::right>(), //lastWriteTimeTrg + fileLeftOnly ->getLastWriteTime<SelectSide::left >(), //lastWriteTimeSrc + + fileRightOnly->getFilePrint<SelectSide::right>(), //filePrintTrg + fileLeftOnly ->getFilePrint<SelectSide::left >(), //filePrintSrc + + fileRightOnly->isFollowedSymlink<SelectSide::right>(), //isSymlinkTrg + fileLeftOnly ->isFollowedSymlink<SelectSide::left >()); //isSymlinkSrc + + fileLeftOnly->setItemName<SelectSide::right>(fileRightOnly->getItemName<SelectSide::right>()); + + assert(fileLeftOnly->isActive() && fileRightOnly->isActive()); //can this fail? excluded files are not added during comparison... + if (fileLeftOnly->isActive() != fileRightOnly->isActive()) //just in case + fileLeftOnly->setActive(false); + + fileRightOnly->removeObject<SelectSide::right>(); //=> call ContainerObject::removeDoubleEmpty() later! + } + else //regular move pair: mark it! + { + fileLeftOnly ->setMoveRef(fileRightOnly->getId()); + fileRightOnly->setMoveRef(fileLeftOnly ->getId()); + } } + else + assert(fileLeftOnly ->getMoveRef() == fileRightOnly->getId() && + fileRightOnly->getMoveRef() == fileLeftOnly ->getId()); } } @@ -476,10 +540,10 @@ private: std::vector<FilePair*> filesL_; //collection of *all* file items (with non-null filePrint) std::vector<FilePair*> filesR_; // => detect duplicate file IDs - std::unordered_map<AFS::FingerPrint, FilePair*> exLeftOnlyById_; //MSVC: twice as fast as std::map for 1 million items! + std::unordered_map<AFS::FingerPrint, FilePair*> exLeftOnlyById_; std::unordered_map<AFS::FingerPrint, FilePair*> exRightOnlyById_; - std::unordered_map<const InSyncFile*, FilePair*> exLeftOnlyByPath_; //MSVC: only 4% faster than std::map for 1 million items! + std::unordered_map<const InSyncFile*, FilePair*> exLeftOnlyByPath_; std::unordered_map<const InSyncFile*, FilePair*> exRightOnlyByPath_; /* Detect Renamed Files: @@ -487,7 +551,7 @@ private: X -> |_| Create right |_| -> Y Delete right - resolve as: Rename Y to X on right + resolve as: Move/Rename Y to X on right Algorithm: ---------- @@ -507,13 +571,15 @@ private: //---------------------------------------------------------------------------------------------- -class SetSyncDirectionsTwoWay +class SetSyncDirViaChanges { public: - static void execute(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) { SetSyncDirectionsTwoWay(baseFolder, dbFolder); } + static void execute(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder, const DirectionByChange& dirs) + { SetSyncDirViaChanges(baseFolder, dbFolder, dirs); } private: - SetSyncDirectionsTwoWay(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder) : + SetSyncDirViaChanges(BaseFolderPair& baseFolder, const InSyncFolder& dbFolder, const DirectionByChange& dirs) : + dirs_(dirs), cmpVar_ (baseFolder.getCompVariant()), fileTimeTolerance_ (baseFolder.getFileTimeTolerance()), ignoreTimeShiftMinutes_(baseFolder.getIgnoredTimeShift()) @@ -524,13 +590,13 @@ private: recurse(baseFolder, &dbFolder); } - void recurse(ContainerObject& hierObj, const InSyncFolder* dbFolder) const + void recurse(ContainerObject& conObj, const InSyncFolder* dbFolder) const { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) processFile(file, dbFolder); - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) processSymlink(symlink, dbFolder); - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) processDir(folder, dbFolder); } @@ -539,11 +605,13 @@ private: const CompareFileResult cat = file.getCategory(); if (cat == FILE_EQUAL) return; + else if (cat == FILE_CONFLICT) //take over category conflict: allow *manual* resolution only! + return file.setSyncDirConflict(file.getCategoryCustomDescription()); //##################### schedule old temporary files for deletion #################### - if (cat == FILE_LEFT_SIDE_ONLY && endsWith(file.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) + if (cat == FILE_LEFT_ONLY && endsWith(file.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) return file.setSyncDir(SyncDirection::left); - else if (cat == FILE_RIGHT_SIDE_ONLY && endsWith(file.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) + else if (cat == FILE_RIGHT_ONLY && endsWith(file.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) return file.setSyncDir(SyncDirection::right); //#################################################################################### @@ -569,13 +637,22 @@ private: dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_)) //check *before* misleadingly reporting txtNoSideChanged_ return file.setSyncDirConflict(txtDbNotInSync_); - const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(file, dbEntryL, ignoreTimeShiftMinutes_); - const bool changeOnRight = !matchesDbEntry<SelectSide::right>(file, dbEntryR, ignoreTimeShiftMinutes_); + //consider renamed/moved files as "updated" with regards to "changes"-based sync settings: https://freefilesync.org/forum/viewtopic.php?t=10594 + const bool renamedOrMoved = cat == FILE_RENAMED || [&file] + { + if (const FileSystemObject::ObjectId moveFileRef = file.getMoveRef()) + if (auto refFile = dynamic_cast<const FilePair*>(FileSystemObject::retrieve(moveFileRef))) + { + if (refFile->getMoveRef() == file.getId()) //both ends should agree... + return true; + else assert(false); //...and why shouldn't they? + } + return false; + }(); + const CudAction changeL = compareDbEntry<SelectSide::left >(file, dbEntryL, ignoreTimeShiftMinutes_, renamedOrMoved); + const CudAction changeR = compareDbEntry<SelectSide::right>(file, dbEntryR, ignoreTimeShiftMinutes_, renamedOrMoved); - if (changeOnLeft == changeOnRight) - file.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_); - else - file.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left); + setSyncDirForChange(file, changeL, changeR); } void processSymlink(SymlinkPair& symlink, const InSyncFolder* dbFolder) const @@ -583,6 +660,8 @@ private: const CompareSymlinkResult cat = symlink.getLinkCategory(); if (cat == SYMLINK_EQUAL) return; + else if (cat == SYMLINK_CONFLICT) //take over category conflict: allow *manual* resolution only! + return symlink.setSyncDirConflict(symlink.getCategoryCustomDescription()); //try to find corresponding database entry auto getDbEntry = [dbFolder](const ZstringNorm& linkName) -> const InSyncSymlink* @@ -606,13 +685,11 @@ private: dbEntry && !stillInSync(*dbEntry, cmpVar_, fileTimeTolerance_, ignoreTimeShiftMinutes_)) return symlink.setSyncDirConflict(txtDbNotInSync_); - const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(symlink, dbEntryL, ignoreTimeShiftMinutes_); - const bool changeOnRight = !matchesDbEntry<SelectSide::right>(symlink, dbEntryR, ignoreTimeShiftMinutes_); + 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); - if (changeOnLeft == changeOnRight) - symlink.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_); - else - symlink.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left); + setSyncDirForChange(symlink, changeL, changeR); } void processDir(FolderPair& folder, const InSyncFolder* dbFolder) const @@ -620,9 +697,9 @@ private: const CompareDirResult cat = folder.getDirCategory(); //########### schedule abandoned temporary recycle bin directory for deletion ########## - if (cat == DIR_LEFT_SIDE_ONLY && endsWith(folder.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) + if (cat == DIR_LEFT_ONLY && endsWith(folder.getItemName<SelectSide::left>(), AFS::TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::left, folder); // - else if (cat == DIR_RIGHT_SIDE_ONLY && endsWith(folder.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) + else if (cat == DIR_RIGHT_ONLY && endsWith(folder.getItemName<SelectSide::right>(), AFS::TEMP_FILE_ENDING)) return setSyncDirectionRec(SyncDirection::right, folder); //don't recurse below! //####################################################################################### @@ -653,31 +730,75 @@ private: } const InSyncFolder* dbEntry = dbEntryL ? dbEntryL : dbEntryR; //exactly one side nullptr? => change in upper/lower case! - if (cat != DIR_EQUAL) + if (cat == DIR_EQUAL) + ; + else if (cat == DIR_CONFLICT) //take over category conflict: allow *manual* resolution only! + folder.setSyncDirConflict(folder.getCategoryCustomDescription()); + else { if (dbEntry && !stillInSync(*dbEntry)) folder.setSyncDirConflict(txtDbNotInSync_); else { - const bool changeOnLeft = !matchesDbEntry<SelectSide::left >(folder, dbEntryL); - const bool changeOnRight = !matchesDbEntry<SelectSide::right>(folder, dbEntryR); + const bool renamedOrMoved = cat == DIR_RENAMED; + const CudAction changeL = compareDbEntry<SelectSide::left >(folder, dbEntryL, renamedOrMoved); + const CudAction changeR = compareDbEntry<SelectSide::right>(folder, dbEntryR, renamedOrMoved); - if (changeOnLeft == changeOnRight) - folder.setSyncDirConflict(changeOnLeft ? txtBothSidesChanged_ : txtNoSideChanged_); - else - folder.setSyncDir(changeOnLeft ? SyncDirection::right : SyncDirection::left); + setSyncDirForChange(folder, changeL, changeR); } } recurse(folder, dbEntry); } + template <SelectSide side> + SyncDirection getSyncDirForChange(CudAction change) const + { + const auto& changedirs = selectParam<side>(dirs_.left, dirs_.right); + switch (change) + { + //*INDENT-OFF* + case CudAction::noChange: return SyncDirection::none; + case CudAction::create: return changedirs.create; + case CudAction::update: return changedirs.update; + case CudAction::delete_: return changedirs.delete_; + //*INDENT-ON* + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); + } + + void setSyncDirForChange(FileSystemObject& fsObj, CudAction changeL, CudAction changeR) const + { + const SyncDirection dirL = getSyncDirForChange<SelectSide::left >(changeL); + const SyncDirection dirR = getSyncDirForChange<SelectSide::right>(changeR); + if (changeL != CudAction::noChange) + { + if (changeR != CudAction::noChange) //both sides changed + { + if (dirL == dirR) //but luckily agree on direction + fsObj.setSyncDir(dirL); + else + fsObj.setSyncDirConflict(txtBothSidesChanged_); + } + else //change on left + fsObj.setSyncDir(dirL); + } + else + { + if (changeR != CudAction::noChange) //change on right + fsObj.setSyncDir(dirR); + else //no change on either side + fsObj.setSyncDirConflict(txtNoSideChanged_); //obscure, but possible if user widens "fileTimeTolerance" + } + } + //need ref-counted strings! see FileSystemObject::syncDirectionConflict_ const Zstringc txtBothSidesChanged_ = utfTo<Zstringc>(_("Both sides have changed since last synchronization.")); const Zstringc txtNoSideChanged_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("No change since last synchronization.")); const Zstringc txtDbNotInSync_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("The database entry is not in sync, considering current settings.")); const Zstringc txtDbAmbiguous_ = utfTo<Zstringc>(_("Cannot determine sync-direction:") + L'\n' + TAB_SPACE + _("The database entry is ambiguous.")); + const DirectionByChange dirs_; const CompareVariant cmpVar_; const int fileTimeTolerance_; const std::vector<unsigned int> ignoreTimeShiftMinutes_; @@ -729,39 +850,38 @@ void fff::redetermineSyncDirection(const std::vector<std::pair<BaseFolderPair*, for (const auto& [baseFolder, dirCfg] : directCfgs) if (!allEqualPairs.contains(baseFolder)) { - auto it = lastSyncStates.find(baseFolder); - const InSyncFolder* lastSyncState = it != lastSyncStates.end() ? &it->second.ref() : nullptr; - - //set sync directions - if (dirCfg.var == SyncVariant::twoWay) + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs)) + SetSyncDirViaDifferences::execute(*diffDirs, *baseFolder); + else { - if (lastSyncState) - SetSyncDirectionsTwoWay::execute(*baseFolder, *lastSyncState); - else //default fallback + const DirectionByChange& changeDirs = std::get<DirectionByChange>(dirCfg.dirs); + + auto it = lastSyncStates.find(baseFolder); + if (const InSyncFolder* lastSyncState = it != lastSyncStates.end() ? &it->second.ref() : nullptr) { - std::wstring msg = _("Setting directions for first synchronization: Old files will be overwritten with newer files."); - if (directCfgs.size() > 1) - msg += L'\n' + AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::left >()) + L' ' + getVariantNameWithSymbol(dirCfg.var) + L' ' + - AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::right>()); + //detect moved files (*before* setting sync directions: might combine moved files into single file pairs, wich changes category!) + DetectMovedFiles::execute(*baseFolder, *lastSyncState); + SetSyncDirViaChanges::execute(*baseFolder, *lastSyncState, changeDirs); + } + else //fallback: + { + std::wstring msg = _("Database file is not available: Setting default directions for synchronization."); + if (directCfgs.size() > 1) + msg += SPACED_DASH + getShortDisplayNameForFolderPair(baseFolder->getAbstractPath<SelectSide::left >(), + baseFolder->getAbstractPath<SelectSide::right>()); try { callback.logMessage(msg, PhaseCallback::MsgType::warning); /*throw X*/} catch (...) {}; - SetSyncDirectionByConfig::execute(getTwoWayUpdateSet(), *baseFolder); + SetSyncDirViaDifferences::execute(getDiffDirDefault(changeDirs), *baseFolder); } } - else - SetSyncDirectionByConfig::execute(extractDirections(dirCfg), *baseFolder); - - //detect renamed files - if (lastSyncState) - DetectMovedFiles::execute(*baseFolder, *lastSyncState); } //*INDENT-ON* ); std::vector<const BaseFolderPair*> baseFoldersForDbLoad; for (const auto& [baseFolder, dirCfg] : directCfgs) - if (dirCfg.var == SyncVariant::twoWay || detectMovedFilesEnabled(dirCfg)) + if (std::get_if<DirectionByChange>(&dirCfg.dirs)) { if (allItemsCategoryEqual(*baseFolder)) //nothing to do: don't even try to open DB files allEqualPairs.insert(baseFolder); @@ -837,45 +957,45 @@ struct Eval<STRATEGY_AND> template <FilterStrategy strategy> -class ApplyHardFilter +class ApplyPathFilter { public: - static void execute(ContainerObject& hierObj, const PathFilter& filterProcIn) { ApplyHardFilter(hierObj, filterProcIn); } + static void execute(ContainerObject& conObj, const PathFilter& filter) { ApplyPathFilter(conObj, filter); } private: - ApplyHardFilter(ContainerObject& hierObj, const PathFilter& filterProcIn) : filterProc(filterProcIn) { recurse(hierObj); } + ApplyPathFilter(ContainerObject& conObj, const PathFilter& filter) : filter_(filter) { recurse(conObj); } - void recurse(ContainerObject& hierObj) const + void recurse(ContainerObject& conObj) const { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) processFile(file); - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) processLink(symlink); - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) processDir(folder); } void processFile(FilePair& file) const { if (Eval<strategy>::process(file)) - file.setActive(filterProc.passFileFilter(file.getRelativePathAny())); + file.setActive(file.passFileFilter(filter_)); } void processLink(SymlinkPair& symlink) const { if (Eval<strategy>::process(symlink)) - symlink.setActive(filterProc.passFileFilter(symlink.getRelativePathAny())); + symlink.setActive(symlink.passFileFilter(filter_)); } void processDir(FolderPair& folder) const { bool childItemMightMatch = true; - const bool filterPassed = filterProc.passDirFilter(folder.getRelativePathAny(), &childItemMightMatch); + const bool filterPassed = folder.passDirFilter(filter_, &childItemMightMatch); if (Eval<strategy>::process(folder)) folder.setActive(filterPassed); - if (!childItemMightMatch) //use same logic like directory traversing here: evaluate filter in subdirs only if objects could match + if (!childItemMightMatch) //use same logic like directory traversing: evaluate filter in subdirs only if objects *could* match { //exclude all files dirs in subfolders => incompatible with STRATEGY_OR! auto onFsItem = [](FileSystemObject& fsObj) { fsObj.setActive(false); }; @@ -886,7 +1006,7 @@ private: recurse(folder); } - const PathFilter& filterProc; + const PathFilter& filter_; }; @@ -894,18 +1014,18 @@ template <FilterStrategy strategy> class ApplySoftFilter //falsify only! -> can run directly after "hard/base filter" { public: - static void execute(ContainerObject& hierObj, const SoftFilter& timeSizeFilter) { ApplySoftFilter(hierObj, timeSizeFilter); } + static void execute(ContainerObject& conObj, const SoftFilter& timeSizeFilter) { ApplySoftFilter(conObj, timeSizeFilter); } private: - ApplySoftFilter(ContainerObject& hierObj, const SoftFilter& timeSizeFilter) : timeSizeFilter_(timeSizeFilter) { recurse(hierObj); } + ApplySoftFilter(ContainerObject& conObj, const SoftFilter& timeSizeFilter) : timeSizeFilter_(timeSizeFilter) { recurse(conObj); } - void recurse(fff::ContainerObject& hierObj) const + void recurse(fff::ContainerObject& conObj) const { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) processFile(file); - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) processLink(symlink); - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) processDir(folder); } @@ -982,7 +1102,7 @@ private: void fff::addHardFiltering(BaseFolderPair& baseFolder, const Zstring& excludeFilter) { - ApplyHardFilter<STRATEGY_AND>::execute(baseFolder, NameFilter(FilterConfig().includeFilter, excludeFilter)); + ApplyPathFilter<STRATEGY_AND>::execute(baseFolder, NameFilter(FilterConfig().includeFilter, excludeFilter)); } @@ -1014,7 +1134,7 @@ void fff::applyFiltering(FolderComparison& folderCmp, const MainConfiguration& m const NormalizedFilter normFilter = normalizeFilters(mainCfg.globalFilter, it->localFilter); //"set" hard filter - ApplyHardFilter<STRATEGY_SET>::execute(baseFolder, normFilter.nameFilter.ref()); + ApplyPathFilter<STRATEGY_SET>::execute(baseFolder, normFilter.nameFilter.ref()); //"and" soft filter addSoftFiltering(baseFolder, normFilter.timeSizeFilter); @@ -1103,31 +1223,6 @@ std::optional<PathDependency> fff::getPathDependency(const AbstractPath& folderP //############################################################################################################ -std::pair<std::wstring, int> fff::getSelectedItemsAsString(std::span<const FileSystemObject* const> selectionLeft, - std::span<const FileSystemObject* const> selectionRight) -{ - //don't use wxString! its dumb linear allocation strategy brings perf down to a crawl! - std::wstring fileList; // - int totalDelCount = 0; - - for (const FileSystemObject* fsObj : selectionLeft) - if (!fsObj->isEmpty<SelectSide::left>()) - { - fileList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::left>()) + L'\n'; - ++totalDelCount; - } - - for (const FileSystemObject* fsObj : selectionRight) - if (!fsObj->isEmpty<SelectSide::right>()) - { - fileList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::right>()) + L'\n'; - ++totalDelCount; - } - - return {fileList, totalDelCount}; -} - - namespace { template <SelectSide side> @@ -1254,38 +1349,34 @@ void copyToAlternateFolderFrom(const std::vector<const FileSystemObject*>& rowsT } -void fff::copyToAlternateFolder(std::span<const FileSystemObject* const> rowsToCopyOnLeft, - std::span<const FileSystemObject* const> rowsToCopyOnRight, +void fff::copyToAlternateFolder(const std::vector<const FileSystemObject*>& selectionL, + const std::vector<const FileSystemObject*>& selectionR, const Zstring& targetFolderPathPhrase, - bool keepRelPaths, - bool overwriteIfExists, + bool keepRelPaths, bool overwriteIfExists, WarningDialogs& warnings, ProcessCallback& callback /*throw X*/) //throw X { - std::vector<const FileSystemObject*> itemSelectionLeft (rowsToCopyOnLeft .begin(), rowsToCopyOnLeft .end()); - std::vector<const FileSystemObject*> itemSelectionRight(rowsToCopyOnRight.begin(), rowsToCopyOnRight.end()); - std::erase_if(itemSelectionLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }); //needed for correct stats! - std::erase_if(itemSelectionRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); }); // + assert(std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); })); + assert(std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); })); - const int itemTotal = static_cast<int>(itemSelectionLeft.size() + itemSelectionRight.size()); + const int itemTotal = static_cast<int>(selectionL.size() + selectionR.size()); int64_t bytesTotal = 0; - for (const FileSystemObject* fsObj : itemSelectionLeft) + for (const FileSystemObject* fsObj : selectionL) visitFSObject(*fsObj, [](const FolderPair& folder) {}, [&](const FilePair& file) { bytesTotal += static_cast<int64_t>(file.getFileSize<SelectSide::left>()); }, [](const SymlinkPair& symlink) {}); - for (const FileSystemObject* fsObj : itemSelectionRight) + for (const FileSystemObject* fsObj : selectionR) visitFSObject(*fsObj, [](const FolderPair& folder) {}, [&](const FilePair& file) { bytesTotal += static_cast<int64_t>(file.getFileSize<SelectSide::right>()); }, [](const SymlinkPair& symlink) {}); callback.initNewPhase(itemTotal, bytesTotal, ProcessPhase::none); //throw X - //------------------------------------------------------------------------------ const AbstractPath targetFolderPath = createAbstractPath(targetFolderPathPhrase); - copyToAlternateFolderFrom<SelectSide::left >(itemSelectionLeft, targetFolderPath, keepRelPaths, overwriteIfExists, callback); - copyToAlternateFolderFrom<SelectSide::right>(itemSelectionRight, targetFolderPath, keepRelPaths, overwriteIfExists, callback); + copyToAlternateFolderFrom<SelectSide::left >(selectionL, targetFolderPath, keepRelPaths, overwriteIfExists, callback); + copyToAlternateFolderFrom<SelectSide::right>(selectionR, targetFolderPath, keepRelPaths, overwriteIfExists, callback); } //############################################################################################################ @@ -1293,11 +1384,12 @@ void fff::copyToAlternateFolder(std::span<const FileSystemObject* const> rowsToC namespace { template <SelectSide side> -void deleteFromGridAndHdOneSide(std::vector<FileSystemObject*>& rowsToDelete, - bool moveToRecycler, - bool& recyclerMissingReportOnce, - bool& warnRecyclerMissing, //WarningDialogs::warnRecyclerMissing - PhaseCallback& callback /*throw X*/) //throw X +void deleteFilesOneSide(const std::vector<FileSystemObject*>& rowsToDelete, + bool moveToRecycler, + bool& recyclerMissingReportOnce, + bool& warnRecyclerMissing, //WarningDialogs::warnRecyclerMissing + const std::unordered_map<const BaseFolderPair*, SyncDirectionConfig>& baseFolderCfgs, + PhaseCallback& callback /*throw X*/) //throw X { const std::wstring txtDelFilePermanent_ = _("Deleting file %x"); const std::wstring txtDelFileRecycler_ = _("Moving file %x to the recycle bin"); @@ -1431,86 +1523,197 @@ void deleteFromGridAndHdOneSide(std::vector<FileSystemObject*>& rowsToDelete, { removeSymlink(symlink.getAbstractPath<side>(), statReporter); //throw FileError, X }); + //------- no-throw from here on ------- + const CompareFileResult catOld = fsObj->getCategory(); fsObj->removeObject<side>(); //if directory: removes recursively! + + //update sync direction: don't call redetermineSyncDirection() because user may have manually changed directions + if (catOld == CompareFileResult::FILE_EQUAL) + { + const SyncDirection newDir = [&] + { + const SyncDirectionConfig& dirCfg = baseFolderCfgs.find(&fsObj->base())->second; //not found? let it crash! + + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs)) + return side == SelectSide::left ? diffDirs->rightOnly : diffDirs->leftOnly; + else + { + const DirectionByChange& changeDirs = std::get<DirectionByChange>(dirCfg.dirs); + return side == SelectSide::left ? changeDirs.left.delete_ : changeDirs.right.delete_; + } + }(); + + setSyncDirectionRec(newDir, *fsObj); //set new direction (recursively) + } + //else: keep old syncDir_ } }, callback); //throw X } } -void fff::deleteFromGridAndHD(const std::vector<FileSystemObject*>& rowsToDeleteOnLeft, //refresh GUI grid after deletion to remove invalid rows - const std::vector<FileSystemObject*>& rowsToDeleteOnRight, //all pointers need to be bound! - const std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>>& directCfgs, //attention: rows will be physically deleted! - bool moveToRecycler, - bool& warnRecyclerMissing, - ProcessCallback& callback /*throw X*/) //throw X +void fff::deleteFiles(const std::vector<FileSystemObject*>& selectionL, + const std::vector<FileSystemObject*>& selectionR, + const std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>>& directCfgs, + bool moveToRecycler, + bool& warnRecyclerMissing, + ProcessCallback& callback /*throw X*/) //throw X { - if (directCfgs.empty()) - return; + assert(std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); })); + assert(std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); })); + + const int itemCount = static_cast<int>(selectionL.size() + selectionR.size()); + callback.initNewPhase(itemCount, 0, ProcessPhase::none); //throw X + //------------------------------------------------------------------------------ + + ZEN_ON_SCOPE_EXIT + ( + //*INDENT-OFF* + for (const auto& [baseFolder, dirCfg] : directCfgs) + baseFolder->removeDoubleEmpty(); + //*INDENT-ON* + ); //build up mapping from base directory to corresponding direction config std::unordered_map<const BaseFolderPair*, SyncDirectionConfig> baseFolderCfgs; for (const auto& [baseFolder, dirCfg] : directCfgs) baseFolderCfgs[baseFolder] = dirCfg; - std::vector<FileSystemObject*> deleteLeft = rowsToDeleteOnLeft; - std::vector<FileSystemObject*> deleteRight = rowsToDeleteOnRight; + bool recyclerMissingReportOnce = false; + deleteFilesOneSide<SelectSide::left >(selectionL, moveToRecycler, recyclerMissingReportOnce, warnRecyclerMissing, baseFolderCfgs, callback); //throw X + deleteFilesOneSide<SelectSide::right>(selectionR, moveToRecycler, recyclerMissingReportOnce, warnRecyclerMissing, baseFolderCfgs, callback); // +} - std::erase_if(deleteLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }); //needed? - std::erase_if(deleteRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); }); //yes, for correct stats: +//############################################################################################################ - const int itemCount = static_cast<int>(deleteLeft.size() + deleteRight.size()); - callback.initNewPhase(itemCount, 0, ProcessPhase::none); //throw X +namespace +{ +template <SelectSide side> +void renameItemsOneSide(const std::vector<FileSystemObject*>& selection, + const std::span<const Zstring> newNames, + const std::unordered_map<const BaseFolderPair*, SyncDirectionConfig>& baseFolderCfgs, + PhaseCallback& callback /*throw X*/) //throw X +{ + assert(selection.size() == newNames.size()); - //------------------------------------------------------------------------------ + const std::wstring txtRenamingFileXtoY_ {_("Renaming file %x to %y")}; + const std::wstring txtRenamingLinkXtoY_ {_("Renaming symbolic link %x to %y")}; + const std::wstring txtRenamingFolderXtoY_{_("Renaming folder %x to %y")}; - //ensure cleanup: redetermination of sync-directions and removal of invalid rows - auto updateDirection = [&] + for (size_t i = 0; i < selection.size(); ++i) + tryReportingError([&] { - //update sync direction: we cannot do a full redetermination since the user may have manual changes applied already - std::vector<FileSystemObject*> rowsToDelete; - append(rowsToDelete, deleteLeft); - append(rowsToDelete, deleteRight); - removeDuplicates(rowsToDelete); + FileSystemObject& fsObj = *selection[i]; + const Zstring& newName = newNames[i]; + + assert(!fsObj.isEmpty<side>()); - for (auto it = rowsToDelete.begin(); it != rowsToDelete.end(); ++it) + auto haveNameClash = [newNameNorm = getUnicodeNormalForm(newName)](const FileSystemObject& fsObj2) { - FileSystemObject& fsObj = **it; //all pointers are required(!) to be bound + return !fsObj2.isEmpty<side>() && getUnicodeNormalForm(fsObj2.getItemName<side>()) == newNameNorm; + }; - if (fsObj.isEmpty<SelectSide::left>() != fsObj.isEmpty<SelectSide::right>()) //make sure objects exists on one side only - { - auto cfgIter = baseFolderCfgs.find(&fsObj.base()); - assert(cfgIter != baseFolderCfgs.end()); - if (cfgIter != baseFolderCfgs.end()) - { - SyncDirection newDir = SyncDirection::none; + const bool nameAlreadyExisting = [&] + { + for (const FilePair& file : fsObj.parent().refSubFiles()) + if (haveNameClash(file)) + return true; - if (cfgIter->second.var == SyncVariant::twoWay) - newDir = fsObj.isEmpty<SelectSide::left>() ? SyncDirection::right : SyncDirection::left; - else - { - const DirectionSet& dirCfg = extractDirections(cfgIter->second); - newDir = fsObj.isEmpty<SelectSide::left>() ? dirCfg.exRightSideOnly : dirCfg.exLeftSideOnly; - } + for (const SymlinkPair& symlink : fsObj.parent().refSubLinks()) + if (haveNameClash(symlink)) + return true; + + for (const FolderPair& folder : fsObj.parent().refSubFolders()) + if (haveNameClash(folder)) + return true; + return false; + }(); + + //--------------------------------------------------------------- + ItemStatReporter statReporter(1, 0, callback); + + const std::wstring* txtRenamingXtoY_ = nullptr; + visitFSObject(fsObj, + [&](const FolderPair& folder) { txtRenamingXtoY_ = &txtRenamingFolderXtoY_; }, + [&](const FilePair& file) { txtRenamingXtoY_ = &txtRenamingFileXtoY_; }, + [&](const SymlinkPair& symlink) { txtRenamingXtoY_ = &txtRenamingLinkXtoY_; }); + + reportInfo(replaceCpy(replaceCpy(*txtRenamingXtoY_, L"%x", fmtPath(AFS::getDisplayPath(fsObj.getAbstractPath<side>()))), + L"%y", fmtPath(newName)), statReporter); //throw X + + if (haveNameClash(fsObj)) + return assert(false); //theoretically possible, but practically showRenameDialog() won't return until there is an actual name change + + if (nameAlreadyExisting) //avoid inconsistent file model: expecting moveAndRenameItem() to fail (ERROR_ALREADY_EXISTS) is not good enough + return callback.reportFatalError(replaceCpy(replaceCpy(_("Cannot rename %x to %y."), + L"%x", fmtPath(AFS::getDisplayPath(fsObj.getAbstractPath<side>()))), + L"%y", fmtPath(newName)) + L"\n\n" + + replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(newName))); //throw X + + AFS::moveAndRenameItem(fsObj.getAbstractPath<side>(), + AFS::appendRelPath(fsObj.parent().getAbstractPath<side>(), newName)); //throw FileError, (ErrorMoveUnsupported) + //------- no-throw from here on ------- + statReporter.reportDelta(1, 0); - setSyncDirectionRec(newDir, fsObj); //set new direction (recursively) + const CompareFileResult catOld = fsObj.getCategory(); + + fsObj.setItemName<side>(newName); + + warn_static("TODO: some users want to manually fix renamed folders/files: combine them here, don't require a re-compare!") + + //update sync direction: don't call redetermineSyncDirection() because user may have manually changed directions + if (catOld == CompareFileResult::FILE_EQUAL) + { + const SyncDirection newDir = [&] + { + const SyncDirectionConfig& dirCfg = baseFolderCfgs.find(&fsObj.base())->second; //not found? let it crash! + + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs)) + return side == SelectSide::left ? diffDirs->leftNewer : diffDirs->rightNewer; + else + { + const DirectionByChange& changeDirs = std::get<DirectionByChange>(dirCfg.dirs); + return side == SelectSide::left ? changeDirs.left.update : changeDirs.right.update; } - } + }(); + + fsObj.setSyncDir(newDir); //folder? => do not recurse! } + //else: keep old syncDir_ + else if (fsObj.getCategory() == FILE_EQUAL) //edge-case, but possible + fsObj.setSyncDir(SyncDirection::none); //shouldn't matter, but avoids hitting some asserts - //last step: cleanup empty rows: this one invalidates all pointers! - for (const auto& [baseFolder, dirCfg] : directCfgs) - BaseFolderPair::removeEmpty(*baseFolder); - }; - ZEN_ON_SCOPE_EXIT(updateDirection()); //MSVC: assert is a macro and it doesn't play nice with ZEN_ON_SCOPE_EXIT, surprise... wasn't there something about macros being "evil"? + }, callback); //throw X +} +} - bool recyclerMissingReportOnce = false; - deleteFromGridAndHdOneSide<SelectSide::left >(deleteLeft, moveToRecycler, recyclerMissingReportOnce, warnRecyclerMissing, callback); //throw X - deleteFromGridAndHdOneSide<SelectSide::right>(deleteRight, moveToRecycler, recyclerMissingReportOnce, warnRecyclerMissing, callback); // + +void fff::renameItems(const std::vector<FileSystemObject*>& selectionL, + const std::span<const Zstring> newNamesL, + const std::vector<FileSystemObject*>& selectionR, + const std::span<const Zstring> newNamesR, + const std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>>& directCfgs, + ProcessCallback& callback /*throw X*/) //throw X +{ + assert(std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); })); + assert(std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); })); + + const int itemCount = static_cast<int>(selectionL.size() + selectionR.size()); + callback.initNewPhase(itemCount, 0, ProcessPhase::none); //throw X + //------------------------------------------------------------------------------ + + //build up mapping from base directory to corresponding direction config + std::unordered_map<const BaseFolderPair*, SyncDirectionConfig> baseFolderCfgs; + for (const auto& [baseFolder, dirCfg] : directCfgs) + baseFolderCfgs[baseFolder] = dirCfg; + + renameItemsOneSide<SelectSide::left >(selectionL, newNamesL, baseFolderCfgs, callback); //throw X + renameItemsOneSide<SelectSide::right>(selectionR, newNamesR, baseFolderCfgs, callback); // } //############################################################################################################ + void fff::deleteListOfFiles(const std::vector<Zstring>& filesToDeletePaths, std::vector<Zstring>& deletedPaths, bool moveToRecycler, diff --git a/FreeFileSync/Source/base/algorithm.h b/FreeFileSync/Source/base/algorithm.h index 8cc8a87b..11aaad95 100644 --- a/FreeFileSync/Source/base/algorithm.h +++ b/FreeFileSync/Source/base/algorithm.h @@ -47,27 +47,28 @@ struct PathDependency std::optional<PathDependency> getPathDependency(const AbstractPath& folderPathL, const PathFilter& filterL, const AbstractPath& folderPathR, const PathFilter& filterR); -std::pair<std::wstring, int> getSelectedItemsAsString( //returns string with item names and total count of selected(!) items, NOT total files/dirs! - std::span<const FileSystemObject* const> selectionLeft, //all pointers need to be bound! - std::span<const FileSystemObject* const> selectionRight); // - //manual copy to alternate folder: -void copyToAlternateFolder(std::span<const FileSystemObject* const> rowsToCopyOnLeft, //all pointers need to be bound! - std::span<const FileSystemObject* const> rowsToCopyOnRight, // +void copyToAlternateFolder(const std::vector<const FileSystemObject*>& selectionL, //all pointers need to be bound and !isEmpty<side>! + const std::vector<const FileSystemObject*>& selectionR, // const Zstring& targetFolderPathPhrase, - bool keepRelPaths, - bool overwriteIfExists, + bool keepRelPaths, bool overwriteIfExists, WarningDialogs& warnings, ProcessCallback& callback /*throw X*/); //throw X //manual deletion of files on main grid -void deleteFromGridAndHD(const std::vector<FileSystemObject*>& rowsToDeleteOnLeft, //refresh GUI grid after deletion to remove invalid rows - const std::vector<FileSystemObject*>& rowsToDeleteOnRight, //all pointers need to be bound! - const std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>>& directCfgs, //attention: rows will be physically deleted! - bool moveToRecycler, - //global warnings: - bool& warnRecyclerMissing, - ProcessCallback& callback /*throw X*/); //throw X +void deleteFiles(const std::vector<FileSystemObject*>& selectionL, //all pointers need to be bound and !isEmpty<side>! + const std::vector<FileSystemObject*>& selectionR, //refresh GUI grid after deletion to remove invalid rows + const std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>>& directCfgs, //attention: rows will be physically deleted! + bool moveToRecycler, + bool& warnRecyclerMissing, + ProcessCallback& callback /*throw X*/); //throw X + +void renameItems(const std::vector<FileSystemObject*>& selectionL, //all pointers need to be bound and !isEmpty<side>! + const std::span<const Zstring> newNamesL, + const std::vector<FileSystemObject*>& selectionR, //refresh GUI grid after deletion to remove invalid rows + const std::span<const Zstring> newNamesR, + const std::vector<std::pair<BaseFolderPair*, SyncDirectionConfig>>& directCfgs, + ProcessCallback& callback /*throw X*/); //throw X void deleteListOfFiles(const std::vector<Zstring>& filesToDeletePaths, std::vector<Zstring>& deletedPaths, diff --git a/FreeFileSync/Source/base/cmp_filetime.h b/FreeFileSync/Source/base/cmp_filetime.h index 04633df7..c148c6b0 100644 --- a/FreeFileSync/Source/base/cmp_filetime.h +++ b/FreeFileSync/Source/base/cmp_filetime.h @@ -66,12 +66,14 @@ enum class TimeResult }; +//number of seconds since Jan 1st 1970 + 1 year (needn't be too precise) +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) { - //number of seconds since Jan 1st 1970 + 1 year (needn't be too precise) - static const time_t oneYearFromNow = std::time(nullptr) + 365 * 24 * 3600; - + assert(oneYearFromNow != 0); if (sameFileTime(lhs, rhs, tolerance, ignoreTimeShiftMinutes)) //last write time may differ by up to 2 seconds (NTFS vs FAT32) return TimeResult::equal; diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 8c1c9544..5b764ca2 100644 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -237,9 +237,10 @@ FolderComparison ComparisonBuffer::execute(const std::vector<std::pair<ResolvedF { //+ only traverse *existing* folders if (getBaseFolderStatus(folderPair.folderPathLeft) == BaseFolderStatus::existing) - foldersToRead.emplace(DirectoryKey({folderPair.folderPathLeft, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})); + foldersToRead.emplace(DirectoryKey{folderPair.folderPathLeft, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); if (getBaseFolderStatus(folderPair.folderPathRight) == BaseFolderStatus::existing) - foldersToRead.emplace(DirectoryKey({folderPair.folderPathRight, fpCfg.filter.nameFilter, fpCfg.handleSymlinks})); + foldersToRead.emplace(DirectoryKey{folderPair.folderPathRight, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); + warn_static("remove DirectoryKey{} prefix once mac supports it") } //------------------------------------------------------------------ @@ -301,8 +302,8 @@ FolderComparison ComparisonBuffer::execute(const std::vector<std::pair<ResolvedF //--------------------assemble conflict descriptions--------------------------- -//const wchar_t arrowLeft [] = L"\u2190"; -//const wchar_t arrowRight[] = L"\u2192"; unicode arrows -> too small +//const wchar_t arrowLeft [] = L"\u2190"; unicode arrows -> too small +//const wchar_t arrowRight[] = L"\u2192"; const wchar_t arrowLeft [] = L"<-"; const wchar_t arrowRight[] = L"->"; @@ -313,15 +314,15 @@ template <SelectSide side, class FileOrLinkPair> inline Zstringc getConflictInvalidDate(const FileOrLinkPair& file) { return utfTo<Zstringc>(replaceCpy(_("File %x has an invalid date."), L"%x", fmtPath(AFS::getDisplayPath(file.template getAbstractPath<side>()))) + L'\n' + - TAB_SPACE + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<side>())); + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<side>())); } Zstringc getConflictSameDateDiffSize(const FilePair& file) { return utfTo<Zstringc>(_("Files have the same date but a different size.") + L'\n' + - TAB_SPACE + arrowLeft + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::left >()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::left>()) + L'\n' + - TAB_SPACE + arrowRight + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::right>()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::right>())); + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::left >()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::left >()) + L' ' + arrowLeft + L'\n' + + _("Date:") + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::right>()) + TAB_SPACE + _("Size:") + L' ' + formatNumber(file.getFileSize<SelectSide::right>()) + L' ' + arrowRight); } @@ -331,25 +332,6 @@ Zstringc getConflictSkippedBinaryComparison() } -Zstringc getDescrDiffMetaShortnameCase(const FileSystemObject& fsObj) -{ - return utfTo<Zstringc>(_("Items differ in attributes only") + L'\n' + - TAB_SPACE + arrowLeft + L' ' + fmtPath(fsObj.getItemName<SelectSide::left >()) + L'\n' + - TAB_SPACE + arrowRight + L' ' + fmtPath(fsObj.getItemName<SelectSide::right>())); -} - - -#if 0 -template <class FileOrLinkPair> -Zstringc getDescrDiffMetaData(const FileOrLinkPair& file) -{ - return utfTo<Zstringc>(_("Items differ in attributes only") + L'\n' + - TAB_SPACE + arrowLeft + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<SelectSide::left >()) + L'\n' + - TAB_SPACE + arrowRight + L' ' + _("Date:") + L' ' + formatUtcToLocalTime(file.template getLastWriteTime<SelectSide::right>())); -} -#endif - - Zstringc getConflictAmbiguousItemName(const Zstring& itemName) { return utfTo<Zstringc>(replaceCpy(_("The name %x is used by more than one item in the folder."), L"%x", fmtPath(itemName))); @@ -364,31 +346,23 @@ void categorizeSymlinkByTime(SymlinkPair& symlink) symlink.getLastWriteTime<SelectSide::right>(), symlink.base().getFileTimeTolerance(), symlink.base().getIgnoredTimeShift())) { case TimeResult::equal: - //Caveat: - //1. SYMLINK_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp - //2. harmonize with "bool stillInSync()" in algorithm.cpp - - if (getUnicodeNormalForm(symlink.getItemName<SelectSide::left >()) == - getUnicodeNormalForm(symlink.getItemName<SelectSide::right>())) - symlink.setCategory<FILE_EQUAL>(); - else - symlink.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(symlink)); + symlink.setContentCategory(FileContentCategory::equal); break; case TimeResult::leftNewer: - symlink.setCategory<FILE_LEFT_NEWER>(); + symlink.setContentCategory(FileContentCategory::leftNewer); break; case TimeResult::rightNewer: - symlink.setCategory<FILE_RIGHT_NEWER>(); + symlink.setContentCategory(FileContentCategory::rightNewer); break; case TimeResult::leftInvalid: - symlink.setCategoryConflict(getConflictInvalidDate<SelectSide::left>(symlink)); + symlink.setCategoryInvalidTime(getConflictInvalidDate<SelectSide::left>(symlink)); break; case TimeResult::rightInvalid: - symlink.setCategoryConflict(getConflictInvalidDate<SelectSide::right>(symlink)); + symlink.setCategoryInvalidTime(getConflictInvalidDate<SelectSide::right>(symlink)); break; } } @@ -412,36 +386,26 @@ SharedRef<BaseFolderPair> ComparisonBuffer::compareByTimeSize(const ResolvedFold file->getLastWriteTime<SelectSide::right>(), fileTimeTolerance_, fpConfig.ignoreTimeShiftMinutes)) { case TimeResult::equal: - //Caveat: - //1. FILE_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp - //2. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile - //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h if (file->getFileSize<SelectSide::left>() == file->getFileSize<SelectSide::right>()) - { - if (getUnicodeNormalForm(file->getItemName<SelectSide::left >()) == - getUnicodeNormalForm(file->getItemName<SelectSide::right>())) - file->setCategory<FILE_EQUAL>(); - else - file->setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(*file)); - } + file->setContentCategory(FileContentCategory::equal); else - file->setCategoryConflict(getConflictSameDateDiffSize(*file)); //same date, different filesize + file->setCategoryInvalidTime(getConflictSameDateDiffSize(*file)); break; case TimeResult::leftNewer: - file->setCategory<FILE_LEFT_NEWER>(); + file->setContentCategory(FileContentCategory::leftNewer); break; case TimeResult::rightNewer: - file->setCategory<FILE_RIGHT_NEWER>(); + file->setContentCategory(FileContentCategory::rightNewer); break; case TimeResult::leftInvalid: - file->setCategoryConflict(getConflictInvalidDate<SelectSide::left>(*file)); + file->setCategoryInvalidTime(getConflictInvalidDate<SelectSide::left>(*file)); break; case TimeResult::rightInvalid: - file->setCategoryConflict(getConflictInvalidDate<SelectSide::right>(*file)); + file->setCategoryInvalidTime(getConflictInvalidDate<SelectSide::right>(*file)); break; } } @@ -467,25 +431,7 @@ void categorizeSymlinkByContent(SymlinkPair& symlink, PhaseCallback& callback) if (!errMsg.empty()) symlink.setCategoryConflict(utfTo<Zstringc>(errMsg)); else - { - if (equalContent) - { - //Caveat: - //1. SYMLINK_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp - //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h - - if (getUnicodeNormalForm(symlink.getItemName<SelectSide::left >()) != - getUnicodeNormalForm(symlink.getItemName<SelectSide::right>())) - symlink.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(symlink)); - //else if (!sameFileTime(symlink.getLastWriteTime<SelectSide::left>(), - // symlink.getLastWriteTime<SelectSide::right>(), symlink.base().getFileTimeTolerance(), symlink.base().getIgnoredTimeShift())) - // symlink.setCategoryDiffMetadata(getDescrDiffMetaData(symlink)); - else - symlink.setCategory<FILE_EQUAL>(); - } - else - symlink.setCategory<FILE_DIFFERENT_CONTENT>(); - } + symlink.setContentCategory(equalContent ? FileContentCategory::equal : FileContentCategory::different); } } @@ -506,19 +452,13 @@ SharedRef<BaseFolderPair> ComparisonBuffer::compareBySize(const ResolvedFolderPa for (FilePair* file : uncategorizedFiles) { //Caveat: - //1. FILE_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp + //1. FILE_EQUAL may only be set if file names match in case: InSyncFolder's mapping tables use file name as a key! see db_file.cpp //2. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h if (file->getFileSize<SelectSide::left>() == file->getFileSize<SelectSide::right>()) - { - if (getUnicodeNormalForm(file->getItemName<SelectSide::left >()) == - getUnicodeNormalForm(file->getItemName<SelectSide::right>())) - file->setCategory<FILE_EQUAL>(); - else - file->setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(*file)); - } + file->setContentCategory(FileContentCategory::equal); else - file->setCategory<FILE_DIFFERENT_CONTENT>(); + file->setContentCategory(FileContentCategory::different); } return output; } @@ -544,7 +484,8 @@ void categorizeFileByContent(FilePair& file, const std::wstring& txtComparingCon bool haveSameContent = false; const std::wstring errMsg = tryReportingError([&] { - std::wstring statusMsg = replaceCpy(txtComparingContentOfFiles, L"%x", fmtPath(file.getRelativePathAny())); + std::wstring statusMsg = replaceCpy(txtComparingContentOfFiles, L"%x", fmtPath(file.getRelativePath<SelectSide::left>())); + //is it possible that right side has a different relPath? maybe, but who cares, it's a short-lived status message ItemStatReporter statReporter(1, file.getFileSize<SelectSide::left>(), acb); PercentStatReporter percentReporter(statusMsg, file.getFileSize<SelectSide::left>(), statReporter); @@ -566,27 +507,7 @@ void categorizeFileByContent(FilePair& file, const std::wstring& txtComparingCon if (!errMsg.empty()) file.setCategoryConflict(utfTo<Zstringc>(errMsg)); else - { - if (haveSameContent) - { - //Caveat: - //1. FILE_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp - //2. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile - //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h - if (getUnicodeNormalForm(file.getItemName<SelectSide::left >()) != - getUnicodeNormalForm(file.getItemName<SelectSide::right>())) - file.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(file)); -#if 0 //don't synchronize modtime only see FolderPairSyncer::synchronizeFileInt(), SO_COPY_METADATA_TO_* - else if (!sameFileTime(file.getLastWriteTime<SelectSide::left>(), - file.getLastWriteTime<SelectSide::right>(), file.base().getFileTimeTolerance(), file.base().getIgnoredTimeShift())) - file.setCategoryDiffMetadata(getDescrDiffMetaData(file)); -#endif - else - file.setCategory<FILE_EQUAL>(); - } - else - file.setCategory<FILE_DIFFERENT_CONTENT>(); - } + file.setContentCategory(haveSameContent ? FileContentCategory::equal : FileContentCategory::different); } } @@ -631,11 +552,12 @@ std::vector<SharedRef<BaseFolderPair>> ComparisonBuffer::compareByContent(const for (FilePair* file : undefinedFiles) //pre-check: files have different content if they have a different file size (must not be FILE_EQUAL: see InSyncFile) if (file->getFileSize<SelectSide::left>() != file->getFileSize<SelectSide::right>()) - file->setCategory<FILE_DIFFERENT_CONTENT>(); + file->setContentCategory(FileContentCategory::different); else { //perf: skip binary comparison for excluded rows (e.g. via time span and size filter)! //both soft and hard filter were already applied in ComparisonBuffer::performComparison()! + assert(file->getContentCategory() == FileContentCategory::unknown); //=default if (!file->isActive()) file->setCategoryConflict(txtConflictSkippedBinaryComparison); else @@ -728,44 +650,65 @@ std::vector<SharedRef<BaseFolderPair>> ComparisonBuffer::compareByContent(const class MergeSides { public: - MergeSides(const std::unordered_map<ZstringNoCase, Zstringc>& errorsByRelPath, - std::vector<FilePair*>& undefinedFilesOut, - std::vector<SymlinkPair*>& undefinedSymlinksOut) : - errorsByRelPath_(errorsByRelPath), - undefinedFiles_(undefinedFilesOut), - undefinedSymlinks_(undefinedSymlinksOut) {} - - void execute(const FolderContainer& lhs, const FolderContainer& rhs, ContainerObject& output) + static void execute(const FolderContainer& lhs, const FolderContainer& rhs, + const std::unordered_map<Zstring, Zstringc>& errorsByRelPathL, + const std::unordered_map<Zstring, Zstringc>& errorsByRelPathR, + ContainerObject& output, + std::vector<FilePair*>& undefinedFilesOut, + std::vector<SymlinkPair*>& undefinedSymlinksOut) { - auto it = errorsByRelPath_.find(Zstring()); //empty path if read-error for whole base directory + MergeSides inst(errorsByRelPathL, errorsByRelPathR, undefinedFilesOut, undefinedSymlinksOut); + + const Zstringc* errorMsg = nullptr; + if (auto it = inst.errorsByRelPathL_.find(Zstring()); //empty path if read-error for whole base directory + it != inst.errorsByRelPathL_.end()) + errorMsg = &it->second; + else if (auto it2 = inst.errorsByRelPathR_.find(Zstring()); + it2 != inst.errorsByRelPathR_.end()) + errorMsg = &it2->second; - mergeTwoSides(lhs, rhs, - it != errorsByRelPath_.end() ? &it->second : nullptr, - output); + inst.mergeFolders(lhs, rhs, errorMsg, output); } private: - void mergeTwoSides(const FolderContainer& lhs, const FolderContainer& rhs, const Zstringc* errorMsg, ContainerObject& output); + MergeSides(const std::unordered_map<Zstring, Zstringc>& errorsByRelPathL, + const std::unordered_map<Zstring, Zstringc>& errorsByRelPathR, + std::vector<FilePair*>& undefinedFilesOut, + std::vector<SymlinkPair*>& undefinedSymlinksOut) : + errorsByRelPathL_(errorsByRelPathL), + errorsByRelPathR_(errorsByRelPathR), + undefinedFiles_(undefinedFilesOut), + undefinedSymlinks_(undefinedSymlinksOut) {} + + void mergeFolders(const FolderContainer& lhs, const FolderContainer& rhs, const Zstringc* errorMsg, ContainerObject& output); template <SelectSide side> void fillOneSide(const FolderContainer& folderCont, const Zstringc* errorMsg, ContainerObject& output); + template <SelectSide side> + const Zstringc* checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg); + const Zstringc* checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg); - const std::unordered_map<ZstringNoCase, Zstringc>& errorsByRelPath_; //base-relative paths or empty if read-error for whole base directory + const std::unordered_map<Zstring, Zstringc>& errorsByRelPathL_; //base-relative paths or empty if read-error for whole base directory + const std::unordered_map<Zstring, Zstringc>& errorsByRelPathR_; // std::vector<FilePair*>& undefinedFiles_; std::vector<SymlinkPair*>& undefinedSymlinks_; }; -inline +template <SelectSide side> inline const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg) { if (!errorMsg) - if (!errorsByRelPath_.empty()) //only pay for ZstringNoCase construction when needed - if (const auto it = errorsByRelPath_.find(fsObj.getRelativePathAny()); - it != errorsByRelPath_.end()) + { + const std::unordered_map<Zstring, Zstringc>& errorsByRelPath = selectParam<side>(errorsByRelPathL_, errorsByRelPathR_); + + if (!errorsByRelPath.empty()) //only pay for relPath construction when needed + if (const auto it = errorsByRelPath.find(fsObj.getRelativePath<side>()); + it != errorsByRelPath.end()) errorMsg = &it->second; + } if (errorMsg) //make sure all items are disabled => avoid user panicking: https://freefilesync.org/forum/viewtopic.php?t=7582 { @@ -777,6 +720,15 @@ const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstri } +const Zstringc* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstringc* errorMsg) +{ + if (const Zstringc* errorMsgNew = checkFailedRead<SelectSide::left>(fsObj, errorMsg)) + return errorMsgNew; + + return checkFailedRead<SelectSide::right>(fsObj, errorMsg); +} + + template <class MapType, class Function> void forEachSorted(const MapType& fileMap, Function fun) { @@ -802,20 +754,19 @@ void MergeSides::fillOneSide(const FolderContainer& folderCont, const Zstringc* forEachSorted(folderCont.files, [&](const Zstring& fileName, const FileAttributes& attrib) { FilePair& newItem = output.addFile<side>(fileName, attrib); - checkFailedRead(newItem, errorMsg); + checkFailedRead<side>(newItem, errorMsg); }); forEachSorted(folderCont.symlinks, [&](const Zstring& linkName, const LinkAttributes& attrib) { SymlinkPair& newItem = output.addLink<side>(linkName, attrib); - checkFailedRead(newItem, errorMsg); + checkFailedRead<side>(newItem, errorMsg); }); forEachSorted(folderCont.folders, [&](const Zstring& folderName, const std::pair<FolderAttributes, FolderContainer>& attrib) { FolderPair& newFolder = output.addFolder<side>(folderName, attrib.first); - const Zstringc* errorMsgNew = checkFailedRead(newFolder, errorMsg); - + const Zstringc* errorMsgNew = checkFailedRead<side>(newFolder, errorMsg); fillOneSide<side>(attrib.second, errorMsgNew, newFolder); //recurse }); } @@ -894,13 +845,13 @@ void matchFolders(const MapType& mapLeft, const MapType& mapRight, ProcessLeftOn } -void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer& rhs, const Zstringc* errorMsg, ContainerObject& output) +void MergeSides::mergeFolders(const FolderContainer& lhs, const FolderContainer& rhs, const Zstringc* errorMsg, ContainerObject& output) { using FileData = FolderContainer::FileList::value_type; matchFolders(lhs.files, rhs.files, [&](const FileData& fileLeft, const Zstringc* conflictMsg) { - FilePair& newItem = output.addFile<SelectSide::left >(fileLeft.first, fileLeft.second); + FilePair& newItem = output.addFile<SelectSide::left>(fileLeft.first, fileLeft.second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const FileData& fileRight, const Zstringc* conflictMsg) @@ -912,7 +863,6 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer { FilePair& newItem = output.addFile(fileLeft.first, fileLeft.second, - FILE_CONFLICT, //dummy-value until categorization is finished later fileRight.first, fileRight.second); if (!checkFailedRead(newItem, errorMsg)) @@ -925,7 +875,7 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer matchFolders(lhs.symlinks, rhs.symlinks, [&](const SymlinkData& symlinkLeft, const Zstringc* conflictMsg) { - SymlinkPair& newItem = output.addLink<SelectSide::left >(symlinkLeft.first, symlinkLeft.second); + SymlinkPair& newItem = output.addLink<SelectSide::left>(symlinkLeft.first, symlinkLeft.second); checkFailedRead(newItem, conflictMsg ? conflictMsg : errorMsg); }, [&](const SymlinkData& symlinkRight, const Zstringc* conflictMsg) @@ -937,7 +887,6 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer { SymlinkPair& newItem = output.addLink(symlinkLeft.first, symlinkLeft.second, - SYMLINK_CONFLICT, //dummy-value until categorization is finished later symlinkRight.first, symlinkRight.second); if (!checkFailedRead(newItem, errorMsg)) @@ -961,34 +910,28 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer }, [&](const FolderData& dirLeft, const FolderData& dirRight) { - FolderPair& newFolder = output.addFolder(dirLeft.first, dirLeft.second.first, DIR_EQUAL, dirRight.first, dirRight.second.first); + FolderPair& newFolder = output.addFolder(dirLeft.first, dirLeft.second.first, dirRight.first, dirRight.second.first); const Zstringc* errorMsgNew = checkFailedRead(newFolder, errorMsg); - - if (!errorMsgNew) - if (getUnicodeNormalForm(dirLeft.first) != - getUnicodeNormalForm(dirRight.first)) - newFolder.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(newFolder)); - - mergeTwoSides(dirLeft.second.second, dirRight.second.second, errorMsgNew, newFolder); //recurse + mergeFolders(dirLeft.second.second, dirRight.second.second, errorMsgNew, newFolder); //recurse }); } //----------------------------------------------------------------------------------------------- //uncheck excluded directories (see parallelDeviceTraversal()) + remove superfluous excluded subdirectories -void stripExcludedDirectories(ContainerObject& hierObj, const PathFilter& filterProc) +void stripExcludedDirectories(ContainerObject& conObj, const PathFilter& filter) { - for (FolderPair& folder : hierObj.refSubFolders()) - stripExcludedDirectories(folder, filterProc); + for (FolderPair& folder : conObj.refSubFolders()) + stripExcludedDirectories(folder, filter); - //remove superfluous directories: - // this does not invalidate "std::vector<FilePair*>& undefinedFiles", since we delete folders only - // and there is no side-effect for memory positions of FilePair and SymlinkPair thanks to std::list! + /* remove superfluous directories: + this does not invalidate "std::vector<FilePair*>& undefinedFiles", since we delete folders only + and there is no side-effect for memory positions of FilePair and SymlinkPair thanks to std::list! */ static_assert(std::is_same_v<std::list<FolderPair>, ContainerObject::FolderList>); - hierObj.refSubFolders().remove_if([&](FolderPair& folder) + conObj.refSubFolders().remove_if([&](FolderPair& folder) { - const bool included = filterProc.passDirFilter(folder.getRelativePathAny(), nullptr); //childItemMightMatch is false, child items were already excluded during scanning + const bool included = folder.passDirFilter(filter, nullptr /*childItemMightMatch*/); //child items were already excluded during scanning if (!included) //falsify only! (e.g. might already be inactive due to read error!) folder.setActive(false); @@ -1014,7 +957,8 @@ SharedRef<BaseFolderPair> ComparisonBuffer::performComparison(const ResolvedFold const BaseFolderStatus folderStatusR = getBaseFolderStatus(fp.folderPathRight); - std::unordered_map<ZstringNoCase, Zstringc> failedReads; //base-relative paths or empty if read-error for whole base directory + std::unordered_map<Zstring, Zstringc> failedReadsL; //base-relative paths or empty if read-error for whole base directory + std::unordered_map<Zstring, Zstringc> failedReadsR; // const FolderContainer* folderContL = nullptr; const FolderContainer* folderContR = nullptr; @@ -1027,14 +971,14 @@ SharedRef<BaseFolderPair> ComparisonBuffer::performComparison(const ResolvedFold if (it == folderStatus_.failedChecks.end()) it = folderStatus_.failedChecks.find(fp.folderPathRight); - failedReads[Zstring() /*empty string for root*/] = utfTo<Zstringc>(it->second.toString()); + failedReadsL[Zstring() /*empty string for root*/] = failedReadsR[Zstring()] = utfTo<Zstringc>(it->second.toString()); folderContL = ∅ //no need to list or display one-sided results if - folderContR = ∅ //*any* folder existence check fails (even if other side would have been in folderBuffer_!) + folderContR = ∅ //*any* folder existence check failed (even if other side exists in folderBuffer_!) } else { - auto evalBuffer = [&](const AbstractPath& folderPath, const FolderContainer*& folderCont) + auto evalBuffer = [&](const AbstractPath& folderPath, const FolderContainer*& folderCont, std::unordered_map<Zstring, Zstringc>& failedReads) { auto it = folderBuffer_.find({folderPath, fpCfg.filter.nameFilter, fpCfg.handleSymlinks}); if (it != folderBuffer_.end()) @@ -1043,9 +987,9 @@ SharedRef<BaseFolderPair> ComparisonBuffer::performComparison(const ResolvedFold //mix failedFolderReads with failedItemReads: //associate folder traversing errors with folder (instead of child items only) to show on GUI! See "MergeSides" - //=> minor pessimization for "excludefilterFailedRead" which needlessly excludes parent folders, too - failedReads.insert(dirVal.failedFolderReads.begin(), dirVal.failedFolderReads.end()); - failedReads.insert(dirVal.failedItemReads .begin(), dirVal.failedItemReads .end()); + //=> minor pessimization for "excludeFilterFailedRead" which needlessly excludes parent folders, too + failedReads = dirVal.failedFolderReads; //failedReads.insert(dirVal.failedFolderReads.begin(), dirVal.failedFolderReads.end()); + failedReads.insert(dirVal.failedItemReads.begin(), dirVal.failedItemReads.end()); assert(getBaseFolderStatus(folderPath) == BaseFolderStatus::existing); folderCont = &dirVal.folderCont; @@ -1056,34 +1000,41 @@ SharedRef<BaseFolderPair> ComparisonBuffer::performComparison(const ResolvedFold folderCont = ∅ } }; - evalBuffer(fp.folderPathLeft, folderContL); - evalBuffer(fp.folderPathRight, folderContR); + evalBuffer(fp.folderPathLeft, folderContL, failedReadsL); + evalBuffer(fp.folderPathRight, folderContR, failedReadsR); } - Zstring excludefilterFailedRead; - if (failedReads.contains(Zstring())) //empty path if read-error for whole base directory - excludefilterFailedRead += Zstr("*\n"); + Zstring excludeFilterFailedRead; + if (failedReadsL.contains(Zstring()) || + failedReadsR.contains(Zstring())) //empty path if read-error for whole base directory + excludeFilterFailedRead += Zstr("*\n"); else - for (const auto& [relPath, errorMsg] : failedReads) - excludefilterFailedRead += relPath.upperCase + Zstr('\n'); //exclude item AND (potential) child items! + { + for (const auto& [relPath, errorMsg] : failedReadsL) + excludeFilterFailedRead += relPath + Zstr('\n'); //exclude item AND (potential) child items! + + for (const auto& [relPath, errorMsg] : failedReadsR) + excludeFilterFailedRead += relPath + Zstr('\n'); + } //somewhat obscure, but it's possible on Linux file systems to have a backslash as part of a file name //=> avoid misinterpretation when parsing the filter phrase in PathFilter (see path_filter.cpp::parseFilterPhrase()) - if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(excludefilterFailedRead, Zstr('/'), Zstr('?')); - if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(excludefilterFailedRead, Zstr('\\'), Zstr('?')); + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(excludeFilterFailedRead, Zstr('/'), Zstr('?')); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(excludeFilterFailedRead, Zstr('\\'), Zstr('?')); SharedRef<BaseFolderPair> output = makeSharedRef<BaseFolderPair>(fp.folderPathLeft, folderStatusL, //check folder existence only once! fp.folderPathRight, folderStatusR, // - fpCfg.filter.nameFilter.ref().copyFilterAddingExclusion(excludefilterFailedRead), + fpCfg.filter.nameFilter.ref().copyFilterAddingExclusion(excludeFilterFailedRead), fpCfg.compareVar, fileTimeTolerance_, fpCfg.ignoreTimeShiftMinutes); //PERF_START; - MergeSides(failedReads, undefinedFiles, undefinedSymlinks).execute(*folderContL, *folderContR, output.ref()); + MergeSides::execute(*folderContL, *folderContR, failedReadsL, failedReadsR, + output.ref(), undefinedFiles, undefinedSymlinks); //PERF_STOP; //##################### in/exclude rows according to filtering ##################### diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index 193abe7a..e5dac708 100644 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -24,7 +24,7 @@ namespace //------------------------------------------------------------------------------------------------------------------------------- const char DB_FILE_DESCR[] = "FreeFileSync"; const int DB_FILE_VERSION = 11; //2020-02-07 -const int DB_STREAM_VERSION = 4; //2021-02-14 +const int DB_STREAM_VERSION = 5; //2023-07-29 //------------------------------------------------------------------------------------------------------------------------------- struct SessionData @@ -294,7 +294,6 @@ private: for (const auto& [itemName, inSyncData] : container.folders) { writeItemName(itemName.normStr); - writeNumber<int32_t>(streamOutSmallNum_, inSyncData.status); recurse(inSyncData); } @@ -368,7 +367,7 @@ public: const std::string tmpL = readContainer<std::string>(streamInL); const std::string tmpR = readContainer<std::string>(streamInR); - auto output = makeSharedRef<InSyncFolder>(InSyncFolder::DIR_STATUS_IN_SYNC); + auto output = makeSharedRef<InSyncFolder>(); StreamParserV2 parser(decompress(tmpL), // decompress(tmpR), //throw SysError decompress(tmpB)); // @@ -376,6 +375,7 @@ public: return output; } else if (streamVersion == 3 || //TODO: remove migration code at some time! 2021-02-14 + streamVersion == 4 || //TODO: remove migration code at some time! 2023-07-29 streamVersion == DB_STREAM_VERSION) { MemoryStreamIn& streamInPart1 = leadStreamLeft ? streamInL : streamInR; @@ -393,7 +393,7 @@ public: const std::string bufSmallNum = readContainer<std::string>(streamIn); //throw SysErrorUnexpectedEos const std::string bufBigNum = readContainer<std::string>(streamIn); // - auto output = makeSharedRef<InSyncFolder>(InSyncFolder::DIR_STATUS_IN_SYNC); + auto output = makeSharedRef<InSyncFolder>(); StreamParser parser(streamVersion, decompress(bufText), // decompress(bufSmallNum), //throw SysError @@ -433,12 +433,12 @@ private: const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(streamInSmallNum_)); // const uint64_t fileSize = readNumber<uint64_t>(streamInSmallNum_); // - const InSyncDescrFile dataL = readFileDescr(); //throw SysErrorUnexpectedEos - const InSyncDescrFile dataT = readFileDescr(); // + const InSyncDescrFile descrL = readFileDescr(); //throw SysErrorUnexpectedEos + const InSyncDescrFile descrT = readFileDescr(); // container.addFile(itemName, - selectParam<leadSide>(dataL, dataT), - selectParam<leadSide>(dataT, dataL), cmpVar, fileSize); + selectParam<leadSide>(descrL, descrT), + selectParam<leadSide>(descrT, descrL), cmpVar, fileSize); } size_t linkCount = readNumber<uint32_t>(streamInSmallNum_); @@ -447,21 +447,23 @@ private: const Zstring itemName = readItemName(); // const auto cmpVar = static_cast<CompareVariant>(readNumber<int32_t>(streamInSmallNum_)); // - const InSyncDescrLink dataL{static_cast<time_t>(readNumber<int64_t>(streamInBigNum_))}; //throw SysErrorUnexpectedEos - const InSyncDescrLink dataT{static_cast<time_t>(readNumber<int64_t>(streamInBigNum_))}; // + const InSyncDescrLink descrL{static_cast<time_t>(readNumber<int64_t>(streamInBigNum_))}; //throw SysErrorUnexpectedEos + const InSyncDescrLink descrT{static_cast<time_t>(readNumber<int64_t>(streamInBigNum_))}; // container.addSymlink(itemName, - selectParam<leadSide>(dataL, dataT), - selectParam<leadSide>(dataT, dataL), cmpVar); + selectParam<leadSide>(descrL, descrT), + selectParam<leadSide>(descrT, descrL), cmpVar); } size_t dirCount = readNumber<uint32_t>(streamInSmallNum_); // while (dirCount-- != 0) { const Zstring itemName = readItemName(); // - const auto status = static_cast<InSyncFolder::InSyncStatus>(readNumber<int32_t>(streamInSmallNum_)); // - InSyncFolder& dbFolder = container.addFolder(itemName, status); + if (streamVersion_ <= 4) //TODO: remove migration code at some time! 2023-07-29 + /*const auto status = static_cast<InSyncFolder::InSyncStatus>(*/ readNumber<int32_t>(streamInSmallNum_); + + InSyncFolder& dbFolder = container.addFolder(itemName); recurse<leadSide>(dbFolder); } } @@ -530,9 +532,9 @@ private: while (dirCount-- != 0) { const Zstring itemName = utfTo<Zstring>(readContainer<std::string>(inputBoth_)); - const auto status = static_cast<InSyncFolder::InSyncStatus>(readNumber<int32_t>(inputBoth_)); + /*const auto status = static_cast<InSyncFolder::InSyncStatus>(*/ readNumber<int32_t>(inputBoth_); - InSyncFolder& dbFolder = container.addFolder(itemName, status); + InSyncFolder& dbFolder = container.addFolder(itemName); recurse(dbFolder); } } @@ -567,7 +569,7 @@ public: static void execute(const BaseFolderPair& baseFolder, InSyncFolder& dbFolder) { LastSynchronousStateUpdater updater(baseFolder.getCompVariant(), baseFolder.getFilter()); - updater.recurse(baseFolder, dbFolder); + updater.recurse(baseFolder, Zstring(), dbFolder); } private: @@ -575,11 +577,11 @@ private: filter_(filter), activeCmpVar_(activeCmpVar) {} - void recurse(const ContainerObject& hierObj, InSyncFolder& dbFolder) + void recurse(const ContainerObject& conObj, const Zstring& relPath, InSyncFolder& dbFolder) { - process(hierObj.refSubFiles (), hierObj.getRelativePathAny(), dbFolder.files); - process(hierObj.refSubLinks (), hierObj.getRelativePathAny(), dbFolder.symlinks); - process(hierObj.refSubFolders(), hierObj.getRelativePathAny(), dbFolder.folders); + process(conObj.refSubFiles (), relPath, dbFolder.files); + process(conObj.refSubLinks (), relPath, dbFolder.symlinks); + process(conObj.refSubFolders(), relPath, dbFolder.folders); } void process(const ContainerObject::FileList& currentFiles, const Zstring& parentRelPath, InSyncFolder::FileList& dbFiles) @@ -591,20 +593,22 @@ private: { if (file.getCategory() == FILE_EQUAL) //data in sync: write current state { - //Caveat: If FILE_EQUAL, we *implicitly* assume equal left and right short names matching case: InSyncFolder's mapping tables use short name as a key! + //Caveat: If FILE_EQUAL, we *implicitly* assume equal left and right file names matching case: InSyncFolder's mapping tables use file name as a key! //This makes us silently dependent from code in algorithm.h!!! - assert(getUnicodeNormalForm(file.getItemName<SelectSide::left>()) == getUnicodeNormalForm(file.getItemName<SelectSide::right>())); + assert(file.hasEquivalentItemNames()); + const Zstring& fileName = file.getItemName<SelectSide::left>(); assert(file.getFileSize<SelectSide::left>() == file.getFileSize<SelectSide::right>()); //create or update new "in-sync" state - dbFiles.insert_or_assign(file.getItemNameAny(), - InSyncFile(InSyncDescrFile{file.getLastWriteTime<SelectSide::left >(), - file.getFilePrint <SelectSide::left >()}, - InSyncDescrFile{file.getLastWriteTime<SelectSide::right>(), - file.getFilePrint <SelectSide::right>()}, - activeCmpVar_, - file.getFileSize<SelectSide::left>())); - toPreserve.insert(file.getItemNameAny()); + dbFiles.insert_or_assign(fileName, InSyncFile + { + .left = InSyncDescrFile{file.getLastWriteTime<SelectSide::left >(), file.getFilePrint<SelectSide::left >()}, + .right = InSyncDescrFile{file.getLastWriteTime<SelectSide::right>(), file.getFilePrint<SelectSide::right>()}, + .cmpVar = activeCmpVar_, + .fileSize = file.getFileSize<SelectSide::left>(), + } + ); + toPreserve.insert(fileName); } else //not in sync: preserve last synchronous state { @@ -634,14 +638,17 @@ private: { if (symlink.getLinkCategory() == SYMLINK_EQUAL) //data in sync: write current state { - assert(getUnicodeNormalForm(symlink.getItemName<SelectSide::left>()) == getUnicodeNormalForm(symlink.getItemName<SelectSide::right>())); + assert(symlink.hasEquivalentItemNames()); + const Zstring& linkName = symlink.getItemName<SelectSide::left>(); //create or update new "in-sync" state - dbSymlinks.insert_or_assign(symlink.getItemNameAny(), - InSyncSymlink(InSyncDescrLink{symlink.getLastWriteTime<SelectSide::left >()}, - InSyncDescrLink{symlink.getLastWriteTime<SelectSide::right>()}, - activeCmpVar_)); - toPreserve.insert(symlink.getItemNameAny()); + dbSymlinks.insert_or_assign(linkName, InSyncSymlink + { + .left = InSyncDescrLink{symlink.getLastWriteTime<SelectSide::left >()}, + .right = InSyncDescrLink{symlink.getLastWriteTime<SelectSide::right>()}, + .cmpVar = activeCmpVar_, + }); + toPreserve.insert(linkName); } else //not in sync: preserve last synchronous state { @@ -670,35 +677,36 @@ private: { if (folder.getDirCategory() == DIR_EQUAL) { - assert(getUnicodeNormalForm(folder.getItemName<SelectSide::left>()) == getUnicodeNormalForm(folder.getItemName<SelectSide::right>())); + assert(folder.hasEquivalentItemNames()); + const Zstring& folderName = folder.getItemName<SelectSide::left>(); - //update directory entry only (shallow), but do *not touch* existing child elements!!! - InSyncFolder& dbFolder = dbFolders.emplace(folder.getItemNameAny(), InSyncFolder(InSyncFolder::DIR_STATUS_IN_SYNC)).first->second; //get or create - dbFolder.status = InSyncFolder::DIR_STATUS_IN_SYNC; + //create directory entry if not existing (but do *not touch* existing child elements!!!) + dbFolders.try_emplace(folderName); - toPreserve.emplace(folder.getItemNameAny(), &folder); + toPreserve.emplace(folderName, &folder); } else //not in sync: preserve last synchronous state { - toPreserve.emplace(folder.getItemName<SelectSide::left >(), &folder); //names differing in case? => treat like any other folder rename + toPreserve.emplace(folder.getItemName<SelectSide::left >(), &folder); //names differing (in case)? => treat like any other folder rename toPreserve.emplace(folder.getItemName<SelectSide::right>(), &folder); //=> no *new* database entries even if child items are in sync + //BUT: update existing one: there should be only *one* DB entry after a folder rename (matching either folder name on left or right) } } //delete removed items (= "in-sync") from database eraseIf(dbFolders, [&](InSyncFolder::FolderList::value_type& v) { + const Zstring& itemRelPath = appendPath(parentRelPath, v.first.normStr); + if (auto it = toPreserve.find(v.first); it != toPreserve.end()) { - recurse(*(it->second), v.second); //required even if e.g. DIR_LEFT_SIDE_ONLY: + recurse(*(it->second), itemRelPath, v.second); //required even if e.g. DIR_LEFT_ONLY: //existing child-items may not be in sync, but items deleted on both sides *are* in-sync!!! return false; } - const Zstring& itemRelPath = appendPath(parentRelPath, v.first.normStr); //if folder is not included in "current folders", it is either not existing anymore, in which case it should be deleted from database //or it was excluded via filter and the database entry should be preserved - bool childItemMightMatch = true; const bool passFilter = filter_.passDirFilter(itemRelPath, &childItemMightMatch); if (!passFilter && childItemMightMatch) @@ -917,7 +925,7 @@ void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transa //load last synchrounous state auto itStreamOldL = streamsL.cend(); auto itStreamOldR = streamsR.cend(); - InSyncFolder lastSyncState(InSyncFolder::DIR_STATUS_IN_SYNC); + InSyncFolder lastSyncState; try { //find associated session: there can be at most one session within intersection of left and right IDs diff --git a/FreeFileSync/Source/base/db_file.h b/FreeFileSync/Source/base/db_file.h index 67be33fa..d8f9c82a 100644 --- a/FreeFileSync/Source/base/db_file.h +++ b/FreeFileSync/Source/base/db_file.h @@ -32,7 +32,6 @@ struct InSyncDescrLink //artificial hierarchy of last synchronous state: struct InSyncFile { - InSyncFile(const InSyncDescrFile& l, const InSyncDescrFile& r, CompareVariant cv, uint64_t fileSizeIn) : left(l), right(r), cmpVar(cv), fileSize(fileSizeIn) {} InSyncDescrFile left; //support flip()! InSyncDescrFile right; // CompareVariant cmpVar = CompareVariant::timeSize; //the one active while finding "file in sync" @@ -41,7 +40,6 @@ struct InSyncFile struct InSyncSymlink { - InSyncSymlink(const InSyncDescrLink& l, const InSyncDescrLink& r, CompareVariant cv) : left(l), right(r), cmpVar(cv) {} InSyncDescrLink left; InSyncDescrLink right; CompareVariant cmpVar = CompareVariant::timeSize; @@ -49,17 +47,6 @@ struct InSyncSymlink struct InSyncFolder { - //for directories we have a logical problem: we cannot have "not existent" as an indicator for - //"no last synchronous state" since this precludes child elements that may be in sync! - enum InSyncStatus - { - DIR_STATUS_IN_SYNC, - DIR_STATUS_STRAW_MAN //no last synchronous state, but used as container only - }; - InSyncFolder(InSyncStatus statusIn) : status(statusIn) {} - - InSyncStatus status = DIR_STATUS_STRAW_MAN; - //------------------------------------------------------------------ using FolderList = std::unordered_map<ZstringNorm, InSyncFolder >; // using FileList = std::unordered_map<ZstringNorm, InSyncFile >; // key: file name (ignoring Unicode normal forms) @@ -71,19 +58,26 @@ struct InSyncFolder SymlinkList symlinks; //non-followed symlinks //convenience - InSyncFolder& addFolder(const Zstring& folderName, InSyncStatus st) + InSyncFolder& addFolder(const Zstring& folderName) { - return folders.emplace(folderName, InSyncFolder(st)).first->second; + const auto [it, inserted] = folders.try_emplace(folderName); + assert(inserted); + return it->second; } - void addFile(const Zstring& fileName, const InSyncDescrFile& dataL, const InSyncDescrFile& dataR, CompareVariant cmpVar, uint64_t fileSize) + void addFile(const Zstring& fileName, const InSyncDescrFile& descrL, const InSyncDescrFile& descrR, CompareVariant cmpVar, uint64_t fileSize) { - files.emplace(fileName, InSyncFile(dataL, dataR, cmpVar, fileSize)); + files.emplace(fileName, InSyncFile {descrL, descrR, cmpVar, fileSize}); + assert(inserted); + warn_static("use try_emplace once mac is up to the task!!!") + //"Parenthesized initialization of aggregates" https://en.cppreference.com/w/cpp/compiler_support/20 } - void addSymlink(const Zstring& linkName, const InSyncDescrLink& dataL, const InSyncDescrLink& dataR, CompareVariant cmpVar) + void addSymlink(const Zstring& linkName, const InSyncDescrLink& descrL, const InSyncDescrLink& descrR, CompareVariant cmpVar) { - symlinks.emplace(linkName, InSyncSymlink(dataL, dataR, cmpVar)); + symlinks.emplace(linkName, InSyncSymlink {descrL, descrR, cmpVar}); + assert(inserted); + warn_static("use try_emplace once mac is up to the task!!!") } }; diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h index f6af51fa..640479b9 100644 --- a/FreeFileSync/Source/base/dir_exist_async.h +++ b/FreeFileSync/Source/base/dir_exist_async.h @@ -53,6 +53,8 @@ FolderStatus getFolderStatusParallel(const std::set<AbstractPath>& folderPaths, //---------------------------------------------------------------------- std::vector<ThreadGroup<std::packaged_task<bool()>>> deviceThreadGroups; + //---------------------------------------------------------------------- + for (const auto& [device, deviceFolderPaths] : perDevicePaths) { deviceThreadGroups.emplace_back(1, Zstr("DirExist: ") + utfTo<Zstring>(AFS::getDisplayPath(AbstractPath(device, AfsPath())))); diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 0486ccd3..cf0ff18a 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -296,8 +296,6 @@ ProcessStatus getProcessStatus(const LockInformation& lockInfo) //throw FileErro DEFINE_NEW_FILE_ERROR(ErrorFileNotExisting) uint64_t getLockFileSize(const Zstring& filePath) //throw FileError, ErrorFileNotExisting { - //yes, checking error ERROR_FILE_NOT_FOUND, ENOENT is not 100% reliable (e.g. might indicate missing parent folder) - //but good enough for our lock file! struct stat fileInfo = {}; if (::stat(filePath.c_str(), &fileInfo) == 0) return fileInfo.st_size; @@ -401,7 +399,7 @@ void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifySta if (lastCheckTime >= lastLifeSign + EMIT_LIFE_SIGN_INTERVAL + std::chrono::seconds(1)) { const int remainingSeconds = std::max(0, static_cast<int>(std::chrono::duration_cast<std::chrono::seconds>(DETECT_ABANDONED_INTERVAL - (now - lastLifeSign)).count())); - notifyStatus(infoMsg + SPACED_DASH + _("Detecting abandoned lock...") + L' ' + _P("1 sec", "%x sec", remainingSeconds)); //throw X + notifyStatus(infoMsg + SPACED_DASH + _("Lock file apparently abandoned...") + L' ' + _P("1 sec", "%x sec", remainingSeconds)); //throw X } else notifyStatus(std::wstring(infoMsg)); //throw X; emit a message in any case (might clear other one) diff --git a/FreeFileSync/Source/base/file_hierarchy.cpp b/FreeFileSync/Source/base/file_hierarchy.cpp index e3ed5c88..8945d705 100644 --- a/FreeFileSync/Source/base/file_hierarchy.cpp +++ b/FreeFileSync/Source/base/file_hierarchy.cpp @@ -50,156 +50,214 @@ std::wstring fff::getShortDisplayNameForFolderPair(const AbstractPath& itemPathL else if (AFS::isNullPath(itemPathR)) return getLastComponent(itemPathL); else - return getLastComponent(itemPathL) + SPACED_DASH + + return getLastComponent(itemPathL) + L" | " + getLastComponent(itemPathR); } -void ContainerObject::removeEmptyRec() +void ContainerObject::removeDoubleEmpty() { - bool emptyExisting = false; - auto isEmpty = [&](const FileSystemObject& fsObj) -> bool - { - const bool objEmpty = fsObj.isPairEmpty(); - if (objEmpty) - emptyExisting = true; - return objEmpty; - }; + auto isEmpty = [](const FileSystemObject& fsObj) { return fsObj.isPairEmpty(); }; refSubFiles ().remove_if(isEmpty); refSubLinks ().remove_if(isEmpty); refSubFolders().remove_if(isEmpty); - if (emptyExisting) //notify if actual deletion happened - notifySyncCfgChanged(); //mustn't call this in ~FileSystemObject(), since parent, usually a FolderPair, may already be partially destroyed and existing as a pure ContainerObject! - for (FolderPair& folder : refSubFolders()) - folder.removeEmptyRec(); //recurse + folder.removeDoubleEmpty(); } namespace { -SyncOperation getIsolatedSyncOperation(bool itemExistsLeft, - bool itemExistsRight, - CompareFileResult cmpResult, +SyncOperation getIsolatedSyncOperation(const FileSystemObject& fsObj, bool selectedForSync, SyncDirection syncDir, - bool hasDirectionConflict) //perf: std::wstring was wasteful here + bool hasDirectionConflict) { - assert(( itemExistsLeft && itemExistsRight && cmpResult != FILE_LEFT_SIDE_ONLY && cmpResult != FILE_RIGHT_SIDE_ONLY) || - ( itemExistsLeft && !itemExistsRight && cmpResult == FILE_LEFT_SIDE_ONLY ) || - (!itemExistsLeft && itemExistsRight && cmpResult == FILE_RIGHT_SIDE_ONLY) || - (!itemExistsLeft && !itemExistsRight && cmpResult == FILE_EQUAL && syncDir == SyncDirection::none && !hasDirectionConflict) || - cmpResult == FILE_CONFLICT); - assert(!hasDirectionConflict || syncDir == SyncDirection::none); - if (!selectedForSync) - return cmpResult == FILE_EQUAL ? - SO_EQUAL : - SO_DO_NOTHING; - - switch (cmpResult) + if (fsObj.isEmpty<SelectSide::left>() || fsObj.isEmpty<SelectSide::right>()) { - case FILE_EQUAL: - assert(syncDir == SyncDirection::none); - return SO_EQUAL; + if (!selectedForSync) + return SO_DO_NOTHING; - case FILE_LEFT_SIDE_ONLY: - switch (syncDir) - { - case SyncDirection::left: - return SO_DELETE_LEFT; //delete files on left - case SyncDirection::right: - return SO_CREATE_NEW_RIGHT; //copy files to right - case SyncDirection::none: - return hasDirectionConflict ? SO_UNRESOLVED_CONFLICT : SO_DO_NOTHING; - } - break; + if (hasDirectionConflict) + return SO_UNRESOLVED_CONFLICT; - case FILE_RIGHT_SIDE_ONLY: + if (fsObj.isEmpty<SelectSide::left>()) + { + if (fsObj.isEmpty<SelectSide::right>()) //both sides empty: should only occur temporarily, if ever + return SO_EQUAL; + else //right-only + switch (syncDir) + { + //*INDENT-OFF* + case SyncDirection::left: return SO_CREATE_LEFT; + case SyncDirection::right: return SO_DELETE_RIGHT; + case SyncDirection::none: return SO_DO_NOTHING; + //*INDENT-ON* + } + } + else //left-only switch (syncDir) { - case SyncDirection::left: - return SO_CREATE_NEW_LEFT; //copy files to left - case SyncDirection::right: - return SO_DELETE_RIGHT; //delete files on right - case SyncDirection::none: - return hasDirectionConflict ? SO_UNRESOLVED_CONFLICT : SO_DO_NOTHING; + //*INDENT-OFF* + case SyncDirection::left: return SO_DELETE_LEFT; + case SyncDirection::right: return SO_CREATE_RIGHT; + case SyncDirection::none: return SO_DO_NOTHING; + //*INDENT-ON* } - break; + } + //-------------------------------------------------------------- + std::optional<SyncOperation> result; - case FILE_LEFT_NEWER: - case FILE_RIGHT_NEWER: - case FILE_DIFFERENT_CONTENT: - switch (syncDir) - { - case SyncDirection::left: - return SO_OVERWRITE_LEFT; //copy from right to left - case SyncDirection::right: - return SO_OVERWRITE_RIGHT; //copy from left to right - case SyncDirection::none: - return hasDirectionConflict ? SO_UNRESOLVED_CONFLICT : SO_DO_NOTHING; - } - break; + visitFSObject(fsObj, + [&](const FolderPair& folder) //see FolderPair::getCategory() + { + if (folder.hasEquivalentItemNames()) //a.k.a. DIR_EQUAL + { + assert(syncDir == SyncDirection::none); + return result = SO_EQUAL; //no matter if "conflict" (e.g. traversal error) or "not selected" + } - case FILE_DIFFERENT_METADATA: - switch (syncDir) - { - case SyncDirection::left: - return SO_COPY_METADATA_TO_LEFT; - case SyncDirection::right: - return SO_COPY_METADATA_TO_RIGHT; - case SyncDirection::none: - return hasDirectionConflict ? SO_UNRESOLVED_CONFLICT : SO_DO_NOTHING; - } - break; + if (!selectedForSync) + return result = SO_DO_NOTHING; - case FILE_CONFLICT: - switch (syncDir) - { - case SyncDirection::left: - return itemExistsLeft && itemExistsRight ? SO_OVERWRITE_LEFT : itemExistsLeft ? SO_DELETE_LEFT: SO_CREATE_NEW_LEFT; - case SyncDirection::right: - return itemExistsLeft && itemExistsRight ? SO_OVERWRITE_RIGHT : itemExistsLeft ? SO_CREATE_NEW_RIGHT : SO_DELETE_RIGHT; - case SyncDirection::none: - return hasDirectionConflict ? SO_UNRESOLVED_CONFLICT : SO_DO_NOTHING; - } - break; - } + if (hasDirectionConflict) + return result = SO_UNRESOLVED_CONFLICT; - assert(false); - return SO_DO_NOTHING; + switch (syncDir) + { + //*INDENT-OFF* + case SyncDirection::left: return result = SO_RENAME_LEFT; + case SyncDirection::right: return result = SO_RENAME_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + //*INDENT-ON* + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); + }, + //-------------------------------------------------------------- + [&](const FilePair& file) //see FilePair::getCategory() + { + if (file.getContentCategory() == FileContentCategory::equal && file.hasEquivalentItemNames()) //a.k.a. FILE_EQUAL + { + assert(syncDir == SyncDirection::none); + return result = SO_EQUAL; //no matter if "conflict" (e.g. traversal error) or "not selected" + } + + if (!selectedForSync) + return result = SO_DO_NOTHING; + + if (hasDirectionConflict) + return result = SO_UNRESOLVED_CONFLICT; + + switch (file.getContentCategory()) + { + case FileContentCategory::unknown: + case FileContentCategory::leftNewer: + case FileContentCategory::rightNewer: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: + switch (syncDir) + { + //*INDENT-OFF* + case SyncDirection::left: return result = SO_OVERWRITE_LEFT; + case SyncDirection::right: return result = SO_OVERWRITE_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + //*INDENT-ON* + } + break; + + case FileContentCategory::equal: + switch (syncDir) + { + //*INDENT-OFF* + case SyncDirection::left: return result = SO_RENAME_LEFT; + case SyncDirection::right: return result = SO_RENAME_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + //*INDENT-ON* + } + break; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); + }, + //-------------------------------------------------------------- + [&](const SymlinkPair& symlink) //see SymlinkPair::getCategory() + { + if (symlink.getContentCategory() == FileContentCategory::equal && symlink.hasEquivalentItemNames()) //a.k.a. SYMLINK_EQUAL + { + assert(syncDir == SyncDirection::none); + return result = SO_EQUAL; //no matter if "conflict" (e.g. traversal error) or "not selected" + } + + if (!selectedForSync) + return result = SO_DO_NOTHING; + + if (hasDirectionConflict) + return result = SO_UNRESOLVED_CONFLICT; + + switch (symlink.getContentCategory()) + { + case FileContentCategory::unknown: + case FileContentCategory::leftNewer: + case FileContentCategory::rightNewer: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: + switch (syncDir) + { + //*INDENT-OFF* + case SyncDirection::left: return result = SO_OVERWRITE_LEFT; + case SyncDirection::right: return result = SO_OVERWRITE_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + //*INDENT-ON* + } + break; + + case FileContentCategory::equal: + switch (syncDir) + { + //*INDENT-OFF* + case SyncDirection::left: return result = SO_RENAME_LEFT; + case SyncDirection::right: return result = SO_RENAME_RIGHT; + case SyncDirection::none: return result = SO_DO_NOTHING; + //*INDENT-ON* + } + break; + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); + }); + return *result; } template <class Predicate> inline -bool hasDirectChild(const ContainerObject& hierObj, Predicate p) +bool hasDirectChild(const ContainerObject& conObj, Predicate p) { - return std::any_of(hierObj.refSubFiles ().begin(), hierObj.refSubFiles ().end(), p) || - std::any_of(hierObj.refSubLinks ().begin(), hierObj.refSubLinks ().end(), p) || - std::any_of(hierObj.refSubFolders().begin(), hierObj.refSubFolders().end(), p); + return std::any_of(conObj.refSubFiles ().begin(), conObj.refSubFiles ().end(), p) || + std::any_of(conObj.refSubLinks ().begin(), conObj.refSubLinks ().end(), p) || + std::any_of(conObj.refSubFolders().begin(), conObj.refSubFolders().end(), p); } } SyncOperation FileSystemObject::testSyncOperation(SyncDirection testSyncDir) const //semantics: "what if"! assumes "active, no conflict, no recursion (directory)! { - return getIsolatedSyncOperation(!isEmpty<SelectSide::left>(), !isEmpty<SelectSide::right>(), getCategory(), true, testSyncDir, false); + return getIsolatedSyncOperation(*this, true, testSyncDir, false); } +//SyncOperation FolderPair::testSyncOperation() const -> no recursion: we do NOT consider child elements when testing! + SyncOperation FileSystemObject::getSyncOperation() const { - return getIsolatedSyncOperation(!isEmpty<SelectSide::left>(), !isEmpty<SelectSide::right>(), getCategory(), selectedForSync_, getSyncDir(), !syncDirectionConflict_.empty()); + return getIsolatedSyncOperation(*this, selectedForSync_, syncDir_, !syncDirectionConflict_.empty()); //do *not* make a virtual call to testSyncOperation()! See FilePair::testSyncOperation()! <- better not implement one in terms of the other!!! } -//SyncOperation FolderPair::testSyncOperation() const -> no recursion: we do NOT want to consider child elements when testing! - - SyncOperation FolderPair::getSyncOperation() const { if (!syncOpBuffered_) //redetermine... @@ -210,18 +268,18 @@ SyncOperation FolderPair::getSyncOperation() const //action for child elements may occassionally have to overwrite parent task: switch (*syncOpBuffered_) { + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: case SO_MOVE_LEFT_FROM: case SO_MOVE_LEFT_TO: case SO_MOVE_RIGHT_FROM: case SO_MOVE_RIGHT_TO: assert(false); [[fallthrough]]; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_OVERWRITE_LEFT: - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: case SO_EQUAL: break; //take over suggestion, no problem for child-elements case SO_DELETE_LEFT: @@ -232,18 +290,17 @@ SyncOperation FolderPair::getSyncOperation() const { //1. if at least one child-element is to be created, make sure parent folder is created also //note: this automatically fulfills "create parent folders even if excluded" - if (hasDirectChild(*this, - [](const FileSystemObject& fsObj) + if (hasDirectChild(*this, [](const FileSystemObject& fsObj) { - const SyncOperation op = fsObj.getSyncOperation(); - return op == SO_CREATE_NEW_LEFT || + assert(!fsObj.isPairEmpty() || fsObj.getSyncOperation() == SO_DO_NOTHING); + const SyncOperation op = fsObj.getSyncOperation(); + return op == SO_CREATE_LEFT || op == SO_MOVE_LEFT_TO; })) - syncOpBuffered_ = SO_CREATE_NEW_LEFT; + syncOpBuffered_ = SO_CREATE_LEFT; //2. cancel parent deletion if only a single child is not also scheduled for deletion else if (*syncOpBuffered_ == SO_DELETE_RIGHT && - hasDirectChild(*this, - [](const FileSystemObject& fsObj) + hasDirectChild(*this, [](const FileSystemObject& fsObj) { if (fsObj.isPairEmpty()) return false; //fsObj may already be empty because it once contained a "move source" @@ -255,17 +312,16 @@ SyncOperation FolderPair::getSyncOperation() const } else if (isEmpty<SelectSide::right>()) { - if (hasDirectChild(*this, - [](const FileSystemObject& fsObj) + if (hasDirectChild(*this, [](const FileSystemObject& fsObj) { - const SyncOperation op = fsObj.getSyncOperation(); - return op == SO_CREATE_NEW_RIGHT || + assert(!fsObj.isPairEmpty() || fsObj.getSyncOperation() == SO_DO_NOTHING); + const SyncOperation op = fsObj.getSyncOperation(); + return op == SO_CREATE_RIGHT || op == SO_MOVE_RIGHT_TO; })) - syncOpBuffered_ = SO_CREATE_NEW_RIGHT; + syncOpBuffered_ = SO_CREATE_RIGHT; else if (*syncOpBuffered_ == SO_DELETE_LEFT && - hasDirectChild(*this, - [](const FileSystemObject& fsObj) + hasDirectChild(*this, [](const FileSystemObject& fsObj) { if (fsObj.isPairEmpty()) return false; @@ -288,24 +344,26 @@ SyncOperation FilePair::applyMoveOptimization(SyncOperation op) const /* check whether we can optimize "create + delete" via "move": note: as long as we consider "create + delete" cases only, detection of renamed files, should be fine even for "binary" comparison variant! */ if (moveFileRef_) - if (auto refFile = dynamic_cast<const FilePair*>(FileSystemObject::retrieve(moveFileRef_))) //we expect a "FilePair", but only need a "FileSystemObject" here + if (auto refFile = dynamic_cast<const FilePair*>(FileSystemObject::retrieve(moveFileRef_))) + { if (refFile->moveFileRef_ == getId()) //both ends should agree... { const SyncOperation opRef = refFile->FileSystemObject::getSyncOperation(); //do *not* make a virtual call! - - if (op == SO_CREATE_NEW_LEFT && + if (op == SO_CREATE_LEFT && opRef == SO_DELETE_LEFT) op = SO_MOVE_LEFT_TO; else if (op == SO_DELETE_LEFT && - opRef == SO_CREATE_NEW_LEFT) + opRef == SO_CREATE_LEFT) op = SO_MOVE_LEFT_FROM; - else if (op == SO_CREATE_NEW_RIGHT && + else if (op == SO_CREATE_RIGHT && opRef == SO_DELETE_RIGHT) op = SO_MOVE_RIGHT_TO; else if (op == SO_DELETE_RIGHT && - opRef == SO_CREATE_NEW_RIGHT) + opRef == SO_CREATE_RIGHT) op = SO_MOVE_RIGHT_FROM; } + else assert(false); //...and why shouldn't they? + } return op; } @@ -326,9 +384,13 @@ std::wstring fff::getCategoryDescription(CompareFileResult cmpRes) { switch (cmpRes) { - case FILE_LEFT_SIDE_ONLY: + case FILE_EQUAL: + return _("Both sides are equal"); + case FILE_RENAMED: + return _("Items differ in name only"); + case FILE_LEFT_ONLY: return _("Item exists on left side only"); - case FILE_RIGHT_SIDE_ONLY: + case FILE_RIGHT_ONLY: return _("Item exists on right side only"); case FILE_LEFT_NEWER: return _("Left side is newer"); @@ -336,10 +398,7 @@ std::wstring fff::getCategoryDescription(CompareFileResult cmpRes) return _("Right side is newer"); case FILE_DIFFERENT_CONTENT: return _("Items have different content"); - case FILE_EQUAL: - return _("Both sides are equal"); - case FILE_DIFFERENT_METADATA: - return _("Items differ in attributes only"); + case FILE_TIME_INVALID: case FILE_CONFLICT: return _("Conflict/item cannot be categorized"); } @@ -352,20 +411,34 @@ namespace { const wchar_t arrowLeft [] = L"<-"; const wchar_t arrowRight[] = L"->"; +//const wchar_t arrowRight[] = L"\u2192"; unicode arrows -> too small } std::wstring fff::getCategoryDescription(const FileSystemObject& fsObj) { - const std::wstring footer = L"\n[" + utfTo<std::wstring>(fsObj. getItemNameAny()) + L']'; + const std::wstring footer = [&] + { + if (fsObj.hasEquivalentItemNames()) + return L'\n' + fmtPath(fsObj.getItemName<SelectSide::left>()); + else + return std::wstring(L"\n") + + fmtPath(fsObj.getItemName<SelectSide::left >()) + L' ' + arrowLeft + L'\n' + + fmtPath(fsObj.getItemName<SelectSide::right>()) + L' ' + arrowRight; + }(); + + if (const Zstringc descr = fsObj.getCategoryCustomDescription(); + !descr.empty()) + return utfTo<std::wstring>(descr) + footer; const CompareFileResult cmpRes = fsObj.getCategory(); switch (cmpRes) { - case FILE_LEFT_SIDE_ONLY: - case FILE_RIGHT_SIDE_ONLY: - case FILE_DIFFERENT_CONTENT: case FILE_EQUAL: + case FILE_RENAMED: + case FILE_LEFT_ONLY: + case FILE_RIGHT_ONLY: + case FILE_DIFFERENT_CONTENT: return getCategoryDescription(cmpRes) + footer; //use generic description case FILE_LEFT_NEWER: @@ -377,21 +450,22 @@ std::wstring fff::getCategoryDescription(const FileSystemObject& fsObj) [&](const FilePair& file) { descr += std::wstring(L"\n") + - arrowLeft + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::left >()) + L'\n' + - arrowRight + L' ' + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::right>()); + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::left >()) + L' ' + arrowLeft + L'\n' + + formatUtcToLocalTime(file.getLastWriteTime<SelectSide::right>()) + L' ' + arrowRight ; }, [&](const SymlinkPair& symlink) { descr += std::wstring(L"\n") + - arrowLeft + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime<SelectSide::left >()) + L'\n' + - arrowRight + L' ' + formatUtcToLocalTime(symlink.getLastWriteTime<SelectSide::right>()); + formatUtcToLocalTime(symlink.getLastWriteTime<SelectSide::left >()) + L' ' + arrowLeft + L'\n' + + formatUtcToLocalTime(symlink.getLastWriteTime<SelectSide::right>()) + L' ' + arrowRight ; }); return descr + footer; } - case FILE_DIFFERENT_METADATA: + case FILE_TIME_INVALID: case FILE_CONFLICT: - return utfTo<std::wstring>(fsObj.getCatExtraDescription()) + footer; + assert(false); //should have getCategoryDescription()! + return _("Error") + footer; } assert(false); return std::wstring(); @@ -402,9 +476,9 @@ std::wstring fff::getSyncOpDescription(SyncOperation op) { switch (op) { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: return _("Copy new item to left"); - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: return _("Copy new item to right"); case SO_DELETE_LEFT: return _("Delete left item"); @@ -412,10 +486,10 @@ std::wstring fff::getSyncOpDescription(SyncOperation op) return _("Delete right item"); case SO_MOVE_LEFT_FROM: case SO_MOVE_LEFT_TO: - return _("Move file on left"); //move only supported for files + return _("Move left file"); //move only supported for files case SO_MOVE_RIGHT_FROM: case SO_MOVE_RIGHT_TO: - return _("Move file on right"); + return _("Move right file"); case SO_OVERWRITE_LEFT: return _("Update left item"); case SO_OVERWRITE_RIGHT: @@ -424,10 +498,10 @@ std::wstring fff::getSyncOpDescription(SyncOperation op) return _("Do nothing"); case SO_EQUAL: return _("Both sides are equal"); - case SO_COPY_METADATA_TO_LEFT: - return _("Update attributes on left"); - case SO_COPY_METADATA_TO_RIGHT: - return _("Update attributes on right"); + case SO_RENAME_LEFT: + return _("Rename left item"); + case SO_RENAME_RIGHT: + return _("Rename right item"); case SO_UNRESOLVED_CONFLICT: //not used on GUI, but in .csv return _("Conflict/item cannot be categorized"); } @@ -438,37 +512,43 @@ std::wstring fff::getSyncOpDescription(SyncOperation op) std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) { - const std::wstring footer = L"\n[" + utfTo<std::wstring>(fsObj. getItemNameAny()) + L']'; - const SyncOperation op = fsObj.getSyncOperation(); + + auto generateFooter = [&] + { + if (fsObj.hasEquivalentItemNames()) + return L'\n' + fmtPath(fsObj.getItemName<SelectSide::left>()); + + Zstring itemNameNew = fsObj.getItemName<SelectSide::left >(); + Zstring itemNameOld = fsObj.getItemName<SelectSide::right>(); + + if (const SyncDirection dir = getEffectiveSyncDir(op); + dir != SyncDirection::none) + { + if (dir == SyncDirection::left) + std::swap(itemNameNew, itemNameOld); + + return L'\n' + fmtPath(itemNameOld) + L' ' + RIGHT_ARROW_CURV_DOWN + L'\n' + fmtPath(itemNameNew); + } + else + return L'\n' + + fmtPath(itemNameNew) + L' ' + arrowLeft + L'\n' + + fmtPath(itemNameOld) + L' ' + arrowRight; + }; + switch (op) { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: case SO_DELETE_LEFT: case SO_DELETE_RIGHT: case SO_OVERWRITE_LEFT: case SO_OVERWRITE_RIGHT: case SO_DO_NOTHING: case SO_EQUAL: - return getSyncOpDescription(op) + footer; //use generic description - - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: - //harmonize with synchronization.cpp::FolderPairSyncer::synchronizeFileInt, ect!! - { - Zstring itemNameOld = fsObj.getItemName<SelectSide::right>(); - Zstring itemNameNew = fsObj.getItemName<SelectSide::left >(); - if (op == SO_COPY_METADATA_TO_LEFT) - std::swap(itemNameOld, itemNameNew); - - if (getUnicodeNormalForm(itemNameOld) != - getUnicodeNormalForm(itemNameNew)) //detected change in case - return getSyncOpDescription(op) + L'\n' + - fmtPath(itemNameOld) + L' ' + arrowRight + L'\n' + //show short name only - fmtPath(itemNameNew) /*+ footer -> redundant */; - } - return getSyncOpDescription(op) + footer; //fallback + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + return getSyncOpDescription(op) + generateFooter(); case SO_MOVE_LEFT_FROM: case SO_MOVE_LEFT_TO: @@ -484,26 +564,26 @@ std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) if (!isMoveSource) std::swap(fileFrom, fileTo); - auto getRelName = [&](const FileSystemObject& fso, bool leftSide) { return leftSide ? fso.getRelativePath<SelectSide::left>() : fso.getRelativePath<SelectSide::right>(); }; + auto getRelPath = [&](const FileSystemObject& fso) { return onLeft ? fso.getRelativePath<SelectSide::left>() : fso.getRelativePath<SelectSide::right>(); }; - const Zstring relPathFrom = getRelName(*fileFrom, onLeft); - const Zstring relPathTo = getRelName(*fileTo, onLeft); + const Zstring relPathFrom = getRelPath(*fileFrom); + const Zstring relPathTo = getRelPath(*fileTo); - //attention: ::SetWindowText() doesn't handle tab characters correctly in combination with certain file names, so don't use them + //attention: ::SetWindowText() doesn't handle tab characters correctly in combination with certain file names, so don't use return getSyncOpDescription(op) + L'\n' + (beforeLast(relPathFrom, FILE_NAME_SEPARATOR, IfNotFoundReturn::none) == beforeLast(relPathTo, FILE_NAME_SEPARATOR, IfNotFoundReturn::none) ? //detected pure "rename" - fmtPath(getItemName(relPathFrom)) + L' ' + arrowRight + L'\n' + //show short name only + fmtPath(getItemName(relPathFrom)) + L' ' + RIGHT_ARROW_CURV_DOWN + L'\n' + //show file name only fmtPath(getItemName(relPathTo)) : //"move" or "move + rename" - fmtPath(relPathFrom) + L' ' + arrowRight + L'\n' + - fmtPath(relPathTo)) /*+ footer -> redundant */; + fmtPath(relPathFrom) + L' ' + RIGHT_ARROW_CURV_DOWN + L'\n' + + fmtPath(relPathTo)); } break; case SO_UNRESOLVED_CONFLICT: - return fsObj.getSyncOpConflict() + footer; + return fsObj.getSyncOpConflict() + generateFooter(); } assert(false); diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h index cb28b137..040cc0a4 100644 --- a/FreeFileSync/Source/base/file_hierarchy.h +++ b/FreeFileSync/Source/base/file_hierarchy.h @@ -22,73 +22,29 @@ namespace fff { struct FileAttributes { - FileAttributes() {} - FileAttributes(time_t modTimeIn, - uint64_t fileSizeIn, - AFS::FingerPrint filePrintIn, - bool followedSymlink) : - modTime(modTimeIn), - fileSize(fileSizeIn), - filePrint(filePrintIn), - isFollowedSymlink(followedSymlink) - { - static_assert(std::is_signed_v<time_t>, "... and signed!"); - } - time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT uint64_t fileSize = 0; AFS::FingerPrint filePrint = 0; //optional bool isFollowedSymlink = false; + static_assert(std::is_signed_v<time_t>, "we need time_t to be signed"); + std::strong_ordering operator<=>(const FileAttributes&) const = default; }; struct LinkAttributes { - LinkAttributes() {} - explicit LinkAttributes(time_t modTimeIn) : modTime(modTimeIn) {} - time_t modTime = 0; //number of seconds since Jan. 1st 1970 GMT }; struct FolderAttributes { - FolderAttributes() {} - explicit FolderAttributes(bool isSymlink) : - isFollowedSymlink(isSymlink) {} - bool isFollowedSymlink = false; }; -enum class SelectSide -{ - left, - right -}; - - -template <SelectSide side> -constexpr SelectSide getOtherSide = side == SelectSide::left ? SelectSide::right : SelectSide::left; - - -template <SelectSide side, class T> inline -T& selectParam(T& left, T& right) -{ - if constexpr (side == SelectSide::left) - return left; - else - return right; -} - -//------------------------------------------------------------------ - -std::wstring getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR); - -//------------------------------------------------------------------ - struct FolderContainer { //------------------------------------------------------------------ @@ -109,16 +65,12 @@ struct FolderContainer void addFile(const Zstring& itemName, const FileAttributes& attr) { - const auto [it, inserted] = files.emplace(itemName, attr); - if (!inserted) //update entry if already existing (e.g. during folder traverser "retry") - it->second = attr; + files.insert_or_assign(itemName, attr); //update entry if already existing (e.g. during folder traverser "retry") } void addLink(const Zstring& itemName, const LinkAttributes& attr) { - const auto [it, inserted] = symlinks.emplace(itemName, attr); - if (!inserted) - it->second = attr; + symlinks.insert_or_assign(itemName, attr); } FolderContainer& addFolder(const Zstring& itemName, const FolderAttributes& attr) @@ -126,19 +78,82 @@ struct FolderContainer auto& p = folders[itemName]; //value default-construction is okay here p.first = attr; return p.second; - - //const auto [it, inserted] = folders.emplace(itemName, std::pair<FolderAttributes, FolderContainer>(attr, FolderContainer())); - //if (!inserted) - // it->second.first = attr; - //return it->second.second; } }; -class BaseFolderPair; -class FolderPair; -class FilePair; -class SymlinkPair; +//------------------------------------------------------------------ + +enum class SelectSide +{ + left, + right +}; + + +template <SelectSide side> +constexpr SelectSide getOtherSide = side == SelectSide::left ? SelectSide::right : SelectSide::left; + + +template <SelectSide side, class T> inline +T& selectParam(T& left, T& right) +{ + if constexpr (side == SelectSide::left) + return left; + else + return right; +} + + +enum class FileContentCategory : unsigned char +{ + unknown, + equal, + leftNewer, + rightNewer, + invalidTime, + different, + conflict, +}; + + +inline +SyncDirection getEffectiveSyncDir(SyncOperation syncOp) +{ + switch (syncOp) + { + case SO_CREATE_LEFT: + case SO_DELETE_LEFT: + case SO_OVERWRITE_LEFT: + case SO_RENAME_LEFT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + return SyncDirection::left; + + case SO_CREATE_RIGHT: + case SO_DELETE_RIGHT: + case SO_OVERWRITE_RIGHT: + case SO_RENAME_RIGHT: + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + return SyncDirection::right; + + case SO_DO_NOTHING: + case SO_EQUAL: + case SO_UNRESOLVED_CONFLICT: + break; //nothing to do + } + return SyncDirection::none; +} + + +std::wstring getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR); + + class FileSystemObject; +class SymlinkPair; +class FilePair; +class FolderPair; +class BaseFolderPair; /*------------------------------------------------------------------ inheritance diagram: @@ -161,7 +176,6 @@ struct PathInformation //diamond-shaped inheritance! template <SelectSide side> AbstractPath getAbstractPath() const; template <SelectSide side> Zstring getRelativePath() const; //get path relative to base sync dir (without leading/trailing FILE_NAME_SEPARATOR) - Zstring getRelativePathAny() const { return getRelativePathL(); } //side doesn't matter private: virtual AbstractPath getAbstractPathL() const = 0; //implemented by FileSystemObject + BaseFolderPair @@ -181,8 +195,7 @@ template <> inline Zstring PathInformation::getRelativePath<SelectSide::right>() class ContainerObject : public virtual PathInformation { - friend class FolderPair; - friend class FileSystemObject; + friend class FileSystemObject; //access to updateRelPathsRecursion() public: using FileList = std::list<FilePair>; //MergeSides::execute() requires a structure that doesn't invalidate pointers after push_back() @@ -191,7 +204,6 @@ public: FolderPair& addFolder(const Zstring& itemNameL, //file exists on both sides const FolderAttributes& left, - CompareDirResult defaultCmpResult, const Zstring& itemNameR, const FolderAttributes& right); @@ -201,7 +213,6 @@ public: FilePair& addFile(const Zstring& itemNameL, //file exists on both sides const FileAttributes& left, - CompareFileResult defaultCmpResult, const Zstring& itemNameR, const FileAttributes& right); @@ -211,7 +222,6 @@ public: SymlinkPair& addLink(const Zstring& itemNameL, //link exists on both sides const LinkAttributes& left, - CompareSymlinkResult defaultCmpResult, const Zstring& itemNameR, const LinkAttributes& right); @@ -231,17 +241,19 @@ public: const BaseFolderPair& getBase() const { return base_; } /**/ BaseFolderPair& getBase() { return base_; } -protected: - ContainerObject(BaseFolderPair& baseFolder) : //used during BaseFolderPair constructor - base_(baseFolder) {} //take reference only: baseFolder *not yet* fully constructed at this point! + void removeDoubleEmpty(); //remove all invalid entries (where both sides are empty) recursively - ContainerObject(const FileSystemObject& fsAlias); //used during FolderPair constructor + virtual void flip(); - virtual ~ContainerObject() {} //don't need polymorphic deletion, but we have a vtable anyway +protected: + explicit ContainerObject(BaseFolderPair& baseFolder) : //used during BaseFolderPair constructor + base_(baseFolder) //take reference only: baseFolder *not yet* fully constructed at this point! + { assert(relPathL_.c_str() == relPathR_.c_str()); } //expected by the following contructor! - virtual void flip(); + explicit ContainerObject(const FileSystemObject& fsAlias); //used during FolderPair constructor - void removeEmptyRec(); + virtual ~ContainerObject() //don't need polymorphic deletion, but we have a vtable anyway + { assert(relPathL_.c_str() == relPathR_.c_str() || relPathL_ != relPathR_); } template <SelectSide side> void updateRelPathsRecursion(const FileSystemObject& fsAlias); @@ -250,8 +262,6 @@ private: ContainerObject (const ContainerObject&) = delete; //this class is referenced by its child elements => make it non-copyable/movable! ContainerObject& operator=(const ContainerObject&) = delete; - virtual void notifySyncCfgChanged() {} - Zstring getRelativePathL() const override { return relPathL_; } Zstring getRelativePathR() const override { return relPathR_; } @@ -260,7 +270,7 @@ private: FolderList subFolders_; Zstring relPathL_; //path relative to base sync dir (without leading/trailing FILE_NAME_SEPARATOR) - Zstring relPathR_; // + Zstring relPathR_; //class invariant: shared Zstring iff equal! BaseFolderPair& base_; }; @@ -274,7 +284,7 @@ enum class BaseFolderStatus failure, }; -class BaseFolderPair : public ContainerObject //synchronization base directory +class BaseFolderPair : public ContainerObject { public: BaseFolderPair(const AbstractPath& folderPathLeft, @@ -292,8 +302,6 @@ public: folderPathLeft_(folderPathLeft), folderPathRight_(folderPathRight) {} - static void removeEmpty(BaseFolderPair& baseFolder) { baseFolder.removeEmptyRec(); } //physically remove all invalid entries (where both sides are empty) recursively - template <SelectSide side> BaseFolderStatus getFolderStatus() const; //base folder status at the time of comparison! template <SelectSide side> void setFolderStatus(BaseFolderStatus value); //update after creating the directory in FFS @@ -410,15 +418,18 @@ public: template <SelectSide side> bool isEmpty() const; //path getters always return valid values, even if isEmpty<side>()! - Zstring getItemNameAny() const; //like getItemName() but without bias to which side is returned template <SelectSide side> Zstring getItemName() const; //case sensitive! + bool hasEquivalentItemNames() const; //*quick* check if left/right names are equivalent when ignoring Unicode normalization forms + + //for use during compare() only: + virtual void setCategoryConflict(const Zstringc& description) = 0; + //comparison result - CompareFileResult getCategory() const { return cmpResult_; } - Zstringc getCatExtraDescription() const; //only filled if getCategory() == FILE_CONFLICT or FILE_DIFFERENT_METADATA + virtual CompareFileResult getCategory() const = 0; + virtual Zstringc getCategoryCustomDescription() const = 0; //optional //sync settings - SyncDirection getSyncDir() const { return syncDir_; } void setSyncDir(SyncDirection newDir); void setSyncDirConflict(const Zstringc& description); //set syncDir = SyncDirection::none + fill conflict description @@ -426,7 +437,7 @@ public: void setActive(bool active); //sync operation - virtual SyncOperation testSyncOperation(SyncDirection testSyncDir) const; //semantics: "what if"! assumes "active, no conflict, no recursion (directory)! + virtual SyncOperation testSyncOperation(SyncDirection testSyncDir) const; //"what if" semantics! assumes "active, no conflict, no recursion (directory)! virtual SyncOperation getSyncOperation() const; std::wstring getSyncOpConflict() const; //return conflict when determining sync direction or (still unresolved) conflict during categorization @@ -434,35 +445,36 @@ public: const ContainerObject& parent() const { return parent_; } /**/ ContainerObject& parent() { return parent_; } - const BaseFolderPair& base() const { return parent_.getBase(); } - /**/ BaseFolderPair& base() { return parent_.getBase(); } + const BaseFolderPair& base() const { return parent_.getBase(); } + /**/ BaseFolderPair& base() { return parent_.getBase(); } - //for use during init in "CompareProcess" only: - template <CompareFileResult res> void setCategory(); - void setCategoryConflict (const Zstringc& description); - void setCategoryDiffMetadata(const Zstringc& description); + bool passFileFilter(const PathFilter& filter) const; //optimized for perf! + + virtual void flip(); + + template <SelectSide side> + void setItemName(const Zstring& itemName); protected: FileSystemObject(const Zstring& itemNameL, const Zstring& itemNameR, - ContainerObject& parentObj, - CompareFileResult defaultCmpResult) : - cmpResult_(defaultCmpResult), + ContainerObject& parentObj) : itemNameL_(itemNameL), itemNameR_(itemNameL == itemNameR ? itemNameL : itemNameR), //perf: no measurable speed drawback; -3% peak memory => further needed by ContainerObject construction! parent_(parentObj) { assert(itemNameL_.c_str() == itemNameR_.c_str() || itemNameL_ != itemNameR_); //also checks ref-counted string precondition - parent_.notifySyncCfgChanged(); + FileSystemObject::notifySyncCfgChanged(); //non-virtual call! (=> anyway in a constructor!) } - virtual ~FileSystemObject() {} //don't need polymorphic deletion, but we have a vtable anyway - //must not call parent here, it is already partially destroyed and nothing more than a pure ContainerObject! - - virtual void flip(); - virtual void notifySyncCfgChanged() { parent().notifySyncCfgChanged(); /*propagate!*/ } + virtual ~FileSystemObject() //don't need polymorphic deletion, but we have a vtable anyway + { assert(itemNameL_.c_str() == itemNameR_.c_str() || itemNameL_ != itemNameR_); } - void setSynced(const Zstring& itemName); + virtual void notifySyncCfgChanged() + { + if (auto fsParent = dynamic_cast<FileSystemObject*>(&parent_)) + fsParent->notifySyncCfgChanged(); //propagate! + } private: FileSystemObject (const FileSystemObject&) = delete; @@ -475,22 +487,16 @@ private: virtual void removeObjectR() = 0; template <SelectSide side> - void propagateChangedItemName(const Zstring& itemNameOld); //required after any itemName changes - - //categorization - Zstringc cmpResultDescr_; //only filled if getCategory() == FILE_CONFLICT or FILE_DIFFERENT_METADATA - //conserve memory (avoid std::string SSO overhead + allow ref-counting!) - CompareFileResult cmpResult_; //although this uses 4 bytes there is currently *no* space wasted in class layout! + void propagateChangedItemName(); //required after any itemName changes bool selectedForSync_ = true; - //Note: we model *four* states with following two variables => "syncDirectionConflict is empty or syncDir == NONE" is a class invariant!!! - SyncDirection syncDir_ = SyncDirection::none; //1 byte: optimize memory layout! + SyncDirection syncDir_ = SyncDirection::none; Zstringc syncDirectionConflict_; //non-empty if we have a conflict setting sync-direction //conserve memory (avoid std::string SSO overhead + allow ref-counting!) - Zstring itemNameL_; //slightly redundant under Linux, but on Windows the "same" file paths can differ in case - Zstring itemNameR_; //use as indicator: an empty name means: not existing on this side! + Zstring itemNameL_; //use as indicator: empty means "not existing on this side" + Zstring itemNameR_; //class invariant: shared Zstring iff equal! ContainerObject& parent_; }; @@ -500,20 +506,18 @@ private: class FolderPair : public FileSystemObject, public ContainerObject { - friend class ContainerObject; - public: void accept(FSObjectVisitor& visitor) const override; - CompareDirResult getDirCategory() const; //returns actually used subset of CompareFileResult + CompareFileResult getCategory() const override; + CompareDirResult getDirCategory() const { return static_cast<CompareDirResult>(getCategory()); } FolderPair(const Zstring& itemNameL, //use empty itemName if "not existing" const FolderAttributes& attrL, - CompareDirResult defaultCmpResult, const Zstring& itemNameR, const FolderAttributes& attrR, ContainerObject& parentObj) : - FileSystemObject(itemNameL, itemNameR, parentObj, static_cast<CompareFileResult>(defaultCmpResult)), + FileSystemObject(itemNameL, itemNameR, parentObj), ContainerObject(static_cast<FileSystemObject&>(*this)), //FileSystemObject fully constructed at this point! attrL_(attrL), attrR_(attrR) {} @@ -523,18 +527,26 @@ public: SyncOperation getSyncOperation() const override; template <SelectSide sideTrg> - void setSyncedTo(const Zstring& itemName, bool isSymlinkTrg, bool isSymlinkSrc); //call after sync, sets DIR_EQUAL + void setSyncedTo(bool isSymlinkTrg, bool isSymlinkSrc); //call after successful sync + + bool passDirFilter(const PathFilter& filter, bool* childItemMightMatch) const; //optimized for perf! + + void flip() override; + + void setCategoryConflict(const Zstringc& description) override; + Zstringc getCategoryCustomDescription() const override; //optional private: - void flip () override; void removeObjectL() override; void removeObjectR() override; - void notifySyncCfgChanged() override { syncOpBuffered_ = {}; FileSystemObject::notifySyncCfgChanged(); ContainerObject::notifySyncCfgChanged(); } + void notifySyncCfgChanged() override { syncOpBuffered_ = {}; FileSystemObject::notifySyncCfgChanged(); } mutable std::optional<SyncOperation> syncOpBuffered_; //determining sync-op for directory may be expensive as it depends on child-objects => buffer FolderAttributes attrL_; FolderAttributes attrR_; + + Zstringc categoryConflict_; //conserve memory (avoid std::string SSO overhead + allow ref-counting! }; @@ -542,21 +554,20 @@ private: class FilePair : public FileSystemObject { - friend class ContainerObject; //construction - public: void accept(FSObjectVisitor& visitor) const override; FilePair(const Zstring& itemNameL, //use empty string if "not existing" const FileAttributes& attrL, - CompareFileResult defaultCmpResult, const Zstring& itemNameR, // const FileAttributes& attrR, ContainerObject& parentObj) : - FileSystemObject(itemNameL, itemNameR, parentObj, defaultCmpResult), + FileSystemObject(itemNameL, itemNameR, parentObj), attrL_(attrL), attrR_(attrR) {} + CompareFileResult getCategory() const override; + template <SelectSide side> time_t getLastWriteTime() const; template <SelectSide side> uint64_t getFileSize() const; template <SelectSide side> bool isFollowedSymlink() const; @@ -565,22 +576,28 @@ public: template <SelectSide side> void clearFilePrint(); void setMoveRef(ObjectId refId) { moveFileRef_ = refId; } //reference to corresponding renamed file - ObjectId getMoveRef() const { return moveFileRef_; } //may be nullptr - - CompareFileResult getFileCategory() const; + ObjectId getMoveRef() const { assert(!moveFileRef_ || (isEmpty<SelectSide::left>() != isEmpty<SelectSide::right>())); return moveFileRef_; } //may be nullptr SyncOperation testSyncOperation(SyncDirection testSyncDir) const override; //semantics: "what if"! assumes "active, no conflict, no recursion (directory)! SyncOperation getSyncOperation() const override; template <SelectSide sideTrg> - void setSyncedTo(const Zstring& itemName, //call after sync, sets FILE_EQUAL - uint64_t fileSize, + void setSyncedTo(uint64_t fileSize, int64_t lastWriteTimeTrg, int64_t lastWriteTimeSrc, AFS::FingerPrint filePrintTrg, AFS::FingerPrint filePrintSrc, bool isSymlinkTrg, - bool isSymlinkSrc); + bool isSymlinkSrc); //call after successful sync + + void flip() override; + + void setCategoryConflict(const Zstringc& description) override; + void setCategoryInvalidTime(const Zstringc& description); + Zstringc getCategoryCustomDescription() const override; //optional + + void setContentCategory(FileContentCategory category); + FileContentCategory getContentCategory() const; private: Zstring getRelativePathL() const override { return appendPath(parent().getRelativePath<SelectSide::left >(), getItemName<SelectSide::left >()); } @@ -588,65 +605,75 @@ private: SyncOperation applyMoveOptimization(SyncOperation op) const; - void flip () override; - void removeObjectL() override { attrL_ = FileAttributes(); } - void removeObjectR() override { attrR_ = FileAttributes(); } + void removeObjectL() override { attrL_ = FileAttributes(); contentCategory_ = FileContentCategory::unknown; } + void removeObjectR() override { attrR_ = FileAttributes(); contentCategory_ = FileContentCategory::unknown; } FileAttributes attrL_; FileAttributes attrR_; ObjectId moveFileRef_ = nullptr; //optional, filled by redetermineSyncDirection() + + FileContentCategory contentCategory_ = FileContentCategory::unknown; + Zstringc categoryDescr_; //optional: custom category description (e.g. FileContentCategory::conflict or invalidTime) }; //------------------------------------------------------------------ class SymlinkPair : public FileSystemObject //this class models a TRUE symbolic link, i.e. one that is NEVER dereferenced: deref-links should be directly placed in class File/FolderPair { - friend class ContainerObject; //construction - public: void accept(FSObjectVisitor& visitor) const override; - template <SelectSide side> time_t getLastWriteTime() const; //write time of the link, NOT target! - - CompareSymlinkResult getLinkCategory() const; //returns actually used subset of CompareFileResult - - SymlinkPair(const Zstring& itemNameL, //use empty string if "not existing" - const LinkAttributes& attrL, - CompareSymlinkResult defaultCmpResult, - const Zstring& itemNameR, //use empty string if "not existing" - const LinkAttributes& attrR, + SymlinkPair(const Zstring& itemNameL, //use empty string if "not existing" + const LinkAttributes& attrL, + const Zstring& itemNameR, //use empty string if "not existing" + const LinkAttributes& attrR, ContainerObject& parentObj) : - FileSystemObject(itemNameL, itemNameR, parentObj, static_cast<CompareFileResult>(defaultCmpResult)), + FileSystemObject(itemNameL, itemNameR, parentObj), attrL_(attrL), attrR_(attrR) {} + CompareFileResult getCategory() const override; + CompareSymlinkResult getLinkCategory() const { return static_cast<CompareSymlinkResult>(getCategory()); } + + template <SelectSide side> time_t getLastWriteTime() const; //write time of the link, NOT target! + template <SelectSide sideTrg> - void setSyncedTo(const Zstring& itemName, //call after sync, sets SYMLINK_EQUAL - int64_t lastWriteTimeTrg, - int64_t lastWriteTimeSrc); + void setSyncedTo(int64_t lastWriteTimeTrg, + int64_t lastWriteTimeSrc); //call after successful sync + + void flip() override; + + void setCategoryConflict(const Zstringc& description) override; + void setCategoryInvalidTime(const Zstringc& description); + Zstringc getCategoryCustomDescription() const override; //optional + + void setContentCategory(FileContentCategory category); + FileContentCategory getContentCategory() const; private: Zstring getRelativePathL() const override { return appendPath(parent().getRelativePath<SelectSide::left >(), getItemName<SelectSide::left >()); } Zstring getRelativePathR() const override { return appendPath(parent().getRelativePath<SelectSide::right>(), getItemName<SelectSide::right>()); } - void flip() override; - void removeObjectL() override { attrL_ = LinkAttributes(); } - void removeObjectR() override { attrR_ = LinkAttributes(); } + void removeObjectL() override { attrL_ = LinkAttributes(); contentCategory_ = FileContentCategory::unknown; } + void removeObjectR() override { attrR_ = LinkAttributes(); contentCategory_ = FileContentCategory::unknown; } LinkAttributes attrL_; LinkAttributes attrR_; + + FileContentCategory contentCategory_ = FileContentCategory::unknown; + Zstringc categoryDescr_; //optional: custom category description (e.g. FileContentCategory::conflict or invalidTime) }; //------------------------------------------------------------------ -//generic type descriptions (usecase CSV legend, sync config) +//generic descriptions (usecase CSV legend, sync config) std::wstring getCategoryDescription(CompareFileResult cmpRes); -std::wstring getSyncOpDescription (SyncOperation op); +std::wstring getSyncOpDescription(SyncOperation op); -//item-specific type descriptions +//item-specific descriptions std::wstring getCategoryDescription(const FileSystemObject& fsObj); -std::wstring getSyncOpDescription (const FileSystemObject& fsObj); +std::wstring getSyncOpDescription(const FileSystemObject& fsObj); //------------------------------------------------------------------ @@ -688,15 +715,17 @@ public: RecursiveObjectVisitor(Function1 onFolder, Function2 onFile, Function3 onSymlink) : //unifying assignment - onFolder_(std::move(onFolder)), onFile_(std::move(onFile)), onSymlink_(std::move(onSymlink)) {} + onFolder_ (std::move(onFolder)), + onFile_ (std::move(onFile)), + onSymlink_(std::move(onSymlink)) {} - void execute(ContainerObject& hierObj) + void execute(ContainerObject& conObj) { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) onFile_(file); - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) onSymlink_(symlink); - for (FolderPair& subFolder : hierObj.refSubFolders()) + for (FolderPair& subFolder : conObj.refSubFolders()) { onFolder_(subFolder); execute(subFolder); @@ -714,12 +743,12 @@ private: } template <class Function1, class Function2, class Function3> inline -void visitFSObjectRecursively(ContainerObject& hierObj, //consider contained items only +void visitFSObjectRecursively(ContainerObject& conObj, //consider contained items only Function1 onFolder, Function2 onFile, Function3 onSymlink) { - impl::RecursiveObjectVisitor(onFolder, onFile, onSymlink).execute(hierObj); + impl::RecursiveObjectVisitor(onFolder, onFile, onSymlink).execute(conObj); } template <class Function1, class Function2, class Function3> inline @@ -757,34 +786,12 @@ void visitFSObjectRecursively(FileSystemObject& fsObj, //consider item and conta //--------------------- implementation ------------------------------------------ //inline virtual... admittedly its use may be limited -inline void FilePair ::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } inline void FolderPair ::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } +inline void FilePair ::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } inline void SymlinkPair::accept(FSObjectVisitor& visitor) const { visitor.visit(*this); } inline -CompareFileResult FilePair::getFileCategory() const -{ - return getCategory(); -} - - -inline -CompareDirResult FolderPair::getDirCategory() const -{ - return static_cast<CompareDirResult>(getCategory()); -} - - -inline -Zstringc FileSystemObject::getCatExtraDescription() const -{ - assert(getCategory() == FILE_CONFLICT || getCategory() == FILE_DIFFERENT_METADATA); - return cmpResultDescr_; -} - - -inline void FileSystemObject::setSyncDir(SyncDirection newDir) { syncDir_ = newDir; @@ -838,7 +845,7 @@ bool FileSystemObject::isPairEmpty() const template <SelectSide side> inline Zstring FileSystemObject::getItemName() const { - //assert(!itemNameL_.empty() || !itemNameR_.empty()); //-> file pair might be temporarily empty (until permanently removed after sync) + //assert(!itemNameL_.empty() || !itemNameR_.empty()); //-> file pair might be temporarily empty (until removed after sync) const Zstring& itemName = selectParam<side>(itemNameL_, itemNameR_); //empty if not existing if (!itemName.empty()) //avoid ternary-WTF! (implicit copy-constructor call!!!!!!) @@ -848,130 +855,93 @@ Zstring FileSystemObject::getItemName() const inline -Zstring FileSystemObject::getItemNameAny() const +bool FileSystemObject::hasEquivalentItemNames() const { - return getItemName<SelectSide::left>(); //side doesn't matter -} - - -template <> inline -void FileSystemObject::removeObject<SelectSide::left>() -{ - const Zstring itemNameOld = getItemName<SelectSide::left>(); - - cmpResult_ = isEmpty<SelectSide::right>() ? FILE_EQUAL : FILE_RIGHT_SIDE_ONLY; - itemNameL_.clear(); - removeObjectL(); + if (itemNameL_.c_str() == itemNameR_.c_str() || //most likely case + itemNameL_.empty() || itemNameR_.empty()) // + return true; - setSyncDir(SyncDirection::none); //calls notifySyncCfgChanged() - propagateChangedItemName<SelectSide::left>(itemNameOld); + assert(itemNameL_ != itemNameR_); //class invariant + return getUnicodeNormalForm(itemNameL_) == getUnicodeNormalForm(itemNameR_); } -template <> inline -void FileSystemObject::removeObject<SelectSide::right>() +template <SelectSide side> inline +void FileSystemObject::removeObject() { - const Zstring itemNameOld = getItemName<SelectSide::right>(); + if (isEmpty<getOtherSide<side>>()) + itemNameL_ = itemNameR_ = Zstring(); //ensure (c_str) class invariant! + else + selectParam<side>(itemNameL_, itemNameR_).clear(); + + if constexpr (side == SelectSide::left) + removeObjectL(); + else + removeObjectR(); - cmpResult_ = isEmpty<SelectSide::left>() ? FILE_EQUAL : FILE_LEFT_SIDE_ONLY; - itemNameR_.clear(); - removeObjectR(); + if (isPairEmpty()) + setSyncDir(SyncDirection::none); //calls notifySyncCfgChanged() + else //keep current syncDir_ + notifySyncCfgChanged(); //needed!? - setSyncDir(SyncDirection::none); //calls notifySyncCfgChanged() - propagateChangedItemName<SelectSide::right>(itemNameOld); + propagateChangedItemName<side>(); } -inline -void FileSystemObject::setSynced(const Zstring& itemName) +template <SelectSide side> inline +void FileSystemObject::setItemName(const Zstring& itemName) { - const Zstring itemNameOldL = getItemName<SelectSide::left>(); - const Zstring itemNameOldR = getItemName<SelectSide::right>(); - + assert(!itemName.empty()); assert(!isPairEmpty()); - itemNameR_ = itemNameL_ = itemName; - cmpResult_ = FILE_EQUAL; - setSyncDir(SyncDirection::none); - propagateChangedItemName<SelectSide::left >(itemNameOldL); - propagateChangedItemName<SelectSide::right>(itemNameOldR); -} + selectParam<side>(itemNameL_, itemNameR_) = itemName; + if (itemNameL_.c_str() != itemNameR_.c_str() && + itemNameL_ == itemNameR_) + itemNameL_ = itemNameR_; //preserve class invariant + assert(itemNameL_.c_str() == itemNameR_.c_str() || itemNameL_ != itemNameR_); -template <CompareFileResult res> inline -void FileSystemObject::setCategory() -{ - cmpResult_ = res; + propagateChangedItemName<side>(); } -template <> void FileSystemObject::setCategory<FILE_CONFLICT> () = delete; // -template <> void FileSystemObject::setCategory<FILE_DIFFERENT_METADATA>() = delete; //deny use -template <> void FileSystemObject::setCategory<FILE_LEFT_SIDE_ONLY> () = delete; // -template <> void FileSystemObject::setCategory<FILE_RIGHT_SIDE_ONLY> () = delete; // -inline -void FileSystemObject::setCategoryConflict(const Zstringc& description) -{ - assert(!description.empty()); - cmpResult_ = FILE_CONFLICT; - cmpResultDescr_ = description; -} -inline -void FileSystemObject::setCategoryDiffMetadata(const Zstringc& description) -{ - assert(!description.empty()); - cmpResult_ = FILE_DIFFERENT_METADATA; - cmpResultDescr_ = description; -} - -inline -void FileSystemObject::flip() +template <SelectSide side> inline +void FileSystemObject::propagateChangedItemName() { - std::swap(itemNameL_, itemNameR_); + if (itemNameL_.empty() && itemNameR_.empty()) return; //both sides might just have been deleted by removeObject<> - switch (cmpResult_) + if (auto conObj = dynamic_cast<ContainerObject*>(this)) { - case FILE_LEFT_SIDE_ONLY: - cmpResult_ = FILE_RIGHT_SIDE_ONLY; - break; - case FILE_RIGHT_SIDE_ONLY: - cmpResult_ = FILE_LEFT_SIDE_ONLY; - break; - case FILE_LEFT_NEWER: - cmpResult_ = FILE_RIGHT_NEWER; - break; - case FILE_RIGHT_NEWER: - cmpResult_ = FILE_LEFT_NEWER; - break; - case FILE_DIFFERENT_CONTENT: - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: - case FILE_CONFLICT: - break; + const Zstring& itemNameOld = zen::getItemName(conObj->getRelativePath<side>()); + if (itemNameOld != getItemName<side>()) //perf: premature optimization? + conObj->updateRelPathsRecursion<side>(*this); } - - notifySyncCfgChanged(); } template <SelectSide side> inline -void FileSystemObject::propagateChangedItemName(const Zstring& itemNameOld) +void ContainerObject::updateRelPathsRecursion(const FileSystemObject& fsAlias) { - if (itemNameL_.empty() && itemNameR_.empty()) return; //both sides might just have been deleted by removeObject<> - - if (itemNameOld != getItemName<side>()) //perf: premature optimization? - if (auto hierObj = dynamic_cast<ContainerObject*>(this)) - hierObj->updateRelPathsRecursion<side>(*this); -} + //perf: only call if actual item name changed: + assert(selectParam<side>(relPathL_, relPathR_) != appendPath(fsAlias.parent().getRelativePath<side>(), fsAlias.getItemName<side>())); + constexpr SelectSide otherSide = getOtherSide<side>; -template <SelectSide side> inline -void ContainerObject::updateRelPathsRecursion(const FileSystemObject& fsAlias) -{ - assert(selectParam<side>(relPathL_, relPathR_) != //perf: only call if actual item name changed! - appendPath(fsAlias.parent().getRelativePath<side>(), fsAlias.getItemName<side>())); + if (fsAlias.isEmpty<otherSide>()) //=> 1. other side's relPath also needs updating! 2. both sides have same name + selectParam<otherSide>(relPathL_, relPathR_) = appendPath(selectParam<otherSide>(fsAlias.parent().relPathL_, + fsAlias.parent().relPathR_), fsAlias.getItemName<otherSide>()); + else //assume relPath on other side is up to date! + assert(selectParam<otherSide>(relPathL_, relPathR_) == appendPath(fsAlias.parent().getRelativePath<otherSide>(), fsAlias.getItemName<otherSide>())); - selectParam<side>(relPathL_, relPathR_) = appendPath(fsAlias.parent().getRelativePath<side>(), fsAlias.getItemName<side>()); + if (fsAlias.parent().relPathL_.c_str() == // + fsAlias.parent().relPathR_.c_str() && //see ContainerObject constructor and setItemName() + fsAlias.getItemName<SelectSide::left >().c_str() == // + fsAlias.getItemName<SelectSide::right>().c_str()) // + selectParam<side>(relPathL_, relPathR_) = selectParam<otherSide>(relPathL_, relPathR_); + else + selectParam<side>(relPathL_, relPathR_) = appendPath(selectParam<side>(fsAlias.parent().relPathL_, + fsAlias.parent().relPathR_), fsAlias.getItemName<side>()); + assert(relPathL_.c_str() == relPathR_.c_str() || relPathL_ != relPathR_); for (FolderPair& folder : subFolders_) folder.updateRelPathsRecursion<side>(folder); @@ -981,13 +951,12 @@ void ContainerObject::updateRelPathsRecursion(const FileSystemObject& fsAlias) inline ContainerObject::ContainerObject(const FileSystemObject& fsAlias) : relPathL_(appendPath(fsAlias.parent().relPathL_, fsAlias.getItemName<SelectSide::left>())), - relPathR_( - fsAlias.parent().relPathL_.c_str() == // - fsAlias.parent().relPathR_.c_str() && //take advantage of FileSystemObject's Zstring reuse: - fsAlias.getItemName<SelectSide::left >().c_str() == //=> perf: 12% faster merge phase; -4% peak memory - fsAlias.getItemName<SelectSide::right>().c_str() ? // - relPathL_ : //ternary-WTF! (implicit copy-constructor call!!) => no big deal for a Zstring - appendPath(fsAlias.parent().relPathR_, fsAlias.getItemName<SelectSide::right>())), + relPathR_(fsAlias.parent().relPathL_.c_str() == // + fsAlias.parent().relPathR_.c_str() && //take advantage of FileSystemObject's Zstring reuse: + fsAlias.getItemName<SelectSide::left >().c_str() == //=> perf: 12% faster merge phase; -4% peak memory + fsAlias.getItemName<SelectSide::right>().c_str() ? // + relPathL_ : //ternary-WTF! (implicit copy-constructor call!!) => no big deal for a Zstring + appendPath(fsAlias.parent().relPathR_, fsAlias.getItemName<SelectSide::right>())), base_(fsAlias.parent().base_) { assert(relPathL_.c_str() == relPathR_.c_str() || relPathL_ != relPathR_); @@ -995,27 +964,12 @@ ContainerObject::ContainerObject(const FileSystemObject& fsAlias) : inline -void ContainerObject::flip() -{ - for (FilePair& file : refSubFiles()) - file.flip(); - for (SymlinkPair& symlink : refSubLinks()) - symlink.flip(); - for (FolderPair& folder : refSubFolders()) - folder.flip(); - - std::swap(relPathL_, relPathR_); -} - - -inline FolderPair& ContainerObject::addFolder(const Zstring& itemNameL, const FolderAttributes& left, - CompareDirResult defaultCmpResult, const Zstring& itemNameR, const FolderAttributes& right) { - subFolders_.emplace_back(itemNameL, left, defaultCmpResult, itemNameR, right, *this); + subFolders_.emplace_back(itemNameL, left, itemNameR, right, *this); return subFolders_.back(); } @@ -1023,7 +977,7 @@ FolderPair& ContainerObject::addFolder(const Zstring& itemNameL, template <> inline FolderPair& ContainerObject::addFolder<SelectSide::left>(const Zstring& itemName, const FolderAttributes& attr) { - subFolders_.emplace_back(itemName, attr, DIR_LEFT_SIDE_ONLY, Zstring(), FolderAttributes(), *this); + subFolders_.emplace_back(itemName, attr, Zstring(), FolderAttributes(), *this); return subFolders_.back(); } @@ -1031,19 +985,18 @@ FolderPair& ContainerObject::addFolder<SelectSide::left>(const Zstring& itemName template <> inline FolderPair& ContainerObject::addFolder<SelectSide::right>(const Zstring& itemName, const FolderAttributes& attr) { - subFolders_.emplace_back(Zstring(), FolderAttributes(), DIR_RIGHT_SIDE_ONLY, itemName, attr, *this); + subFolders_.emplace_back(Zstring(), FolderAttributes(), itemName, attr, *this); return subFolders_.back(); } inline FilePair& ContainerObject::addFile(const Zstring& itemNameL, - const FileAttributes& left, //file exists on both sides - CompareFileResult defaultCmpResult, + const FileAttributes& left, const Zstring& itemNameR, - const FileAttributes& right) + const FileAttributes& right) //file exists on both sides { - subFiles_.emplace_back(itemNameL, left, defaultCmpResult, itemNameR, right, *this); + subFiles_.emplace_back(itemNameL, left, itemNameR, right, *this); return subFiles_.back(); } @@ -1051,7 +1004,7 @@ FilePair& ContainerObject::addFile(const Zstring& itemNameL, template <> inline FilePair& ContainerObject::addFile<SelectSide::left>(const Zstring& itemName, const FileAttributes& attr) { - subFiles_.emplace_back(itemName, attr, FILE_LEFT_SIDE_ONLY, Zstring(), FileAttributes(), *this); + subFiles_.emplace_back(itemName, attr, Zstring(), FileAttributes(), *this); return subFiles_.back(); } @@ -1059,19 +1012,18 @@ FilePair& ContainerObject::addFile<SelectSide::left>(const Zstring& itemName, co template <> inline FilePair& ContainerObject::addFile<SelectSide::right>(const Zstring& itemName, const FileAttributes& attr) { - subFiles_.emplace_back(Zstring(), FileAttributes(), FILE_RIGHT_SIDE_ONLY, itemName, attr, *this); + subFiles_.emplace_back(Zstring(), FileAttributes(), itemName, attr, *this); return subFiles_.back(); } inline SymlinkPair& ContainerObject::addLink(const Zstring& itemNameL, - const LinkAttributes& left, //link exists on both sides - CompareSymlinkResult defaultCmpResult, + const LinkAttributes& left, const Zstring& itemNameR, - const LinkAttributes& right) + const LinkAttributes& right) //link exists on both sides { - subLinks_.emplace_back(itemNameL, left, defaultCmpResult, itemNameR, right, *this); + subLinks_.emplace_back(itemNameL, left, itemNameR, right, *this); return subLinks_.back(); } @@ -1079,7 +1031,7 @@ SymlinkPair& ContainerObject::addLink(const Zstring& itemNameL, template <> inline SymlinkPair& ContainerObject::addLink<SelectSide::left>(const Zstring& itemName, const LinkAttributes& attr) { - subLinks_.emplace_back(itemName, attr, SYMLINK_LEFT_SIDE_ONLY, Zstring(), LinkAttributes(), *this); + subLinks_.emplace_back(itemName, attr, Zstring(), LinkAttributes(), *this); return subLinks_.back(); } @@ -1087,12 +1039,34 @@ SymlinkPair& ContainerObject::addLink<SelectSide::left>(const Zstring& itemName, template <> inline SymlinkPair& ContainerObject::addLink<SelectSide::right>(const Zstring& itemName, const LinkAttributes& attr) { - subLinks_.emplace_back(Zstring(), LinkAttributes(), SYMLINK_RIGHT_SIDE_ONLY, itemName, attr, *this); + subLinks_.emplace_back(Zstring(), LinkAttributes(), itemName, attr, *this); return subLinks_.back(); } inline +void FileSystemObject::flip() +{ + std::swap(itemNameL_, itemNameR_); + notifySyncCfgChanged(); +} + + +inline +void ContainerObject::flip() +{ + for (FilePair& file : refSubFiles()) + file.flip(); + for (SymlinkPair& symlink : refSubLinks()) + symlink.flip(); + for (FolderPair& folder : refSubFolders()) + folder.flip(); + + std::swap(relPathL_, relPathR_); +} + + +inline void BaseFolderPair::flip() { ContainerObject::flip(); @@ -1102,7 +1076,7 @@ void BaseFolderPair::flip() inline -void FolderPair::flip() +void FolderPair::flip() //this overrides both ContainerObject/FileSystemObject::flip! { ContainerObject ::flip(); //call base class versions FileSystemObject::flip(); // @@ -1111,6 +1085,48 @@ void FolderPair::flip() inline +void FilePair::flip() +{ + FileSystemObject::flip(); //call base class version + std::swap(attrL_, attrR_); + + switch (contentCategory_) + { + //*INDENT-OFF* + case FileContentCategory::unknown: + case FileContentCategory::equal: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: break; + case FileContentCategory::leftNewer: contentCategory_ = FileContentCategory::rightNewer; break; + case FileContentCategory::rightNewer: contentCategory_ = FileContentCategory::leftNewer; break; + //*INDENT-ON* + } +} + + +inline +void SymlinkPair::flip() +{ + FileSystemObject::flip(); //call base class versions + std::swap(attrL_, attrR_); + + switch (contentCategory_) + { + //*INDENT-OFF* + case FileContentCategory::unknown: + case FileContentCategory::equal: + case FileContentCategory::invalidTime: + case FileContentCategory::different: + case FileContentCategory::conflict: break; + case FileContentCategory::leftNewer: contentCategory_ = FileContentCategory::rightNewer; break; + case FileContentCategory::rightNewer: contentCategory_ = FileContentCategory::leftNewer; break; + //*INDENT-ON* + } +} + + +inline void FolderPair::removeObjectL() { for (FilePair& file : refSubFiles()) @@ -1153,16 +1169,206 @@ void BaseFolderPair::setFolderStatus(BaseFolderStatus value) inline -void FilePair::flip() +void FolderPair::setCategoryConflict(const Zstringc& description) { - FileSystemObject::flip(); //call base class version - std::swap(attrL_, attrR_); + assert(!description.empty()); + categoryConflict_ = description; +} + + +inline +void FilePair::setCategoryConflict(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::conflict; +} + + +inline +void SymlinkPair::setCategoryConflict(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::conflict; +} + + +inline +void FilePair::setCategoryInvalidTime(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::invalidTime; +} + + +inline +void SymlinkPair::setCategoryInvalidTime(const Zstringc& description) +{ + assert(!description.empty()); + categoryDescr_ = description; + contentCategory_ = FileContentCategory::invalidTime; +} + + +inline Zstringc FolderPair ::getCategoryCustomDescription() const { return categoryConflict_; } +inline Zstringc FilePair ::getCategoryCustomDescription() const { return categoryDescr_; } +inline Zstringc SymlinkPair::getCategoryCustomDescription() const { return categoryDescr_; } + + +inline +void FilePair::setContentCategory(FileContentCategory category) +{ + assert(!isEmpty<SelectSide::left>() &&!isEmpty<SelectSide::right>()); + assert(category != FileContentCategory::unknown); + contentCategory_ = category; +} + + +inline +void SymlinkPair::setContentCategory(FileContentCategory category) +{ + assert(!isEmpty<SelectSide::left>() &&!isEmpty<SelectSide::right>()); + assert(category != FileContentCategory::unknown); + contentCategory_ = category; +} + + +inline +FileContentCategory FilePair::getContentCategory() const +{ + assert(!isEmpty<SelectSide::left>() &&!isEmpty<SelectSide::right>()); + return contentCategory_; +} + + +inline +FileContentCategory SymlinkPair::getContentCategory() const +{ + assert(!isEmpty<SelectSide::left>() &&!isEmpty<SelectSide::right>()); + return contentCategory_; +} + + +inline +CompareFileResult FolderPair::getCategory() const +{ + if (!categoryConflict_.empty()) + return FILE_CONFLICT; + + if (isEmpty<SelectSide::left>()) + { + if (isEmpty<SelectSide::right>()) + return FILE_EQUAL; + else + return FILE_RIGHT_ONLY; + } + else + { + if (isEmpty<SelectSide::right>()) + return FILE_LEFT_ONLY; + else + return hasEquivalentItemNames() ? FILE_EQUAL : FILE_RENAMED; + } +} + + +inline +CompareFileResult FilePair::getCategory() const +{ + assert(contentCategory_ == FileContentCategory::conflict || + (isEmpty<SelectSide::left>() || isEmpty<SelectSide::right>()) == (contentCategory_ == FileContentCategory::unknown)); + assert(contentCategory_ != FileContentCategory::conflict || !categoryDescr_.empty()); + + if (contentCategory_ == FileContentCategory::conflict) + { + assert(!categoryDescr_.empty()); + return FILE_CONFLICT; + } + + if (isEmpty<SelectSide::left>()) + { + if (isEmpty<SelectSide::right>()) + return FILE_EQUAL; + else + return FILE_RIGHT_ONLY; + } + else + { + if (isEmpty<SelectSide::right>()) + return FILE_LEFT_ONLY; + else + //Caveat: + //1. FILE_EQUAL may only be set if names match in case: InSyncFolder's mapping tables use file name as a key! see db_file.cpp + //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h + //3. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile + switch (contentCategory_) + { + //*INDENT-OFF* + case FileContentCategory::unknown: + case FileContentCategory::conflict: assert(false); return FILE_CONFLICT; + case FileContentCategory::equal: return hasEquivalentItemNames() ? FILE_EQUAL : FILE_RENAMED; + case FileContentCategory::leftNewer: return FILE_LEFT_NEWER; + case FileContentCategory::rightNewer: return FILE_RIGHT_NEWER; + case FileContentCategory::invalidTime: return FILE_TIME_INVALID; + case FileContentCategory::different: return FILE_DIFFERENT_CONTENT; + //*INDENT-ON* + } + } + throw std::logic_error(std::string(__FILE__) + '[' + zen::numberTo<std::string>(__LINE__) + "] Contract violation!"); +} + + +inline +CompareFileResult SymlinkPair::getCategory() const +{ + assert(contentCategory_ == FileContentCategory::conflict || + (isEmpty<SelectSide::left>() || isEmpty<SelectSide::right>()) == (contentCategory_ == FileContentCategory::unknown)); + assert(contentCategory_ != FileContentCategory::conflict || !categoryDescr_.empty()); + + if (contentCategory_ == FileContentCategory::conflict) + { + assert(!categoryDescr_.empty()); + return FILE_CONFLICT; + } + + if (isEmpty<SelectSide::left>()) + { + if (isEmpty<SelectSide::right>()) + return FILE_EQUAL; + else + return FILE_RIGHT_ONLY; + } + else + { + if (isEmpty<SelectSide::right>()) + return FILE_LEFT_ONLY; + else + //Caveat: + //1. SYMLINK_EQUAL may only be set if names match in case: InSyncFolder's mapping tables use link name as a key! see db_file.cpp + //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h + switch (contentCategory_) + { + //*INDENT-OFF* + case FileContentCategory::unknown: + case FileContentCategory::conflict: assert(false); return FILE_CONFLICT; + case FileContentCategory::equal: return hasEquivalentItemNames() ? FILE_EQUAL : FILE_RENAMED; + case FileContentCategory::leftNewer: return FILE_LEFT_NEWER; + case FileContentCategory::rightNewer: return FILE_RIGHT_NEWER; + case FileContentCategory::invalidTime: return FILE_TIME_INVALID; + case FileContentCategory::different: return FILE_DIFFERENT_CONTENT; + //*INDENT-ON* + } + } + throw std::logic_error(std::string(__FILE__) + '[' + zen::numberTo<std::string>(__LINE__) + "] Contract violation!"); } template <SelectSide side> inline FileAttributes FilePair::getAttributes() const { + assert(!isEmpty<side>()); return selectParam<side>(attrL_, attrR_); } @@ -1170,6 +1376,7 @@ FileAttributes FilePair::getAttributes() const template <SelectSide side> inline time_t FilePair::getLastWriteTime() const { + assert(!isEmpty<side>()); return selectParam<side>(attrL_, attrR_).modTime; } @@ -1177,20 +1384,23 @@ time_t FilePair::getLastWriteTime() const template <SelectSide side> inline uint64_t FilePair::getFileSize() const { + assert(!isEmpty<side>()); return selectParam<side>(attrL_, attrR_).fileSize; } template <SelectSide side> inline -bool FilePair::isFollowedSymlink() const +bool FolderPair::isFollowedSymlink() const { + assert(!isEmpty<side>()); return selectParam<side>(attrL_, attrR_).isFollowedSymlink; } template <SelectSide side> inline -bool FolderPair::isFollowedSymlink() const +bool FilePair::isFollowedSymlink() const { + assert(!isEmpty<side>()); return selectParam<side>(attrL_, attrR_).isFollowedSymlink; } @@ -1198,6 +1408,7 @@ bool FolderPair::isFollowedSymlink() const template <SelectSide side> inline AFS::FingerPrint FilePair::getFilePrint() const { + assert(!isEmpty<side>()); return selectParam<side>(attrL_, attrR_).filePrint; } @@ -1210,8 +1421,21 @@ void FilePair::clearFilePrint() template <SelectSide sideTrg> inline -void FilePair::setSyncedTo(const Zstring& itemName, - uint64_t fileSize, +void FolderPair::setSyncedTo(bool isSymlinkTrg, + bool isSymlinkSrc) +{ + selectParam< sideTrg >(attrL_, attrR_) = {.isFollowedSymlink = isSymlinkTrg}; + selectParam<getOtherSide<sideTrg>>(attrL_, attrR_) = {.isFollowedSymlink = isSymlinkSrc}; + + setItemName<sideTrg>(getItemName<getOtherSide<sideTrg>>()); + + categoryConflict_.clear(); + setSyncDir(SyncDirection::none); +} + + +template <SelectSide sideTrg> inline +void FilePair::setSyncedTo(uint64_t fileSize, int64_t lastWriteTimeTrg, int64_t lastWriteTimeSrc, AFS::FingerPrint filePrintTrg, @@ -1219,58 +1443,100 @@ void FilePair::setSyncedTo(const Zstring& itemName, bool isSymlinkTrg, bool isSymlinkSrc) { - //FILE_EQUAL is only allowed for same short name and file size: enforced by this method! - selectParam< sideTrg >(attrL_, attrR_) = FileAttributes(lastWriteTimeTrg, fileSize, filePrintTrg, isSymlinkTrg); - selectParam<getOtherSide<sideTrg>>(attrL_, attrR_) = FileAttributes(lastWriteTimeSrc, fileSize, filePrintSrc, isSymlinkSrc); + selectParam< sideTrg >(attrL_, attrR_) = {lastWriteTimeTrg, fileSize, filePrintTrg, isSymlinkTrg}; + selectParam<getOtherSide<sideTrg>>(attrL_, attrR_) = {lastWriteTimeSrc, fileSize, filePrintSrc, isSymlinkSrc}; + warn_static("same thing for delete!?") + //cut ties between "move" pairs + if (moveFileRef_) + if (auto refFile = dynamic_cast<FilePair*>(FileSystemObject::retrieve(moveFileRef_))) + { + if (refFile->moveFileRef_ == getId()) //both ends should agree... + refFile->moveFileRef_ = nullptr; + else assert(false); //...and why shouldn't they? + } moveFileRef_ = nullptr; - FileSystemObject::setSynced(itemName); //set FileSystemObject specific part + + setItemName<sideTrg>(getItemName<getOtherSide<sideTrg>>()); + + contentCategory_ = FileContentCategory::equal; + categoryDescr_.clear(); + setSyncDir(SyncDirection::none); } template <SelectSide sideTrg> inline -void SymlinkPair::setSyncedTo(const Zstring& itemName, - int64_t lastWriteTimeTrg, +void SymlinkPair::setSyncedTo(int64_t lastWriteTimeTrg, int64_t lastWriteTimeSrc) { - selectParam< sideTrg >(attrL_, attrR_) = LinkAttributes(lastWriteTimeTrg); - selectParam<getOtherSide<sideTrg>>(attrL_, attrR_) = LinkAttributes(lastWriteTimeSrc); + selectParam< sideTrg >(attrL_, attrR_) = {.modTime = lastWriteTimeTrg}; + selectParam<getOtherSide<sideTrg>>(attrL_, attrR_) = {.modTime = lastWriteTimeSrc}; + + setItemName<sideTrg>(getItemName<getOtherSide<sideTrg>>()); - FileSystemObject::setSynced(itemName); //set FileSystemObject specific part + contentCategory_ = FileContentCategory::equal; + categoryDescr_.clear(); + setSyncDir(SyncDirection::none); } -template <SelectSide sideTrg> inline -void FolderPair::setSyncedTo(const Zstring& itemName, - bool isSymlinkTrg, - bool isSymlinkSrc) +inline +bool FolderPair::passDirFilter(const PathFilter& filter, bool* childItemMightMatch) const { - selectParam< sideTrg >(attrL_, attrR_) = FolderAttributes(isSymlinkTrg); - selectParam<getOtherSide<sideTrg>>(attrL_, attrR_) = FolderAttributes(isSymlinkSrc); + const Zstring& relPathL = getRelativePath<SelectSide::left >(); + const Zstring& relPathR = getRelativePath<SelectSide::right>(); + assert(relPathL.c_str() == relPathR.c_str() || relPathL!= relPathR); - FileSystemObject::setSynced(itemName); //set FileSystemObject specific part + if (filter.passDirFilter(relPathL, childItemMightMatch)) + return relPathL.c_str() == relPathR.c_str() /*perf!*/ || equalNoCase(relPathL, relPathR) || + filter.passDirFilter(relPathR, childItemMightMatch); + else + { + if (childItemMightMatch && *childItemMightMatch && + relPathL.c_str() != relPathR.c_str() /*perf!*/ && !equalNoCase(relPathL, relPathR)) + filter.passDirFilter(relPathR, childItemMightMatch); + return false; + } } -template <SelectSide side> inline -time_t SymlinkPair::getLastWriteTime() const +inline +bool FileSystemObject::passFileFilter(const PathFilter& filter) const { - return selectParam<side>(attrL_, attrR_).modTime; -} + assert(!dynamic_cast<const FolderPair*>(this)); + assert(parent().getRelativePath<SelectSide::left >().c_str() == + parent().getRelativePath<SelectSide::right>().c_str() || + parent().getRelativePath<SelectSide::left >()!= + parent().getRelativePath<SelectSide::right>()); + assert(getItemName<SelectSide::left >().c_str() == + getItemName<SelectSide::right>().c_str() || + getItemName<SelectSide::left >() != + getItemName<SelectSide::right>()); + const Zstring relPathL = getRelativePath<SelectSide::left>(); -inline -CompareSymlinkResult SymlinkPair::getLinkCategory() const -{ - return static_cast<CompareSymlinkResult>(getCategory()); + if (!filter.passFileFilter(relPathL)) + return false; + + if (parent().getRelativePath<SelectSide::left >().c_str() == // + parent().getRelativePath<SelectSide::right>().c_str() && //perf! see ContainerObject constructor + getItemName<SelectSide::left >().c_str() == // + getItemName<SelectSide::right>().c_str()) // + return true; + + const Zstring relPathR = getRelativePath<SelectSide::right>(); + + if (equalNoCase(relPathL, relPathR)) + return true; + + return filter.passFileFilter(relPathR); } -inline -void SymlinkPair::flip() +template <SelectSide side> inline +time_t SymlinkPair::getLastWriteTime() const { - FileSystemObject::flip(); //call base class versions - std::swap(attrL_, attrR_); + return selectParam<side>(attrL_, attrR_).modTime; } } diff --git a/FreeFileSync/Source/base/multi_rename.cpp b/FreeFileSync/Source/base/multi_rename.cpp index bc1e48ad..19467d5f 100644 --- a/FreeFileSync/Source/base/multi_rename.cpp +++ b/FreeFileSync/Source/base/multi_rename.cpp @@ -115,6 +115,10 @@ size_t getPlaceholderIndex(wchar_t c) } } + +bool fff::isRenamePlaceholderChar(wchar_t c) { return getPlaceholderIndex(c) < std::size(placeholders); } + + struct fff::RenameBuf { explicit RenameBuf(const std::vector<std::wstring>& s) : strings(s) {} @@ -127,7 +131,7 @@ struct fff::RenameBuf //e.g. "Season ❶, Episode ❷ - ❸.avi" std::pair<std::wstring /*phrase*/, SharedRef<const RenameBuf>> fff::getPlaceholderPhrase(const std::vector<std::wstring>& strings) { - auto renameBuf = makeSharedRef<const RenameBuf>(strings); + auto renameBuf = makeSharedRef<const RenameBuf>(strings); std::wstring phrase; size_t placeIdx = 0; diff --git a/FreeFileSync/Source/base/multi_rename.h b/FreeFileSync/Source/base/multi_rename.h index 40139049..ce44ddbb 100644 --- a/FreeFileSync/Source/base/multi_rename.h +++ b/FreeFileSync/Source/base/multi_rename.h @@ -7,9 +7,7 @@ #ifndef MULTI_RENAME_H_489572039485723453425 #define MULTI_RENAME_H_489572039485723453425 -//#include <span> -//#include <string> -//#include <vector> +#include <string> #include <zen/stl_tools.h> namespace fff @@ -18,6 +16,8 @@ struct RenameBuf; std::pair<std::wstring /*phrase*/, zen::SharedRef<const RenameBuf>> getPlaceholderPhrase(const std::vector<std::wstring>& strings); const std::vector<std::wstring> resolvePlaceholderPhrase(const std::wstring_view phrase, const RenameBuf& buf); + +bool isRenamePlaceholderChar(wchar_t c); } #endif //MULTI_RENAME_H_489572039485723453425 diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp index 626a9371..8ccb7e64 100644 --- a/FreeFileSync/Source/base/parallel_scan.cpp +++ b/FreeFileSync/Source/base/parallel_scan.cpp @@ -291,7 +291,13 @@ void DirCallback::onFile(const AFS::FileInfo& fi) //throw ThreadStopRequest return; //note: sync.ffs_db database and lock files are excluded via path filter! - output_.addFile(fi.itemName, FileAttributes(fi.modTime, fi.fileSize, fi.filePrint, fi.isFollowedSymlink)); + output_.addFile(fi.itemName, + { + .modTime = fi.modTime, + .fileSize = fi.fileSize, + .filePrint = fi.filePrint, + .isFollowedSymlink = fi.isFollowedSymlink, + }); cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator } @@ -315,7 +321,7 @@ std::shared_ptr<AFS::TraverserCallback> DirCallback::onFolder(const AFS::FolderI return nullptr; //do NOT traverse subdirs //else: ensure directory filtering is applied later to exclude actually filtered directories!!! - FolderContainer& subFolder = output_.addFolder(fi.itemName, FolderAttributes(fi.isFollowedSymlink)); + FolderContainer& subFolder = output_.addFolder(fi.itemName, {.isFollowedSymlink = fi.isFollowedSymlink}); if (passFilter) cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator @@ -354,7 +360,7 @@ DirCallback::HandleLink DirCallback::onSymlink(const AFS::SymlinkInfo& si) //thr case SymLinkHandling::asLink: if (cfg_.filter.ref().passFileFilter(relPath)) //always use file filter: Link type may not be "stable" on Linux! { - output_.addLink(si.itemName, LinkAttributes(si.modTime)); + output_.addLink(si.itemName, {.modTime = si.modTime}); cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator } return HandleLink::skip; diff --git a/FreeFileSync/Source/base/soft_filter.h b/FreeFileSync/Source/base/soft_filter.h index c3cd8d0f..44356f38 100644 --- a/FreeFileSync/Source/base/soft_filter.h +++ b/FreeFileSync/Source/base/soft_filter.h @@ -24,9 +24,9 @@ Semantics of SoftFilter: class SoftFilter { public: - SoftFilter(size_t timeSpan, UnitTime unitTimeSpan, - size_t sizeMin, UnitSize unitSizeMin, - size_t sizeMax, UnitSize unitSizeMax); + SoftFilter(size_t timeSpan, UnitTime unitTimeSpan, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax); bool matchTime(time_t writeTime) const { return timeFrom_ <= writeTime; } bool matchSize(uint64_t fileSize) const { return sizeMin_ <= fileSize && fileSize <= sizeMax_; } @@ -63,9 +63,9 @@ private: // ----------------------- implementation ----------------------- inline -SoftFilter::SoftFilter(size_t timeSpan, UnitTime unitTimeSpan, - size_t sizeMin, UnitSize unitSizeMin, - size_t sizeMax, UnitSize unitSizeMax) : +SoftFilter::SoftFilter(size_t timeSpan, UnitTime unitTimeSpan, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax) : matchesFolder_(unitTimeSpan == UnitTime::none && unitSizeMin == UnitSize::none && unitSizeMax == UnitSize::none) //exclude folders if size or date filter is active: avoids creating empty folders if not needed! diff --git a/FreeFileSync/Source/base/status_handler_impl.h b/FreeFileSync/Source/base/status_handler_impl.h index e0bf44ad..fa794d6a 100644 --- a/FreeFileSync/Source/base/status_handler_impl.h +++ b/FreeFileSync/Source/base/status_handler_impl.h @@ -532,16 +532,16 @@ void massParallelExecute(const std::vector<std::pair<AbstractPath, ParallelWorkI std::atomic<size_t> activeDeviceCount(perDeviceWorkload.size()); // //--------------------------------------------------------------------------------------------------------- - std::map<AfsDevice, ThreadGroup<std::function<void()>>> deviceThreadGroups; //worker threads live here... + std::vector<ThreadGroup<std::function<void()>>> deviceThreadGroups; //worker threads live here... //--------------------------------------------------------------------------------------------------------- for (const auto& [afsDevice, wl] : perDeviceWorkload) { const size_t statusPrio = deviceThreadGroups.size(); - auto& threadGroup = deviceThreadGroups.emplace(afsDevice, ThreadGroup<std::function<void()>>( - 1, - threadGroupName + Zstr(' ') + utfTo<Zstring>(AFS::getDisplayPath(AbstractPath(afsDevice, AfsPath()))))).first->second; + const Zstring& deviceGroupName = threadGroupName + Zstr(' ') + utfTo<Zstring>(AFS::getDisplayPath(AbstractPath(afsDevice, AfsPath()))); + deviceThreadGroups.emplace_back(1, deviceGroupName); + auto& threadGroup = deviceThreadGroups.back(); for (const std::pair<AbstractPath, ParallelWorkItem>* item : wl) threadGroup.run([&acb, statusPrio, &itemPath = item->first, &task = item->second] diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp index b2e70d5e..c1da1bf5 100644 --- a/FreeFileSync/Source/base/structures.cpp +++ b/FreeFileSync/Source/base/structures.cpp @@ -69,69 +69,128 @@ std::wstring fff::getVariantNameWithSymbol(SyncVariant var) } -DirectionSet fff::extractDirections(const SyncDirectionConfig& cfg) +DirectionByDiff fff::getDiffDirDefault(const DirectionByChange& changeDirs) { - DirectionSet output; - switch (cfg.var) + return { - case SyncVariant::twoWay: - throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] there are no predefined directions for automatic mode!"); - - case SyncVariant::mirror: - output.exLeftSideOnly = SyncDirection::right; - output.exRightSideOnly = SyncDirection::right; - output.leftNewer = SyncDirection::right; - output.rightNewer = SyncDirection::right; - output.different = SyncDirection::right; - output.conflict = SyncDirection::right; - break; - - case SyncVariant::update: - output.exLeftSideOnly = SyncDirection::right; - output.exRightSideOnly = SyncDirection::none; - output.leftNewer = SyncDirection::right; - output.rightNewer = SyncDirection::none; - output.different = SyncDirection::right; - output.conflict = SyncDirection::none; - break; - - case SyncVariant::custom: - output = cfg.custom; - break; - } - return output; + .leftOnly = changeDirs.left.create, + .rightOnly = changeDirs.right.create, + .leftNewer = changeDirs.left.update, + .rightNewer = changeDirs.right.update, + }; +} + + +DirectionByChange fff::getChangesDirDefault(const DirectionByDiff& diffDirs) +{ + return + { + .left + { + .create = diffDirs.leftOnly, + .update = diffDirs.leftNewer, + .delete_ = diffDirs.rightOnly, + }, + .right + { + .create = diffDirs.rightOnly, + .update = diffDirs.rightNewer, + .delete_ = diffDirs.leftOnly, + } + }; +} + + +namespace +{ +DirectionByChange getTwoWayDirSet() +{ + return + { + .left + { + .create = SyncDirection::right, + .update = SyncDirection::right, + .delete_ = SyncDirection::right, + }, + .right + { + .create = SyncDirection::left, + .update = SyncDirection::left, + .delete_ = SyncDirection::left, + } + }; } -bool fff::detectMovedFilesSelectable(const SyncDirectionConfig& cfg) +DirectionByDiff getMirrorDirSet() { - if (cfg.var == SyncVariant::twoWay) - return false; //moved files are always detected since we have the database file anyway - - const DirectionSet tmp = fff::extractDirections(cfg); - return (tmp.exLeftSideOnly == SyncDirection::right && - tmp.exRightSideOnly == SyncDirection::right) || - (tmp.exLeftSideOnly == SyncDirection::left && - tmp.exRightSideOnly == SyncDirection::left); + return + { + .leftOnly = SyncDirection::right, + .rightOnly = SyncDirection::right, + .leftNewer = SyncDirection::right, + .rightNewer = SyncDirection::right, + }; } -bool fff::detectMovedFilesEnabled(const SyncDirectionConfig& cfg) +DirectionByChange getUpdateDirSet() { - return detectMovedFilesSelectable(cfg) ? cfg.detectMovedFiles : cfg.var == SyncVariant::twoWay; + return + { + .left + { + .create = SyncDirection::right, + .update = SyncDirection::right, + .delete_ = SyncDirection::none, + }, + .right + { + .create = SyncDirection::none, + .update = SyncDirection::none, + .delete_ = SyncDirection::none, + } + }; +} } -DirectionSet fff::getTwoWayUpdateSet() +SyncVariant fff::getSyncVariant(const SyncDirectionConfig& cfg) { - DirectionSet output; - output.exLeftSideOnly = SyncDirection::right; - output.exRightSideOnly = SyncDirection::left; - output.leftNewer = SyncDirection::right; - output.rightNewer = SyncDirection::left; - output.different = SyncDirection::none; - output.conflict = SyncDirection::none; - return output; + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&cfg.dirs)) + { + if (*diffDirs == getMirrorDirSet()) + return SyncVariant::mirror; + if (*diffDirs == getDiffDirDefault(getUpdateDirSet())) //poor man's "update", still deserves name on GUI + return SyncVariant::update; + } + else + { + const DirectionByChange& changeDirs = std::get<DirectionByChange>(cfg.dirs); + if (changeDirs == getTwoWayDirSet()) + return SyncVariant::twoWay; + if (changeDirs == getChangesDirDefault(getMirrorDirSet())) //equivalent: "mirror" defined in terms of "changes" + return SyncVariant::mirror; + if (changeDirs == getUpdateDirSet()) + return SyncVariant::update; + } + return SyncVariant::custom; +} + + +SyncDirectionConfig fff::getDefaultSyncCfg(SyncVariant syncVar) +{ + switch (syncVar) + { + //*INDENT-OFF* + case SyncVariant::twoWay: return { .dirs = getTwoWayDirSet() }; + case SyncVariant::mirror: return { .dirs = getMirrorDirSet() }; + case SyncVariant::update: return { .dirs = getUpdateDirSet() }; + case SyncVariant::custom: return { .dirs = getDiffDirDefault(getTwoWayDirSet()) }; + //*INDENT-ON* + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } @@ -172,14 +231,15 @@ std::wstring fff::getSymbol(CompareFileResult cmpRes) switch (cmpRes) { //*INDENT-OFF* - case FILE_LEFT_SIDE_ONLY: return L"only <-"; - case FILE_RIGHT_SIDE_ONLY: return L"only ->"; + case FILE_EQUAL: return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs + case FILE_RENAMED: return L"renamed"; + case FILE_LEFT_ONLY: return L"only <-"; + case FILE_RIGHT_ONLY: return L"only ->"; case FILE_LEFT_NEWER: return L"newer <-"; case FILE_RIGHT_NEWER: return L"newer ->"; case FILE_DIFFERENT_CONTENT: return L"!="; - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: /*= sub-category of equal!*/ return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs - case FILE_CONFLICT: return L"conflict"; + case FILE_TIME_INVALID: + case FILE_CONFLICT: return L"conflict"; //*INDENT-ON* } assert(false); @@ -192,21 +252,21 @@ std::wstring fff::getSymbol(SyncOperation op) switch (op) { //*INDENT-OFF* - case SO_CREATE_NEW_LEFT: return L"create <-"; - case SO_CREATE_NEW_RIGHT: return L"create ->"; - case SO_DELETE_LEFT: return L"delete <-"; - case SO_DELETE_RIGHT: return L"delete ->"; - case SO_MOVE_LEFT_FROM: return L"move from <-"; - case SO_MOVE_LEFT_TO: return L"move to <-"; - case SO_MOVE_RIGHT_FROM: return L"move from ->"; - case SO_MOVE_RIGHT_TO: return L"move to ->"; - case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: return L"update <-"; - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: return L"update ->"; - case SO_DO_NOTHING: return L" -"; - case SO_EQUAL: return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs - case SO_UNRESOLVED_CONFLICT: return L"conflict"; //portable Unicode symbol: ⚡ + case SO_CREATE_LEFT: return L"create <-"; + case SO_CREATE_RIGHT: return L"create ->"; + case SO_DELETE_LEFT: return L"delete <-"; + case SO_DELETE_RIGHT: return L"delete ->"; + case SO_MOVE_LEFT_FROM: return L"move from <-"; + case SO_MOVE_LEFT_TO: return L"move to <-"; + case SO_MOVE_RIGHT_FROM: return L"move from ->"; + case SO_MOVE_RIGHT_TO: return L"move to ->"; + case SO_OVERWRITE_LEFT: return L"update <-"; + case SO_OVERWRITE_RIGHT: return L"update ->"; + case SO_RENAME_LEFT: return L"rename <-"; + case SO_RENAME_RIGHT: return L"rename ->"; + case SO_DO_NOTHING: return L" -"; + case SO_EQUAL: return L"'=="; //added quotation mark to avoid error in Excel cell when exporting to *.cvs + case SO_UNRESOLVED_CONFLICT: return L"conflict"; //portable Unicode symbol: ⚡ //*INDENT-ON* }; assert(false); @@ -261,7 +321,7 @@ time_t resolve(size_t value, UnitTime unit, time_t defaultVal) } -uint64_t resolve(size_t value, UnitSize unit, uint64_t defaultVal) +uint64_t resolve(uint64_t value, UnitSize unit, uint64_t defaultVal) { constexpr uint64_t maxVal = std::numeric_limits<uint64_t>::max(); @@ -284,8 +344,8 @@ uint64_t resolve(size_t value, UnitSize unit, uint64_t defaultVal) } void fff::resolveUnits(size_t timeSpan, UnitTime unitTimeSpan, - size_t sizeMin, UnitSize unitSizeMin, - size_t sizeMax, UnitSize unitSizeMax, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax, time_t& timeFrom, //unit: UTC time, seconds uint64_t& sizeMinBy, //unit: bytes uint64_t& sizeMaxBy) //unit: bytes @@ -296,7 +356,7 @@ void fff::resolveUnits(size_t timeSpan, UnitTime unitTimeSpan, } -std::optional<CompareVariant> fff::getCompVariant(const MainConfiguration& mainCfg) +std::optional<CompareVariant> fff::getCommonCompVariant(const MainConfiguration& mainCfg) { const CompareVariant firstVar = mainCfg.firstPair.localCmpCfg ? mainCfg.firstPair.localCmpCfg->compareVar : @@ -315,18 +375,18 @@ std::optional<CompareVariant> fff::getCompVariant(const MainConfiguration& mainC } -std::optional<SyncVariant> fff::getSyncVariant(const MainConfiguration& mainCfg) +std::optional<SyncVariant> fff::getCommonSyncVariant(const MainConfiguration& mainCfg) { - const SyncVariant firstVar = mainCfg.firstPair.localSyncCfg ? - mainCfg.firstPair.localSyncCfg->directionCfg.var : - mainCfg.syncCfg.directionCfg.var; //fallback to main sync cfg + const SyncVariant firstVar = getSyncVariant(mainCfg.firstPair.localSyncCfg ? + mainCfg.firstPair.localSyncCfg->directionCfg : + mainCfg.syncCfg.directionCfg); //fallback to main sync cfg //test if there's a deviating variant within the additional folder pairs for (const LocalPairConfig& lpc : mainCfg.additionalPairs) { - const SyncVariant localVariant = lpc.localSyncCfg ? - lpc.localSyncCfg->directionCfg.var : - mainCfg.syncCfg.directionCfg.var; + const SyncVariant localVariant = getSyncVariant(lpc.localSyncCfg ? + lpc.localSyncCfg->directionCfg: + mainCfg.syncCfg.directionCfg); if (localVariant != firstVar) return std::nullopt; } diff --git a/FreeFileSync/Source/base/structures.h b/FreeFileSync/Source/base/structures.h index 4b8ed570..9b7ba360 100644 --- a/FreeFileSync/Source/base/structures.h +++ b/FreeFileSync/Source/base/structures.h @@ -7,6 +7,7 @@ #ifndef STRUCTURES_H_8210478915019450901745 #define STRUCTURES_H_8210478915019450901745 +#include <variant> #include <vector> #include <memory> #include <chrono> @@ -45,34 +46,36 @@ enum class SyncDirection : unsigned char //save space for use in FileSystemObjec enum CompareFileResult { FILE_EQUAL, - FILE_LEFT_SIDE_ONLY, - FILE_RIGHT_SIDE_ONLY, - FILE_LEFT_NEWER, //CompareVariant::timeSize only! - FILE_RIGHT_NEWER, // + FILE_RENAMED, //both sides equal, except for different file name + FILE_LEFT_ONLY, + FILE_RIGHT_ONLY, + FILE_LEFT_NEWER, // + FILE_RIGHT_NEWER, //CompareVariant::timeSize only! + FILE_TIME_INVALID, // -> sync dirction can be determined (if leftNewer/rightNewer agree), unlike with FILE_CONFLICT FILE_DIFFERENT_CONTENT, //CompareVariant::content, CompareVariant::size only! - FILE_DIFFERENT_METADATA, //both sides equal, but different metadata only: short name case FILE_CONFLICT }; //attention make sure these /|\ \|/ three enums match!!! enum CompareDirResult { - DIR_EQUAL = FILE_EQUAL, - DIR_LEFT_SIDE_ONLY = FILE_LEFT_SIDE_ONLY, - DIR_RIGHT_SIDE_ONLY = FILE_RIGHT_SIDE_ONLY, - DIR_DIFFERENT_METADATA = FILE_DIFFERENT_METADATA, //both sides equal, but different metadata only: short name case - DIR_CONFLICT = FILE_CONFLICT + DIR_EQUAL = FILE_EQUAL, + DIR_RENAMED = FILE_RENAMED, + DIR_LEFT_ONLY = FILE_LEFT_ONLY, + DIR_RIGHT_ONLY = FILE_RIGHT_ONLY, + DIR_CONFLICT = FILE_CONFLICT }; enum CompareSymlinkResult { - SYMLINK_EQUAL = FILE_EQUAL, - SYMLINK_LEFT_SIDE_ONLY = FILE_LEFT_SIDE_ONLY, - SYMLINK_RIGHT_SIDE_ONLY = FILE_RIGHT_SIDE_ONLY, - SYMLINK_LEFT_NEWER = FILE_LEFT_NEWER, - SYMLINK_RIGHT_NEWER = FILE_RIGHT_NEWER, - SYMLINK_DIFFERENT_CONTENT = FILE_DIFFERENT_CONTENT, - SYMLINK_DIFFERENT_METADATA = FILE_DIFFERENT_METADATA, //both sides equal, but different metadata only: short name case - SYMLINK_CONFLICT = FILE_CONFLICT + SYMLINK_EQUAL = FILE_EQUAL, + SYMLINK_RENAMED = FILE_RENAMED, + SYMLINK_LEFT_ONLY = FILE_LEFT_ONLY, + SYMLINK_RIGHT_ONLY = FILE_RIGHT_ONLY, + SYMLINK_LEFT_NEWER = FILE_LEFT_NEWER, + SYMLINK_RIGHT_NEWER = FILE_RIGHT_NEWER, + SYMLINK_TIME_INVALID = FILE_TIME_INVALID, + SYMLINK_DIFFERENT_CONTENT = FILE_DIFFERENT_CONTENT, + SYMLINK_CONFLICT = FILE_CONFLICT }; @@ -81,21 +84,22 @@ std::wstring getSymbol(CompareFileResult cmpRes); enum SyncOperation { - SO_CREATE_NEW_LEFT, - SO_CREATE_NEW_RIGHT, + SO_CREATE_LEFT, + SO_CREATE_RIGHT, SO_DELETE_LEFT, SO_DELETE_RIGHT, - SO_MOVE_LEFT_FROM, //SO_DELETE_LEFT - optimization! - SO_MOVE_LEFT_TO, //SO_CREATE_NEW_LEFT - - SO_MOVE_RIGHT_FROM, //SO_DELETE_RIGHT - optimization! - SO_MOVE_RIGHT_TO, //SO_CREATE_NEW_RIGHT - SO_OVERWRITE_LEFT, SO_OVERWRITE_RIGHT, - SO_COPY_METADATA_TO_LEFT, //objects are already equal: transfer metadata only - optimization - SO_COPY_METADATA_TO_RIGHT, // + + SO_MOVE_LEFT_FROM, //SO_DELETE_LEFT - optimization! + SO_MOVE_LEFT_TO, //SO_CREATE_LEFT + + SO_MOVE_RIGHT_FROM, //SO_DELETE_RIGHT - optimization! + SO_MOVE_RIGHT_TO, //SO_CREATE_RIGHT + + SO_RENAME_LEFT, //items are otherwise equal + SO_RENAME_RIGHT, // SO_DO_NOTHING, //nothing will be synced: both sides differ SO_EQUAL, //nothing will be synced: both sides are equal @@ -105,62 +109,71 @@ enum SyncOperation std::wstring getSymbol(SyncOperation op); //method used for exporting .csv file only! -struct DirectionSet +enum class CudAction { - SyncDirection exLeftSideOnly = SyncDirection::right; - SyncDirection exRightSideOnly = SyncDirection::left; - SyncDirection leftNewer = SyncDirection::right; //CompareVariant::timeSize only! - SyncDirection rightNewer = SyncDirection::left; // - SyncDirection different = SyncDirection::none; //CompareVariant::content, CompareVariant::size only! - SyncDirection conflict = SyncDirection::none; - - bool operator==(const DirectionSet&) const = default; + noChange, + create, + update, + delete_, //"delete" is a reserved keyword :( }; -DirectionSet getTwoWayUpdateSet(); +struct DirectionByDiff +{ + SyncDirection leftOnly = SyncDirection::none; + SyncDirection rightOnly = SyncDirection::none; + SyncDirection leftNewer = SyncDirection::none; + SyncDirection rightNewer = SyncDirection::none; + + bool operator==(const DirectionByDiff&) const = default; +}; -enum class SyncVariant +struct DirectionByChange //=> requires sync.ffs_db { - twoWay, //use sync-database to determine directions - mirror, //predefined - update, // - custom, //use custom directions + struct Changes + { + SyncDirection create = SyncDirection::none; + SyncDirection update = SyncDirection::none; + SyncDirection delete_ = SyncDirection::none; //"delete" is a reserved keyword :( + + bool operator==(const Changes&) const = default; + } left, right; + + bool operator==(const DirectionByChange&) const = default; }; + + struct SyncDirectionConfig { - SyncVariant var = SyncVariant::twoWay; - DirectionSet custom; //sync directions for SyncVariant::custom - bool detectMovedFiles = false; //variant-dependent: e.g. always active for SyncVariant::twoWay! => use functions below for evaluation! + std::variant<DirectionByDiff, DirectionByChange> dirs; + + bool operator==(const SyncDirectionConfig&) const = default; }; -bool detectMovedFilesSelectable(const SyncDirectionConfig& cfg); -bool detectMovedFilesEnabled (const SyncDirectionConfig& cfg); -DirectionSet extractDirections(const SyncDirectionConfig& cfg); //get sync directions: DON'T call for SyncVariant::twoWay! +inline +bool effectivelyEqual(const SyncDirectionConfig& lhs, const SyncDirectionConfig& rhs) { return lhs == rhs; } //no change in behavior + + +enum class SyncVariant +{ + twoWay, + mirror, + update, + custom, +}; +SyncVariant getSyncVariant(const SyncDirectionConfig& cfg); + +SyncDirectionConfig getDefaultSyncCfg(SyncVariant syncVar); + +DirectionByDiff getDiffDirDefault(const DirectionByChange& changeDirs); //= when sync.ffs_db not yet available +DirectionByChange getChangesDirDefault(const DirectionByDiff& diffDirs); std::wstring getVariantName(std::optional<CompareVariant> var); std::wstring getVariantName(std::optional<SyncVariant> var); std::wstring getVariantNameWithSymbol(SyncVariant var); -inline -bool operator==(const SyncDirectionConfig& lhs, const SyncDirectionConfig& rhs) -{ - return lhs.var == rhs.var && - (lhs.var != SyncVariant::custom || lhs.custom == rhs.custom) && //no need to consider custom directions if var != CUSTOM - lhs.detectMovedFiles == rhs.detectMovedFiles; //useful to remember this setting even if the current sync variant does not need it - //adapt effectivelyEqual() on changes, too! -} - -inline -bool effectivelyEqual(const SyncDirectionConfig& lhs, const SyncDirectionConfig& rhs) -{ - return (lhs.var == SyncVariant::twoWay) == (rhs.var == SyncVariant::twoWay) && //either both two-way or none - (lhs.var == SyncVariant::twoWay || extractDirections(lhs) == extractDirections(rhs)) && - detectMovedFilesEnabled(lhs) == detectMovedFilesEnabled(rhs); -} - struct CompConfig { @@ -192,7 +205,7 @@ enum class VersioningStyle struct SyncConfig { //sync direction settings - SyncDirectionConfig directionCfg; + SyncDirectionConfig directionCfg = getDefaultSyncCfg(SyncVariant::twoWay); DeletionVariant deletionVariant = DeletionVariant::recycler; //use Recycle Bin, delete permanently or move to user-defined location @@ -212,7 +225,7 @@ bool operator==(const SyncConfig& lhs, const SyncConfig& rhs) { return lhs.directionCfg == rhs.directionCfg && lhs.deletionVariant == rhs.deletionVariant && //!= DeletionVariant::versioning => still consider versioningFolderPhrase: e.g. user temporarily - lhs.versioningFolderPhrase == rhs.versioningFolderPhrase && //switched to "permanent" deletion and accidentally saved cfg => versioning folder is easily restored + lhs.versioningFolderPhrase == rhs.versioningFolderPhrase && //switched to "permanent" deletion and accidentally saved cfg => versioning folder can be restored lhs.versioningStyle == rhs.versioningStyle && (lhs.versioningStyle == VersioningStyle::replace || ( @@ -264,24 +277,6 @@ enum class UnitTime struct FilterConfig { - FilterConfig() {} - FilterConfig(const Zstring& include, - const Zstring& exclude, - size_t timeSpanIn, - UnitTime unitTimeSpanIn, - size_t sizeMinIn, - UnitSize unitSizeMinIn, - size_t sizeMaxIn, - UnitSize unitSizeMaxIn) : - includeFilter(include), - excludeFilter(exclude), - timeSpan (timeSpanIn), - unitTimeSpan (unitTimeSpanIn), - sizeMin (sizeMinIn), - unitSizeMin (unitSizeMinIn), - sizeMax (sizeMaxIn), - unitSizeMax (unitSizeMaxIn) {} - /* Semantics of PathFilter: 1. using it creates a NEW folder hierarchy! -> must be considered by <Two way> variant! (fortunately it turns out, doing nothing already has perfect semantics :) 2. it applies equally to both sides => it always matches either both sides or none! => can be used while traversing a single folder! */ @@ -292,13 +287,13 @@ struct FilterConfig 1. It potentially may match only one side => it MUST NOT be applied while traversing a single folder to avoid mismatches 2. => it is applied after traversing and just marks rows, (NO deletions after comparison are allowed) 3. => equivalent to a user temporarily (de-)selecting rows -> not relevant for <Two way> variant! ;) */ - size_t timeSpan = 0; + unsigned int timeSpan = 0; UnitTime unitTimeSpan = UnitTime::none; - size_t sizeMin = 0; + uint64_t sizeMin = 0; UnitSize unitSizeMin = UnitSize::none; - size_t sizeMax = 0; + uint64_t sizeMax = 0; UnitSize unitSizeMax = UnitSize::none; bool operator==(const FilterConfig&) const = default; @@ -306,8 +301,8 @@ struct FilterConfig void resolveUnits(size_t timeSpan, UnitTime unitTimeSpan, - size_t sizeMin, UnitSize unitSizeMin, - size_t sizeMax, UnitSize unitSizeMax, + uint64_t sizeMin, UnitSize unitSizeMin, + uint64_t sizeMax, UnitSize unitSizeMax, time_t& timeFrom, //unit: UTC time, seconds uint64_t& sizeMinBy, //unit: bytes uint64_t& sizeMaxBy); //unit: bytes @@ -315,19 +310,6 @@ void resolveUnits(size_t timeSpan, UnitTime unitTimeSpan, struct LocalPairConfig //enhanced folder pairs with (optional) alternate configuration { - LocalPairConfig() {} - - LocalPairConfig(const Zstring& phraseLeft, - const Zstring& phraseRight, - const std::optional<CompConfig>& cmpCfg, - const std::optional<SyncConfig>& syncCfg, - const FilterConfig& filter) : - folderPathPhraseLeft (phraseLeft), - folderPathPhraseRight(phraseRight), - localCmpCfg(cmpCfg), - localSyncCfg(syncCfg), - localFilter(filter) {} - Zstring folderPathPhraseLeft; //unresolved directory names as entered by user! Zstring folderPathPhraseRight; // @@ -390,8 +372,8 @@ size_t getDeviceParallelOps(const std::map<AfsDevice, size_t>& deviceParallelOps void setDeviceParallelOps( std::map<AfsDevice, size_t>& deviceParallelOps, const Zstring& folderPathPhrase, size_t parallelOps); -std::optional<CompareVariant> getCompVariant(const MainConfiguration& mainCfg); -std::optional<SyncVariant> getSyncVariant(const MainConfiguration& mainCfg); +std::optional<CompareVariant> getCommonCompVariant(const MainConfiguration& mainCfg); +std::optional<SyncVariant> getCommonSyncVariant(const MainConfiguration& mainCfg); struct WarningDialogs diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 1d686900..94ef3918 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -39,9 +39,9 @@ SyncStatistics::SyncStatistics(const FolderComparison& folderCmp) } -SyncStatistics::SyncStatistics(const ContainerObject& hierObj) +SyncStatistics::SyncStatistics(const ContainerObject& conObj) { - recurse(hierObj); + recurse(conObj); } @@ -53,18 +53,33 @@ SyncStatistics::SyncStatistics(const FilePair& file) inline -void SyncStatistics::recurse(const ContainerObject& hierObj) +void SyncStatistics::recurse(const ContainerObject& conObj) { - for (const FilePair& file : hierObj.refSubFiles()) + for (const FilePair& file : conObj.refSubFiles()) processFile(file); - for (const SymlinkPair& symlink : hierObj.refSubLinks()) + for (const SymlinkPair& symlink : conObj.refSubLinks()) processLink(symlink); - for (const FolderPair& folder : hierObj.refSubFolders()) + for (const FolderPair& folder : conObj.refSubFolders()) processFolder(folder); - rowsTotal_ += hierObj.refSubFolders().size(); - rowsTotal_ += hierObj.refSubFiles ().size(); - rowsTotal_ += hierObj.refSubLinks ().size(); + rowsTotal_ += conObj.refSubFolders().size(); + rowsTotal_ += conObj.refSubFiles ().size(); + rowsTotal_ += conObj.refSubLinks ().size(); +} + + +inline +void SyncStatistics::logConflict(const FileSystemObject& fsObj) +{ + if (conflictsPreview_.size() < CONFLICTS_PREVIEW_MAX) + { + const Zstring& relPathL = fsObj.getRelativePath<SelectSide::left >(); + const Zstring& relPathR = fsObj.getRelativePath<SelectSide::right>(); + + conflictsPreview_.push_back((getUnicodeNormalForm(relPathL) == getUnicodeNormalForm(relPathR) ? + utfTo<std::wstring>(relPathL) : + utfTo<std::wstring>(relPathL + Zstr('\n') + relPathR)) + L": " + fsObj.getSyncOpConflict()); + } } @@ -73,12 +88,12 @@ void SyncStatistics::processFile(const FilePair& file) { switch (file.getSyncOperation()) //evaluate comparison result and sync direction { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: ++createLeft_; bytesToProcess_ += static_cast<int64_t>(file.getFileSize<SelectSide::right>()); break; - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: ++createRight_; bytesToProcess_ += static_cast<int64_t>(file.getFileSize<SelectSide::left>()); break; @@ -115,15 +130,14 @@ void SyncStatistics::processFile(const FilePair& file) case SO_UNRESOLVED_CONFLICT: ++conflictCount_; - if (conflictsPreview_.size() < CONFLICTS_PREVIEW_MAX) - conflictsPreview_.push_back({file.getRelativePathAny(), file.getSyncOpConflict()}); + logConflict(file); break; - case SO_COPY_METADATA_TO_LEFT: + case SO_RENAME_LEFT: ++updateLeft_; break; - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_RIGHT: ++updateRight_; break; @@ -139,11 +153,11 @@ void SyncStatistics::processLink(const SymlinkPair& symlink) { switch (symlink.getSyncOperation()) //evaluate comparison result and sync direction { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: ++createLeft_; break; - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: ++createRight_; break; @@ -156,19 +170,18 @@ void SyncStatistics::processLink(const SymlinkPair& symlink) break; case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: + case SO_RENAME_LEFT: ++updateLeft_; break; case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_RIGHT: ++updateRight_; break; case SO_UNRESOLVED_CONFLICT: ++conflictCount_; - if (conflictsPreview_.size() < CONFLICTS_PREVIEW_MAX) - conflictsPreview_.push_back({symlink.getRelativePathAny(), symlink.getSyncOpConflict()}); + logConflict(symlink); break; case SO_MOVE_LEFT_FROM: @@ -189,11 +202,11 @@ void SyncStatistics::processFolder(const FolderPair& folder) { switch (folder.getSyncOperation()) //evaluate comparison result and sync direction { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: ++createLeft_; break; - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: ++createRight_; break; @@ -207,20 +220,19 @@ void SyncStatistics::processFolder(const FolderPair& folder) case SO_UNRESOLVED_CONFLICT: ++conflictCount_; - if (conflictsPreview_.size() < CONFLICTS_PREVIEW_MAX) - conflictsPreview_.push_back({folder.getRelativePathAny(), folder.getSyncOpConflict()}); + logConflict(folder); break; - case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: + case SO_RENAME_LEFT: ++updateLeft_; break; - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_RIGHT: ++updateRight_; break; + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: case SO_MOVE_LEFT_FROM: case SO_MOVE_RIGHT_FROM: case SO_MOVE_LEFT_TO: @@ -254,17 +266,17 @@ public: } private: - void recurse(const ContainerObject& hierObj) + void recurse(const ContainerObject& conObj) { //process files - for (const FilePair& file : hierObj.refSubFiles()) + for (const FilePair& file : conObj.refSubFiles()) switch (file.getSyncOperation()) //evaluate comparison result and sync direction { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: spaceNeededLeft_ += static_cast<int64_t>(file.getFileSize<SelectSide::right>()); break; - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: spaceNeededRight_ += static_cast<int64_t>(file.getFileSize<SelectSide::left>()); break; @@ -293,8 +305,8 @@ private: case SO_DO_NOTHING: case SO_EQUAL: case SO_UNRESOLVED_CONFLICT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: case SO_MOVE_LEFT_FROM: case SO_MOVE_RIGHT_FROM: case SO_MOVE_LEFT_TO: @@ -306,7 +318,7 @@ private: //[...] //recurse into sub-dirs - for (const FolderPair& folder : hierObj.refSubFolders()) + for (const FolderPair& folder : conObj.refSubFolders()) switch (folder.getSyncOperation()) { case SO_DELETE_LEFT: @@ -318,18 +330,18 @@ private: recurse(folder); break; + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: case SO_MOVE_LEFT_FROM: case SO_MOVE_RIGHT_FROM: case SO_MOVE_LEFT_TO: case SO_MOVE_RIGHT_TO: assert(false); [[fallthrough]]; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_OVERWRITE_LEFT: - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: case SO_DO_NOTHING: case SO_EQUAL: case SO_UNRESOLVED_CONFLICT: @@ -359,9 +371,8 @@ std::vector<FolderPairSyncCfg> fff::extractSyncCfg(const MainConfiguration& main output.push_back( { - syncCfg.directionCfg.var, - syncCfg.directionCfg.var == SyncVariant::twoWay || detectMovedFilesEnabled(syncCfg.directionCfg), - + getSyncVariant(syncCfg.directionCfg), + !!std::get_if<DirectionByChange>(&syncCfg.directionCfg.dirs), syncCfg.deletionVariant, syncCfg.versioningFolderPhrase, syncCfg.versioningStyle, @@ -377,36 +388,6 @@ std::vector<FolderPairSyncCfg> fff::extractSyncCfg(const MainConfiguration& main namespace { -inline -std::optional<SelectSide> getTargetDirection(SyncOperation syncOp) -{ - switch (syncOp) - { - case SO_CREATE_NEW_LEFT: - case SO_DELETE_LEFT: - case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: - case SO_MOVE_LEFT_FROM: - case SO_MOVE_LEFT_TO: - return SelectSide::left; - - case SO_CREATE_NEW_RIGHT: - case SO_DELETE_RIGHT: - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: - case SO_MOVE_RIGHT_FROM: - case SO_MOVE_RIGHT_TO: - return SelectSide::right; - - case SO_DO_NOTHING: - case SO_EQUAL: - case SO_UNRESOLVED_CONFLICT: - break; //nothing to do - } - return {}; -} - - //test if user accidentally selected the wrong folders to sync bool significantDifferenceDetected(const SyncStatistics& folderPairStat) { @@ -431,10 +412,15 @@ bool significantDifferenceDetected(const SyncStatistics& folderPairStat) template <SelectSide side> bool plannedWriteAccess(const FileSystemObject& fsObj) { - if (std::optional<SelectSide> dir = getTargetDirection(fsObj.getSyncOperation())) - return side == *dir; - else - return false; + switch (getEffectiveSyncDir(fsObj.getSyncOperation())) + { + //*INDENT-OFF* + case SyncDirection::none: return false; + case SyncDirection::left: return side == SelectSide::left; + case SyncDirection::right: return side == SelectSide::right; + //*INDENT-ON* + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); } @@ -470,12 +456,15 @@ std::weak_ordering comparePathNoCase(const PathRaceItem& lhs, const PathRaceItem std::wstring formatRaceItem(const PathRaceItem& item) { - const std::optional<SelectSide> syncDir = getTargetDirection(item.fsObj->getSyncOperation()); + const SyncDirection syncDir = getEffectiveSyncDir(item.fsObj->getSyncOperation()); + + const bool writeAcess = (syncDir == SyncDirection::left && item.side == SelectSide::left) || + (syncDir == SyncDirection::right && item.side == SelectSide::right); return AFS::getDisplayPath(item.side == SelectSide::left ? item.fsObj->base().getAbstractPath<SelectSide::left>() : item.fsObj->base().getAbstractPath<SelectSide::right>()) + - (syncDir && *syncDir == item.side ? L" 💾 " : L" 👓 ") + + (writeAcess ? L" 💾 " : L" 👓 ") + utfTo<std::wstring>(item.side == SelectSide::left ? item.fsObj->getRelativePath<SelectSide::left>() : item.fsObj->getRelativePath<SelectSide::right>()); @@ -512,16 +501,16 @@ private: GetChildItemsHashed() {} - void recurse(const ContainerObject& hierObj, uint64_t parentPathHash) + void recurse(const ContainerObject& conObj, uint64_t parentPathHash) { - for (const FilePair& file : hierObj.refSubFiles()) + for (const FilePair& file : conObj.refSubFiles()) childPathRefs_.push_back({&file, getPathHash(file, parentPathHash)}); //S1 -> T (update) is not a conflict (anymore) if S1, S2 contain different files //S2 -> T (update) https://freefilesync.org/forum/viewtopic.php?t=9365#p36466 - for (const SymlinkPair& symlink : hierObj.refSubLinks()) + for (const SymlinkPair& symlink : conObj.refSubLinks()) childPathRefs_.push_back({&symlink, getPathHash(symlink, parentPathHash)}); - for (const FolderPair& subFolder : hierObj.refSubFolders()) + for (const FolderPair& subFolder : conObj.refSubFolders()) { const uint64_t folderPathHash = getPathHash(subFolder, parentPathHash); @@ -1208,11 +1197,11 @@ private: }; -template <class List> inline -bool haveNameClash(const Zstring& itemName, const List& m) +template <SelectSide side, class List> inline +bool haveNameClash(const FileSystemObject& fsObj, const List& m) { - return std::any_of(m.begin(), m.end(), - [&](const typename List::value_type& obj) { return equalNoCase(obj.getItemNameAny(), itemName); }); //equalNoCase: when in doubt => assume name clash! + return std::any_of(m.begin(), m.end(), [itemName = fsObj.getItemName<side>()](const FileSystemObject& sibling) + { return equalNoCase(sibling.getItemName<side>(), itemName); }); //equalNoCase: when in doubt => assume name clash! } @@ -1310,7 +1299,9 @@ private: const std::wstring txtUpdatingFile_ {_("Updating file %x" )}; const std::wstring txtUpdatingLink_ {_("Updating symbolic link %x")}; const std::wstring txtVerifyingFile_ {_("Verifying file %x" )}; - const std::wstring txtUpdatingAttributes_{_("Updating attributes of %x")}; + const std::wstring txtRenamingFileXtoY_ {_("Renaming file %x to %y" )}; + const std::wstring txtRenamingLinkXtoY_ {_("Renaming symbolic link %x to %y")}; + const std::wstring txtRenamingFolderXtoY_{_("Renaming folder %x to %y" )}; const std::wstring txtMovingFileXtoY_ {_("Moving file %x to %y" )}; const std::wstring txtSourceItemNotExist_{_("Source item %x is not existing")}; }; @@ -1381,7 +1372,7 @@ RingBuffer<Workload::WorkItems> FolderPairSyncer::getFolderLevelWorkItems(PassNo while (!foldersToInspect.empty()) { - ContainerObject& hierObj = *foldersToInspect. front(); + ContainerObject& conObj = *foldersToInspect. front(); /**/ foldersToInspect.pop_front(); RingBuffer<std::function<void()>> workItems; @@ -1389,10 +1380,8 @@ RingBuffer<Workload::WorkItems> FolderPairSyncer::getFolderLevelWorkItems(PassNo if (pass == PassNo::zero) { //create folders as required by file move targets: - for (FolderPair& folder : hierObj.refSubFolders()) - if (needZeroPass(folder) && - !haveNameClash(folder.getItemNameAny(), folder.parent().refSubFiles()) && //name clash with files/symlinks? obscure => skip folder creation - !haveNameClash(folder.getItemNameAny(), folder.parent().refSubLinks())) // => move: fall back to delete + copy + for (FolderPair& folder : conObj.refSubFolders()) + if (needZeroPass(folder)) workItems.push_back([this, &folder, &workload, pass] { tryReportingError([&] { synchronizeFolder(folder); }, acb_); //throw ThreadStopRequest @@ -1402,14 +1391,14 @@ RingBuffer<Workload::WorkItems> FolderPairSyncer::getFolderLevelWorkItems(PassNo else foldersToInspect.push_back(&folder); - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) if (needZeroPass(file)) workItems.push_back([this, &file] { executeFileMove(file); /*throw ThreadStopRequest*/ }); } else { //synchronize folders *first* (see comment above "Multithreaded File Copy") - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) if (pass == getPass(folder)) workItems.push_back([this, &folder, &workload, pass] { @@ -1421,7 +1410,7 @@ RingBuffer<Workload::WorkItems> FolderPairSyncer::getFolderLevelWorkItems(PassNo foldersToInspect.push_back(&folder); //synchronize files: - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) if (pass == getPass(file)) workItems.push_back([this, &file] { @@ -1429,7 +1418,7 @@ RingBuffer<Workload::WorkItems> FolderPairSyncer::getFolderLevelWorkItems(PassNo }); //synchronize symbolic links: - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) if (pass == getPass(symlink)) workItems.push_back([this, &symlink] { @@ -1484,21 +1473,17 @@ void FolderPairSyncer::executeFileMoveImpl(FilePair& fileFrom, FilePair& fileTo) if (parentMissing) { - reportItemInfo(_("Cannot move file %x to %y.") + L"\n\n" + - replaceCpy(_("Parent folder %x is not existing."), L"%x", fmtPath(AFS::getDisplayPath(parentMissing->getAbstractPath<side>()))), - fileFrom.getAbstractPath<side>(), - fileTo .getAbstractPath<side>()); //throw ThreadStopRequest + reportInfo(AFS::generateMoveErrorMsg(fileFrom.getAbstractPath<side>(), fileTo.getAbstractPath<side>()) + L"\n\n" + + replaceCpy(_("Parent folder %x is not existing."), L"%x", fmtPath(AFS::getDisplayPath(parentMissing->getAbstractPath<side>()))), acb_); //throw ThreadStopRequest return true; } //name clash with folders/symlinks? obscure => fall back to delete + copy - if (haveNameClash(fileTo.getItemNameAny(), fileTo.parent().refSubFolders()) || - haveNameClash(fileTo.getItemNameAny(), fileTo.parent().refSubLinks ())) + if (haveNameClash<side>(fileTo, fileTo.parent().refSubFolders()) || + haveNameClash<side>(fileTo, fileTo.parent().refSubLinks ())) { - reportItemInfo(_("Cannot move file %x to %y.") + L"\n\n" + - replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(fileTo.getItemNameAny())), - fileFrom.getAbstractPath<side>(), - fileTo .getAbstractPath<side>()); //throw ThreadStopRequest + reportInfo(AFS::generateMoveErrorMsg(fileFrom.getAbstractPath<side>(), fileTo.getAbstractPath<side>()) + L"\n\n" + + replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(fileTo.getItemName<side>())), acb_); //throw ThreadStopRequest return true; } @@ -1557,16 +1542,16 @@ void FolderPairSyncer::executeFileMove(FilePair& file) //throw ThreadStopRequest else assert(false); break; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: case SO_DELETE_LEFT: case SO_DELETE_RIGHT: case SO_MOVE_LEFT_FROM: //don't try to move more than *once* per pair case SO_MOVE_RIGHT_FROM: // case SO_OVERWRITE_LEFT: case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: case SO_DO_NOTHING: case SO_EQUAL: case SO_UNRESOLVED_CONFLICT: @@ -1595,24 +1580,30 @@ bool FolderPairSyncer::needZeroPass(const FolderPair& folder) { switch (folder.getSyncOperation()) { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - return containsMoveTarget(folder); //recursive! watch perf! + case SO_CREATE_LEFT: + return containsMoveTarget(folder) && //recursive! watch perf! + !haveNameClash<SelectSide::left>(folder, folder.parent().refSubFiles()) && //name clash with files/symlinks? obscure => skip folder creation + !haveNameClash<SelectSide::left>(folder, folder.parent().refSubLinks()); // => move: fall back to delete + copy + + case SO_CREATE_RIGHT: + return containsMoveTarget(folder) && //recursive! watch perf! + !haveNameClash<SelectSide::right>(folder, folder.parent().refSubFiles()) && //name clash with files/symlinks? obscure => skip folder creation + !haveNameClash<SelectSide::right>(folder, folder.parent().refSubLinks()); // => move: fall back to delete + copy case SO_DO_NOTHING: //implies !isEmpty<side>(); see FolderPair::getSyncOperation() case SO_UNRESOLVED_CONFLICT: // case SO_EQUAL: - case SO_OVERWRITE_LEFT: //possible: e.g. manually-resolved dir-traversal conflict - case SO_OVERWRITE_RIGHT: // - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: - assert((!folder.isEmpty<SelectSide::left>() && !folder.isEmpty<SelectSide::right>()) || !containsMoveTarget(folder)); + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + assert(!containsMoveTarget(folder) || (!folder.isEmpty<SelectSide::left>() && !folder.isEmpty<SelectSide::right>())); //we're good to move contained items break; case SO_DELETE_LEFT: //not possible in the context of planning to move a child item, see FolderPair::getSyncOperation() case SO_DELETE_RIGHT: // assert(!containsMoveTarget(folder)); break; + case SO_OVERWRITE_LEFT: // + case SO_OVERWRITE_RIGHT: // case SO_MOVE_LEFT_FROM: // case SO_MOVE_RIGHT_FROM: //status not possible for folder case SO_MOVE_LEFT_TO: // @@ -1633,16 +1624,16 @@ bool FolderPairSyncer::needZeroPass(const FilePair& file) case SO_MOVE_RIGHT_TO: return true; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: case SO_DELETE_LEFT: case SO_DELETE_RIGHT: case SO_MOVE_LEFT_FROM: //don't try to move more than *once* per pair case SO_MOVE_RIGHT_FROM: // case SO_OVERWRITE_LEFT: case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: case SO_DO_NOTHING: case SO_EQUAL: case SO_UNRESOLVED_CONFLICT: @@ -1678,10 +1669,10 @@ FolderPairSyncer::PassNo FolderPairSyncer::getPass(const FilePair& file) case SO_MOVE_RIGHT_TO: //make sure 2-step move is processed in second pass, after move *target* parent directory was created! return PassNo::two; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: return PassNo::two; case SO_DO_NOTHING: @@ -1705,10 +1696,10 @@ FolderPairSyncer::PassNo FolderPairSyncer::getPass(const SymlinkPair& symlink) case SO_OVERWRITE_LEFT: case SO_OVERWRITE_RIGHT: - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: return PassNo::two; case SO_MOVE_LEFT_FROM: @@ -1736,14 +1727,14 @@ FolderPairSyncer::PassNo FolderPairSyncer::getPass(const FolderPair& folder) case SO_DELETE_RIGHT: return PassNo::one; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_OVERWRITE_LEFT: - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: return PassNo::two; + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: case SO_MOVE_LEFT_FROM: case SO_MOVE_RIGHT_FROM: case SO_MOVE_LEFT_TO: @@ -1767,9 +1758,10 @@ void FolderPairSyncer::synchronizeFile(FilePair& file) //throw FileError, ErrorM assert(isLocked(singleThread_)); const SyncOperation syncOp = file.getSyncOperation(); - if (std::optional<SelectSide> sideTrg = getTargetDirection(syncOp)) + if (const SyncDirection syncDir = getEffectiveSyncDir(syncOp); + syncDir != SyncDirection::none) { - if (*sideTrg == SelectSide::left) + if (syncDir == SyncDirection::left) synchronizeFileInt<SelectSide::left>(file, syncOp); else synchronizeFileInt<SelectSide::right>(file, syncOp); @@ -1785,8 +1777,8 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) switch (syncOp) { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: { if (auto parentFolder = dynamic_cast<const FolderPair*>(&file.parent())) if (parentFolder->isEmpty<sideTrg>()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() @@ -1809,7 +1801,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) statReporter.reportDelta(1, 0); //update FilePair - file.setSyncedTo<sideTrg>(file.getItemName<sideSrc>(), result.fileSize, + file.setSyncedTo<sideTrg>(result.fileSize, result.modTime, //target time set from source result.modTime, result.targetFilePrint, @@ -1883,8 +1875,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //update FilePair assert(fileFrom->getFileSize<sideTrg>() == fileTo->getFileSize<sideSrc>()); - fileTo->setSyncedTo<sideTrg>(fileTo ->getItemName<sideSrc>(), - fileTo ->getFileSize<sideSrc>(), + fileTo->setSyncedTo<sideTrg>(fileTo ->getFileSize<sideSrc>(), fileFrom->getLastWriteTime<sideTrg>(), fileTo ->getLastWriteTime<sideSrc>(), fileFrom->getFilePrint<sideTrg>(), @@ -1913,8 +1904,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) AsyncItemStatReporter statReporter(1, file.getFileSize<sideSrc>(), acb_); if (file.isFollowedSymlink<sideTrg>()) //since we follow the link, we need to sync case sensitivity of the link manually! - if (getUnicodeNormalForm(file.getItemName<sideTrg>()) != - getUnicodeNormalForm(file.getItemName<sideSrc>())) //have difference in case? + if (!file.hasEquivalentItemNames()) //already existing: undefined behavior! (e.g. fail/overwrite) parallel::moveAndRenameItem(file.getAbstractPath<sideTrg>(), targetPathLogical, singleThread_); //throw FileError, (ErrorMoveUnsupported) @@ -1943,7 +1933,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //we model "delete + copy" as ONE logical operation //update FilePair - file.setSyncedTo<sideTrg>(file.getItemName<sideSrc>(), result.fileSize, + file.setSyncedTo<sideTrg>(result.fileSize, result.modTime, //target time set from source result.modTime, result.targetFilePrint, @@ -1956,15 +1946,15 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) } break; - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: //harmonize with file_hierarchy.cpp::getSyncOpDescription!! - reportItemInfo(txtUpdatingAttributes_, file.getAbstractPath<sideTrg>()); //throw ThreadStopRequest + reportInfo(replaceCpy(replaceCpy(txtRenamingFileXtoY_, L"%x", fmtPath(AFS::getDisplayPath(file.getAbstractPath<sideTrg>()))), + L"%y", fmtPath(file.getItemName<sideSrc>())), acb_); //throw ThreadStopRequest { AsyncItemStatReporter statReporter(1, 0, acb_); - if (getUnicodeNormalForm(file.getItemName<sideTrg>()) != - getUnicodeNormalForm(file.getItemName<sideSrc>())) //have difference in case? + if (!file.hasEquivalentItemNames()) //already existing: undefined behavior! (e.g. fail/overwrite) parallel::moveAndRenameItem(file.getAbstractPath<sideTrg>(), //throw FileError, (ErrorMoveUnsupported) AFS::appendRelPath(file.parent().getAbstractPath<sideTrg>(), file.getItemName<sideSrc>()), singleThread_); @@ -1982,7 +1972,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //-> both sides *should* be completely equal now... assert(file.getFileSize<sideTrg>() == file.getFileSize<sideSrc>()); - file.setSyncedTo<sideTrg>(file.getItemName<sideSrc>(), file.getFileSize<sideSrc>(), + file.setSyncedTo<sideTrg>(file.getFileSize<sideSrc>(), file.getLastWriteTime <sideTrg>(), file.getLastWriteTime <sideSrc>(), file.getFilePrint <sideTrg>(), @@ -2009,9 +1999,10 @@ void FolderPairSyncer::synchronizeLink(SymlinkPair& symlink) //throw FileError, assert(isLocked(singleThread_)); const SyncOperation syncOp = symlink.getSyncOperation(); - if (std::optional<SelectSide> sideTrg = getTargetDirection(syncOp)) + if (const SyncDirection syncDir = getEffectiveSyncDir(syncOp); + syncDir != SyncDirection::none) { - if (*sideTrg == SelectSide::left) + if (syncDir == SyncDirection::left) synchronizeLinkInt<SelectSide::left>(symlink, syncOp); else synchronizeLinkInt<SelectSide::right>(symlink, syncOp); @@ -2027,8 +2018,8 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy switch (syncOp) { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: { if (auto parentFolder = dynamic_cast<const FolderPair*>(&symlink.parent())) if (parentFolder->isEmpty<sideTrg>()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() @@ -2045,8 +2036,7 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy statReporter.reportDelta(1, 0); //update SymlinkPair - symlink.setSyncedTo<sideTrg>(symlink.getItemName<sideSrc>(), - symlink.getLastWriteTime<sideSrc>(), //target time set from source + symlink.setSyncedTo<sideTrg>(symlink.getLastWriteTime<sideSrc>(), //target time set from source symlink.getLastWriteTime<sideSrc>()); } @@ -2103,36 +2093,29 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy statReporter.reportDelta(1, 0); //we model "delete + copy" as ONE logical operation //update SymlinkPair - symlink.setSyncedTo<sideTrg>(symlink.getItemName<sideSrc>(), - symlink.getLastWriteTime<sideSrc>(), //target time set from source + symlink.setSyncedTo<sideTrg>(symlink.getLastWriteTime<sideSrc>(), //target time set from source symlink.getLastWriteTime<sideSrc>()); } break; - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: - reportItemInfo(txtUpdatingAttributes_, symlink.getAbstractPath<sideTrg>()); //throw ThreadStopRequest + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + reportInfo(replaceCpy(replaceCpy(txtRenamingLinkXtoY_, L"%x", fmtPath(AFS::getDisplayPath(symlink.getAbstractPath<sideTrg>()))), + L"%y", fmtPath(symlink.getItemName<sideSrc>())), acb_); //throw ThreadStopRequest { AsyncItemStatReporter statReporter(1, 0, acb_); - if (getUnicodeNormalForm(symlink.getItemName<sideTrg>()) != - getUnicodeNormalForm(symlink.getItemName<sideSrc>())) //have difference in case? + if (!symlink.hasEquivalentItemNames()) //already existing: undefined behavior! (e.g. fail/overwrite) parallel::moveAndRenameItem(symlink.getAbstractPath<sideTrg>(), //throw FileError, (ErrorMoveUnsupported) AFS::appendRelPath(symlink.parent().getAbstractPath<sideTrg>(), symlink.getItemName<sideSrc>()), singleThread_); else assert(false); - //if (symlink.getLastWriteTime<sideTrg>() != symlink.getLastWriteTime<sideSrc>()) - // //- no need to call sameFileTime() or respect 2 second FAT/FAT32 precision in this comparison - // //- do NOT read *current* source file time, but use buffered value which corresponds to time of comparison! - // parallel::setModTimeSymlink(symlink.getAbstractPath<sideTrg>(), symlink.getLastWriteTime<sideSrc>()); //throw FileError - statReporter.reportDelta(1, 0); //-> both sides *should* be completely equal now... - symlink.setSyncedTo<sideTrg>(symlink.getItemName<sideSrc>(), - symlink.getLastWriteTime<sideTrg>(), //target time set from source + symlink.setSyncedTo<sideTrg>(symlink.getLastWriteTime<sideTrg>(), //target time set from source symlink.getLastWriteTime<sideSrc>()); } break; @@ -2156,9 +2139,10 @@ void FolderPairSyncer::synchronizeFolder(FolderPair& folder) //throw FileError, assert(isLocked(singleThread_)); const SyncOperation syncOp = folder.getSyncOperation(); - if (std::optional<SelectSide> sideTrg = getTargetDirection(syncOp)) + if (const SyncDirection syncDir = getEffectiveSyncDir(syncOp); + syncDir != SyncDirection::none) { - if (*sideTrg == SelectSide::left) + if (syncDir == SyncDirection::left) synchronizeFolderInt<SelectSide::left>(folder, syncOp); else synchronizeFolderInt<SelectSide::right>(folder, syncOp); @@ -2174,8 +2158,8 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy switch (syncOp) { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: { if (auto parentFolder = dynamic_cast<const FolderPair*>(&folder.parent())) if (parentFolder->isEmpty<sideTrg>()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() @@ -2209,8 +2193,7 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy statReporter.reportDelta(1, 0); //update FolderPair - folder.setSyncedTo<sideTrg>(folder.getItemName<sideSrc>(), - false, //isSymlinkTrg + folder.setSyncedTo<sideTrg>(false, //isSymlinkTrg folder.isFollowedSymlink<sideSrc>()); } else //source deleted meanwhile... @@ -2253,32 +2236,30 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy } break; - case SO_OVERWRITE_LEFT: //possible: e.g. manually-resolved dir-traversal conflict - case SO_OVERWRITE_RIGHT: // - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: - reportItemInfo(txtUpdatingAttributes_, folder.getAbstractPath<sideTrg>()); //throw ThreadStopRequest + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: + reportInfo(replaceCpy(replaceCpy(txtRenamingFolderXtoY_, L"%x", fmtPath(AFS::getDisplayPath(folder.getAbstractPath<sideTrg>()))), + L"%y", fmtPath(folder.getItemName<sideSrc>())), acb_); //throw ThreadStopRequest { AsyncItemStatReporter statReporter(1, 0, acb_); - if (getUnicodeNormalForm(folder.getItemName<sideTrg>()) != - getUnicodeNormalForm(folder.getItemName<sideSrc>())) //have difference in case? + if (!folder.hasEquivalentItemNames()) //already existing: undefined behavior! (e.g. fail/overwrite) parallel::moveAndRenameItem(folder.getAbstractPath<sideTrg>(), //throw FileError, (ErrorMoveUnsupported) AFS::appendRelPath(folder.parent().getAbstractPath<sideTrg>(), folder.getItemName<sideSrc>()), singleThread_); else assert(false); - //copyFileTimes -> useless: modification time changes with each child-object creation/deletion statReporter.reportDelta(1, 0); //-> both sides *should* be completely equal now... - folder.setSyncedTo<sideTrg>(folder.getItemName<sideSrc>(), - folder.isFollowedSymlink<sideTrg>(), + folder.setSyncedTo<sideTrg>(folder.isFollowedSymlink<sideTrg>(), folder.isFollowedSymlink<sideSrc>()); } break; + case SO_OVERWRITE_LEFT: + case SO_OVERWRITE_RIGHT: case SO_MOVE_LEFT_FROM: case SO_MOVE_RIGHT_FROM: case SO_MOVE_LEFT_TO: @@ -2415,7 +2396,7 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, Phas { //create target directory: user presumably ignored warning "dir not yet existing" in order to have it created automatically const AbstractPath folderPath = baseFolder.getAbstractPath<side>(); - static const SelectSide sideSrc = getOtherSide<side>; + constexpr SelectSide sideSrc = getOtherSide<side>; const std::wstring errMsg = tryReportingError([&] { @@ -2509,7 +2490,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //-------------------execute basic checks all at once BEFORE starting sync-------------------------------------- std::vector<unsigned char /*we really want bool*/> skipFolderPair(folderCmp.size(), false); //folder pairs may be skipped after fatal errors were found - std::vector<std::tuple<const BaseFolderPair*, int /*conflict count*/, std::vector<SyncStatistics::ConflictInfo>>> checkUnresolvedConflicts; + std::vector<std::tuple<const BaseFolderPair*, int /*conflict count*/, std::vector<std::wstring>>> checkUnresolvedConflicts; std::vector<std::tuple<const BaseFolderPair*, SelectSide, bool /*write access*/>> checkBaseFolderRaceCondition; @@ -2664,7 +2645,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime if (!checkUnresolvedConflicts.empty()) { //distribute CONFLICTS_PREVIEW_MAX over all pairs, not *per* pair, or else log size with many folder pairs can blow up! - std::vector<std::vector<SyncStatistics::ConflictInfo>> conflictPreviewTrim(checkUnresolvedConflicts.size()); + std::vector<std::vector<std::wstring>> conflictPreviewTrim(checkUnresolvedConflicts.size()); size_t previewRemain = CONFLICTS_PREVIEW_MAX; for (size_t i = 0; ; ++i) @@ -2696,8 +2677,8 @@ break2: AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::left >()) + L" <-> " + AFS::getDisplayPath(baseFolder->getAbstractPath<SelectSide::right>()); - for (const SyncStatistics::ConflictInfo& item : *itPrevi) - msg += L'\n' + utfTo<std::wstring>(item.relPath) + L": " + item.msg; + for (const std::wstring& conflictMsg : *itPrevi) + msg += L'\n' + conflictMsg; if (makeUnsigned(conflictCount) > itPrevi->size()) msg += L"\n [...] " + replaceCpy(_P("Showing %y of 1 item", "Showing %y of %x items", conflictCount), //%x used as plural form placeholder! @@ -2925,7 +2906,7 @@ break2: }); //guarantee removal of invalid entries (where element is empty on both sides) - ZEN_ON_SCOPE_EXIT(BaseFolderPair::removeEmpty(baseFolder)); + ZEN_ON_SCOPE_EXIT(baseFolder.removeDoubleEmpty()); bool copyPermissionsFp = false; tryReportingError([&] diff --git a/FreeFileSync/Source/base/synchronization.h b/FreeFileSync/Source/base/synchronization.h index 0e790f40..063563bc 100644 --- a/FreeFileSync/Source/base/synchronization.h +++ b/FreeFileSync/Source/base/synchronization.h @@ -20,7 +20,7 @@ class SyncStatistics //count *logical* operations, (create, update, delete + byt //-> note the fundamental difference compared to counting disk accesses! public: explicit SyncStatistics(const FolderComparison& folderCmp); - explicit SyncStatistics(const ContainerObject& hierObj); + explicit SyncStatistics(const ContainerObject& conObj); explicit SyncStatistics(const FilePair& file); template <SelectSide side> @@ -38,16 +38,12 @@ public: int64_t getBytesToProcess() const { return bytesToProcess_; } size_t rowCount () const { return rowsTotal_; } - struct ConflictInfo - { - Zstring relPath; - std::wstring msg; - }; - const std::vector<ConflictInfo>& getConflictsPreview() const { return conflictsPreview_; } + const std::vector<std::wstring>& getConflictsPreview() const { return conflictsPreview_; } int conflictCount() const { return conflictCount_; } private: - void recurse(const ContainerObject& hierObj); + void recurse(const ContainerObject& conObj); + void logConflict(const FileSystemObject& fsObj); void processFile (const FilePair& file); void processLink (const SymlinkPair& symlink); @@ -64,7 +60,7 @@ private: size_t rowsTotal_ = 0; int conflictCount_ = 0; - std::vector<ConflictInfo> conflictsPreview_; //conflict texts to display as a warning message + std::vector<std::wstring> conflictsPreview_; //conflict texts to display as a warning message //limit conflict count! e.g. there may be hundred thousands of "same date but a different size" }; diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp index c3822d65..3417bebc 100644 --- a/FreeFileSync/Source/base/versioning.cpp +++ b/FreeFileSync/Source/base/versioning.cpp @@ -282,8 +282,11 @@ void FileVersioner::revisionFolderImpl(const AbstractPath& folderPath, const Zst for (const AFS::FileInfo& fileInfo : files) { - const FileDescriptor fileDescr{AFS::appendRelPath(folderPath, fileInfo.itemName), - FileAttributes(fileInfo.modTime, fileInfo.fileSize, fileInfo.filePrint, false /*isFollowedSymlink*/)}; + const FileDescriptor fileDescr + { + .path = AFS::appendRelPath(folderPath, fileInfo.itemName), + .attr = {fileInfo.modTime, fileInfo.fileSize, fileInfo.filePrint, false /*isFollowedSymlink*/}, + }; revisionFileImpl(fileDescr, appendPath(relPath, fileInfo.itemName), onBeforeFileMove, notifyUnbufferedIO); //throw FileError, X } diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index 45c20daa..884be499 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -25,7 +25,7 @@ namespace { //------------------------------------------------------------------------------------------------------------------------------- const int XML_FORMAT_GLOBAL_CFG = 27; //2023-05-13 -const int XML_FORMAT_SYNC_CFG = 19; //2023-06-09 +const int XML_FORMAT_SYNC_CFG = 23; //2023-08-24 //------------------------------------------------------------------------------------------------------------------------------- } @@ -703,44 +703,6 @@ bool readText(const std::string& input, VersioningStyle& value) template <> inline -void writeText(const SyncVariant& value, std::string& output) -{ - switch (value) - { - case SyncVariant::twoWay: - output = "TwoWay"; - break; - case SyncVariant::mirror: - output = "Mirror"; - break; - case SyncVariant::update: - output = "Update"; - break; - case SyncVariant::custom: - output = "Custom"; - break; - } -} - -template <> inline -bool readText(const std::string& input, SyncVariant& value) -{ - const std::string tmp = trimCpy(input); - if (tmp == "TwoWay") - value = SyncVariant::twoWay; - else if (tmp == "Mirror") - value = SyncVariant::mirror; - else if (tmp == "Update") - value = SyncVariant::update; - else if (tmp == "Custom") - value = SyncVariant::custom; - else - return false; - return true; -} - - -template <> inline void writeStruc(const ColAttributesRim& value, XmlElement& output) { output.setAttribute("Type", value.type); @@ -1010,30 +972,100 @@ void readConfig(const XmlIn& in, CompConfig& cmpCfg) } -void readConfig(const XmlIn& in, SyncDirectionConfig& dirCfg) +void readConfig(const XmlIn& in, SyncDirectionConfig& dirCfg, int formatVer) { - in["Variant"](dirCfg.var); - - if (dirCfg.var == SyncVariant::custom) + if (formatVer < 21) //TODO: remove if parameter migration after some time! 2023-08-09 { - XmlIn inCustDir = in["CustomDirections"]; - inCustDir["LeftOnly" ](dirCfg.custom.exLeftSideOnly); - inCustDir["RightOnly" ](dirCfg.custom.exRightSideOnly); - inCustDir["LeftNewer" ](dirCfg.custom.leftNewer); - inCustDir["RightNewer"](dirCfg.custom.rightNewer); - inCustDir["Different" ](dirCfg.custom.different); - inCustDir["Conflict" ](dirCfg.custom.conflict); + std::string varName; + in["Variant"](varName); + trim(varName); + + if (varName == "TwoWay") + dirCfg = getDefaultSyncCfg(SyncVariant::twoWay); + else if (varName == "Mirror") + { + dirCfg = getDefaultSyncCfg(SyncVariant::mirror); + + bool detectMovedFiles = false; + in["DetectMovedFiles"](detectMovedFiles); + if (detectMovedFiles) + { + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs)) + dirCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled + else assert(false); + } + } + else if (varName == "Update") + dirCfg.dirs = DirectionByDiff + { + .leftOnly = SyncDirection::right, + .rightOnly = SyncDirection::none, + .leftNewer = SyncDirection::right, + .rightNewer = SyncDirection::none, //note: will be fixed below for CompareVariant::content/size + }; + else + { + assert(varName == "Custom"); + + dirCfg.dirs = DirectionByDiff(); + + XmlIn inCustDir = in["CustomDirections"]; + inCustDir["LeftOnly" ](std::get<DirectionByDiff>(dirCfg.dirs).leftOnly); + inCustDir["RightOnly" ](std::get<DirectionByDiff>(dirCfg.dirs).rightOnly); + inCustDir["LeftNewer" ](std::get<DirectionByDiff>(dirCfg.dirs).leftNewer); //note: will be fixed below for CompareVariant::content/size + inCustDir["RightNewer"](std::get<DirectionByDiff>(dirCfg.dirs).rightNewer); // + } } - //else - // dirCfg.custom = DirectionSet(); + else if (formatVer < 22) //TODO: remove if parameter migration after some time! 2023-08-20 + { + warn_static("formatVer == 21 was development-internal => get rid, once the betas are out of likely use") - in["DetectMovedFiles"](dirCfg.detectMovedFiles); + if (XmlIn inDir = in["Differences"]) + { + dirCfg.dirs = DirectionByDiff(); + inDir["LeftOnly" ](std::get<DirectionByDiff>(dirCfg.dirs).leftOnly); + inDir["RightOnly" ](std::get<DirectionByDiff>(dirCfg.dirs).rightOnly); + inDir["LeftNewer" ](std::get<DirectionByDiff>(dirCfg.dirs).leftNewer); + inDir["RightNewer"](std::get<DirectionByDiff>(dirCfg.dirs).rightNewer); + } + else + { + assert(in["Changes"]); + dirCfg = getDefaultSyncCfg(SyncVariant::twoWay); + } + } + else + { + if (XmlIn inDirs = in["Differences"]) + { + dirCfg.dirs = DirectionByDiff(); + inDirs.attribute("LeftOnly", std::get<DirectionByDiff>(dirCfg.dirs).leftOnly); + inDirs.attribute("LeftNewer", std::get<DirectionByDiff>(dirCfg.dirs).leftNewer); + inDirs.attribute("RightNewer", std::get<DirectionByDiff>(dirCfg.dirs).rightNewer); + inDirs.attribute("RightOnly", std::get<DirectionByDiff>(dirCfg.dirs).rightOnly); + } + else + { + assert(in["Changes"]); + dirCfg.dirs = DirectionByChange(); + + XmlIn inDirsL = in["Changes"]["Left"]; + inDirsL.attribute("Create", std::get<DirectionByChange>(dirCfg.dirs).left.create); + inDirsL.attribute("Update", std::get<DirectionByChange>(dirCfg.dirs).left.update); + inDirsL.attribute("Delete", std::get<DirectionByChange>(dirCfg.dirs).left.delete_); + + XmlIn inDirsR = in["Changes"]["Right"]; + inDirsR.attribute("Create", std::get<DirectionByChange>(dirCfg.dirs).right.create); + inDirsR.attribute("Update", std::get<DirectionByChange>(dirCfg.dirs).right.update); + inDirsR.attribute("Delete", std::get<DirectionByChange>(dirCfg.dirs).right.delete_); + } + } } void readConfig(const XmlIn& in, SyncConfig& syncCfg, std::map<AfsDevice, size_t>& deviceParallelOps, int formatVer) { - readConfig(in, syncCfg.directionCfg); + readConfig(in, syncCfg.directionCfg, formatVer); in["DeletionPolicy" ](syncCfg.deletionVariant); in["VersioningFolder"](syncCfg.versioningFolderPhrase); @@ -1142,10 +1174,50 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer) else in["Notes"](mainCfg.notes); + + + warn_static("wxwidgets: disable wxUSE_EXCEPTIONS so that we get proper exception stack locations") +#if 0 //e.g. + std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs); + warn_static("why is std::bad_variant_access caught somewhere in wxEntry when thrown at this location!? ") +#endif + + readConfig(in["Compare"], mainCfg.cmpCfg); readConfig(in["Synchronize"], mainCfg.syncCfg, mainCfg.deviceParallelOps, formatVer); + if (formatVer < 20) //TODO: remove if parameter migration after some time! 2023-08-09 + if (mainCfg.cmpCfg.compareVar == CompareVariant::content || + mainCfg.cmpCfg.compareVar == CompareVariant::size) + if (std::string varName; + in["Synchronize"]["Variant"](varName)) + { + if (varName == "Update") + std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs).rightNewer = SyncDirection::right; + else if (varName == "Custom") + { + SyncDirection different = SyncDirection::none; + in["Synchronize"]["CustomDirections"]["Different"](different); + + std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs).leftNewer = + std::get<DirectionByDiff>(mainCfg.syncCfg.directionCfg.dirs).rightNewer = different; + } + } + + if (formatVer < 23) //TODO: remove if parameter migration after some time! 2023-08-24 + { + bool detectMovedFiles = false; + in["Synchronize"]["DetectMovedFiles"](detectMovedFiles); + if (detectMovedFiles) + if (getSyncVariant(mainCfg.syncCfg.directionCfg) == SyncVariant::mirror) + { + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&mainCfg.syncCfg.directionCfg.dirs)) + mainCfg.syncCfg.directionCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled + else assert(false); + } + } + readConfig(in["Filter"], mainCfg.globalFilter); //########################################################### @@ -1158,6 +1230,40 @@ void readConfig(const XmlIn& in, MainConfiguration& mainCfg, int formatVer) LocalPairConfig lpc; readConfig(inPair, lpc, mainCfg.deviceParallelOps, formatVer); + if (formatVer < 20) //TODO: remove if parameter migration after some time! 2023-08-09 + if (lpc.localSyncCfg) + { + const CompConfig& cmpCfg = lpc.localCmpCfg ? *lpc.localCmpCfg : mainCfg.cmpCfg; + if (cmpCfg.compareVar == CompareVariant::content || + cmpCfg.compareVar == CompareVariant::size) + if (std::string varName; + inPair["Synchronize"]["Variant"](varName)) + if (varName == "Update") + std::get<DirectionByDiff>(lpc.localSyncCfg->directionCfg.dirs).rightNewer = SyncDirection::right; + else if (varName == "Custom") + if (inPair["Synchronize"]["CustomDirections"]["Different"]) + { + SyncDirection different = SyncDirection::none; + inPair["Synchronize"]["CustomDirections"]["Different"](different); + + std::get<DirectionByDiff>(lpc.localSyncCfg->directionCfg.dirs).leftNewer = + std::get<DirectionByDiff>(lpc.localSyncCfg->directionCfg.dirs).rightNewer = different; + } + } + if (formatVer < 23) //TODO: remove if parameter migration after some time! 2023-08-24 + if (lpc.localSyncCfg) + { + bool detectMovedFiles = false; + inPair["Synchronize"]["DetectMovedFiles"](detectMovedFiles); + if (detectMovedFiles) + if (getSyncVariant(lpc.localSyncCfg->directionCfg) == SyncVariant::mirror) + { + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&lpc.localSyncCfg->directionCfg.dirs)) + lpc.localSyncCfg->directionCfg.dirs = getChangesDirDefault(*diffDirs); //convert to "changes"-based mirror, so that move detection is enabled + else assert(false); + } + } + if (firstItem) { firstItem = false; @@ -1875,20 +1981,28 @@ void writeConfig(const CompConfig& cmpCfg, XmlOut& out) void writeConfig(const SyncDirectionConfig& dirCfg, XmlOut& out) { - out["Variant"](dirCfg.var); - - if (dirCfg.var == SyncVariant::custom) + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&dirCfg.dirs)) { - XmlOut outCustDir = out["CustomDirections"]; - outCustDir["LeftOnly" ](dirCfg.custom.exLeftSideOnly); - outCustDir["RightOnly" ](dirCfg.custom.exRightSideOnly); - outCustDir["LeftNewer" ](dirCfg.custom.leftNewer); - outCustDir["RightNewer"](dirCfg.custom.rightNewer); - outCustDir["Different" ](dirCfg.custom.different); - outCustDir["Conflict" ](dirCfg.custom.conflict); + XmlOut outDirs = out["Differences"]; + outDirs.attribute("LeftOnly", diffDirs->leftOnly); + outDirs.attribute("LeftNewer", diffDirs->leftNewer); + outDirs.attribute("RightNewer", diffDirs->rightNewer); + outDirs.attribute("RightOnly", diffDirs->rightOnly); } + else + { + const DirectionByChange& changeDirs = std::get<DirectionByChange>(dirCfg.dirs); - out["DetectMovedFiles"](dirCfg.detectMovedFiles); + XmlOut outDirsL = out["Changes"]["Left"]; + outDirsL.attribute("Create", changeDirs.left.create); + outDirsL.attribute("Update", changeDirs.left.update); + outDirsL.attribute("Delete", changeDirs.left.delete_); + + XmlOut outDirsR = out["Changes"]["Right"]; + outDirsR.attribute("Create", changeDirs.right.create); + outDirsR.attribute("Update", changeDirs.right.update); + outDirsR.attribute("Delete", changeDirs.right.delete_); + } } diff --git a/FreeFileSync/Source/icon_buffer.cpp b/FreeFileSync/Source/icon_buffer.cpp index f4851b06..f8325a3b 100644 --- a/FreeFileSync/Source/icon_buffer.cpp +++ b/FreeFileSync/Source/icon_buffer.cpp @@ -165,12 +165,12 @@ public: std::lock_guard dummy(lockIconList_); //thread safety: moving ImageHolder is free from side effects, but ~wxImage() is NOT! => do NOT delete items from iconList here! - auto rc = iconList.emplace(filePath, IconData()); - assert(rc.second); //insertion took place - if (rc.second) + const auto [it, inserted] = iconList.try_emplace(filePath); + assert(inserted); + if (inserted) { - refData(rc.first).iconHolder = std::move(ih); - priorityListPushBack(rc.first); + refData(it).iconHolder = std::move(ih); + priorityListPushBack(it); } } diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index 7c6fa18a..0ca0d841 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -306,9 +306,6 @@ wxLayoutDirection globalLayoutDir = wxLayout_Default; void fff::localizationInit(const Zstring& zipPath) //throw FileError { - assert(globalTranslations.empty()); - globalTranslations = loadTranslations(zipPath); //throw FileError - /* wxLocale vs wxUILocale (since wxWidgets 3.1.6) ------------------------------------------|-------------------- calls setlocale() Windows, Linux, maCOS | Linux only @@ -336,6 +333,11 @@ void fff::localizationInit(const Zstring& zipPath) //throw FileError assert(!wxTranslations::Get()); wxTranslations::Set(new wxTranslations() /*pass ownership*/); //implicitly done by wxLocale, but *not* wxUILocale + //throw *after* mandatory initialization: setLanguage() requires wxTranslations::Get()! + + assert(globalTranslations.empty()); + globalTranslations = loadTranslations(zipPath); //throw FileError + setLanguage(getDefaultLanguage()); //throw FileError } diff --git a/FreeFileSync/Source/parse_lng.h b/FreeFileSync/Source/parse_lng.h index 2c1a7adb..3614db2a 100644 --- a/FreeFileSync/Source/parse_lng.h +++ b/FreeFileSync/Source/parse_lng.h @@ -10,7 +10,6 @@ #include <unordered_map> #include <unordered_set> #include <zen/utf.h> -#include <zen/ring_buffer.h> #include "parse_plural.h" @@ -80,34 +79,20 @@ template<> struct std::hash<lng::SingularPluralPair> namespace lng { -enum class TranslationNewItemPos -{ - rel, - top -}; - class TranslationUnorderedList //unordered list of unique translation items { public: - TranslationUnorderedList(TranslationNewItemPos newItemPos, TranslationMap&& transOld, TranslationPluralMap&& transPluralOld) : - newItemPos_(newItemPos), transOld_(std::move(transOld)), transPluralOld_(std::move(transPluralOld)) {} + TranslationUnorderedList(TranslationMap&& transOld, TranslationPluralMap&& transPluralOld) : + transOld_(std::move(transOld)), transPluralOld_(std::move(transPluralOld)) {} void addItem(const std::string& orig) { if (!transUnique_.insert(orig).second) return; auto it = transOld_.find(orig); if (it != transOld_.end() && !it->second.empty()) //preserve old translation from .lng file if existing - sequence_.push_back(std::make_shared<RegularItem>(std::make_pair(orig, it->second))); + sequence_.push_back(std::make_shared<SingularItem>(std::make_pair(orig, it->second))); else - switch (newItemPos_) - { - case TranslationNewItemPos::rel: - sequence_.push_back(std::make_shared<RegularItem>(std::make_pair(orig, std::string()))); - break; - case TranslationNewItemPos::top: - sequence_.push_front(std::make_shared<RegularItem>(std::make_pair(orig, std::string()))); //put untranslated items to the front of the .lng filebreak; - break; - } + sequence_.push_back(std::make_shared<SingularItem>(std::make_pair(orig, std::string()))); } void addItem(const SingularPluralPair& orig) @@ -117,15 +102,7 @@ public: if (it != transPluralOld_.end() && !it->second.empty()) //preserve old translation from .lng file if existing sequence_.push_back(std::make_shared<PluralItem>(std::make_pair(orig, it->second))); else - switch (newItemPos_) - { - case TranslationNewItemPos::rel: - sequence_.push_back(std::make_shared<PluralItem>(std::make_pair(orig, PluralForms()))); - break; - case TranslationNewItemPos::top: - sequence_.push_front(std::make_shared<PluralItem>(std::make_pair(orig, PluralForms()))); //put untranslated items to the front of the .lng file - break; - } + sequence_.push_back(std::make_shared<PluralItem>(std::make_pair(orig, PluralForms()))); } bool untranslatedTextExists() const { return std::any_of(sequence_.begin(), sequence_.end(), [](const std::shared_ptr<Item>& item) { return !item->hasTranslation(); }); } @@ -134,7 +111,7 @@ public: void visitItems(Function onTrans, Function2 onPluralTrans) const //onTrans takes (const TranslationMap::value_type&), onPluralTrans takes (const TranslationPluralMap::value_type&) { for (const std::shared_ptr<Item>& item : sequence_) - if (auto regular = dynamic_cast<const RegularItem*>(item.get())) + if (auto regular = dynamic_cast<const SingularItem*>(item.get())) onTrans(regular->value); else if (auto plural = dynamic_cast<const PluralItem*>(item.get())) onPluralTrans(plural->value); @@ -143,11 +120,22 @@ public: private: struct Item { virtual ~Item() {} virtual bool hasTranslation() const = 0; }; - struct RegularItem : public Item { RegularItem(const TranslationMap ::value_type& val) : value(val) {} bool hasTranslation() const override { return !value.second.empty(); } TranslationMap ::value_type value; }; - struct PluralItem : public Item { PluralItem (const TranslationPluralMap::value_type& val) : value(val) {} bool hasTranslation() const override { return !value.second.empty(); } TranslationPluralMap::value_type value; }; + + struct SingularItem : public Item + { + explicit SingularItem(const TranslationMap::value_type& val) : value(val) {} + bool hasTranslation() const override { return !value.second.empty(); } + TranslationMap::value_type value; + }; - const TranslationNewItemPos newItemPos_; - zen::RingBuffer<std::shared_ptr<Item>> sequence_; //ordered list of translation elements + struct PluralItem : public Item + { + explicit PluralItem(const TranslationPluralMap::value_type& val) : value(val) {} + bool hasTranslation() const override { return !value.second.empty(); } + TranslationPluralMap::value_type value; + }; + + std::vector<std::shared_ptr<Item>> sequence_; //ordered list of translation elements std::unordered_set<TranslationMap ::key_type> transUnique_; //check uniqueness std::unordered_set<TranslationPluralMap::key_type> pluralUnique_; // @@ -375,7 +363,7 @@ public: consumeToken(TokenType::localeBegin); //throw ParsingError header.locale = token().text; - consumeToken(TokenType::text); //throw ParsingError + consumeToken(TokenType::text); //throw ParsingError consumeToken(TokenType::localeEnd); // consumeToken(TokenType::flagFileBegin); //throw ParsingError @@ -418,7 +406,7 @@ private: validateTranslation(original, translation); //throw ParsingError consumeToken(TokenType::trgEnd); // - out.emplace(original, translation); + out.emplace(original, std::move(translation)); } void parsePlural(TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) @@ -452,7 +440,7 @@ private: validateTranslation(original, pluralList, pluralInfo); consumeToken(TokenType::trgEnd); //throw ParsingError - pluralOut.emplace(original, pluralList); + pluralOut.emplace(original, std::move(pluralList)); } void validateTranslation(const std::string& original, const std::string& translation) //throw ParsingError @@ -740,25 +728,30 @@ void formatMultiLineText(std::string& text) inline -std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header) +std::string generateLng(const TranslationUnorderedList& in, const TransHeader& header, bool untranslatedToTop) { const KnownTokens tokens; //no need for static non-POD! - std::string out; - //header - out += tokens.text(TokenType::headerBegin) + '\n'; + std::string headerLines; + headerLines += tokens.text(TokenType::headerBegin) + '\n'; - out += '\t' + tokens.text(TokenType::langNameBegin) + header.languageName + tokens.text(TokenType::langNameEnd) + '\n'; - out += '\t' + tokens.text(TokenType::transNameBegin) + header.translatorName + tokens.text(TokenType::transNameEnd) + '\n'; - out += '\t' + tokens.text(TokenType::localeBegin) + header.locale + tokens.text(TokenType::localeEnd) + '\n'; - out += '\t' + tokens.text(TokenType::flagFileBegin) + header.flagFile + tokens.text(TokenType::flagFileEnd) + '\n'; - out += '\t' + tokens.text(TokenType::pluralCountBegin) + zen::numberTo<std::string>(header.pluralCount) + tokens.text(TokenType::pluralCountEnd) + '\n'; - out += '\t' + tokens.text(TokenType::pluralDefBegin) + header.pluralDefinition + tokens.text(TokenType::pluralDefEnd) + '\n'; + headerLines += '\t' + tokens.text(TokenType::langNameBegin) + header.languageName + tokens.text(TokenType::langNameEnd) + '\n'; + headerLines += '\t' + tokens.text(TokenType::transNameBegin) + header.translatorName + tokens.text(TokenType::transNameEnd) + '\n'; + headerLines += '\t' + tokens.text(TokenType::localeBegin) + header.locale + tokens.text(TokenType::localeEnd) + '\n'; + headerLines += '\t' + tokens.text(TokenType::flagFileBegin) + header.flagFile + tokens.text(TokenType::flagFileEnd) + '\n'; + headerLines += '\t' + tokens.text(TokenType::pluralCountBegin) + zen::numberTo<std::string>(header.pluralCount) + tokens.text(TokenType::pluralCountEnd) + '\n'; + headerLines += '\t' + tokens.text(TokenType::pluralDefBegin) + header.pluralDefinition + tokens.text(TokenType::pluralDefEnd) + '\n'; - out += tokens.text(TokenType::headerEnd) + "\n\n"; + headerLines += tokens.text(TokenType::headerEnd) + "\n\n"; + + + std::string topLines; //untranslated items first? + std::string mainLines; in.visitItems([&](const TranslationMap::value_type& trans) { + std::string& out = untranslatedToTop && trans.second.empty() ? topLines : mainLines; + std::string original = trans.first; std::string translation = trans.second; @@ -770,6 +763,8 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h }, [&](const TranslationPluralMap::value_type& transPlural) { + std::string& out = untranslatedToTop && transPlural.second.empty() ? topLines : mainLines; + std::string engSingular = transPlural.first.first; std::string engPlural = transPlural.first.second; const PluralForms& forms = transPlural.second; @@ -793,8 +788,9 @@ std::string generateLng(const TranslationUnorderedList& in, const TransHeader& h out += tokens.text(TokenType::trgEnd) + "\n\n"; }); - assert(!zen::contains(out, "\r\n") && !zen::contains(out, "\r")); - return zen::replaceCpy(out, '\n', "\r\n"); //back to win line endings + std::string output = headerLines + topLines + mainLines; + assert(!zen::contains(output, "\r\n") && !zen::contains(output, "\r")); + return zen::replaceCpy(output, '\n', "\r\n"); //back to Windows line endings } } diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index 859c4044..4078a5c0 100644 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -337,7 +337,7 @@ public: static int getRowDefaultHeight(const Grid& grid) { - return std::max(getDefaultMenuIconSize(), grid.getMainWin().GetCharHeight()) + fastFromDIP(1) /*extra space*/; + return std::max(getDefaultMenuIconSize(), grid.getMainWin().GetCharHeight()) + fastFromDIP(1) /*extra space*/; } int getSyncOverdueDays() const { return syncOverdueDays_; } diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index ebadc892..50089617 100644 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -102,43 +102,36 @@ wxColor getDefaultBackgroundColorAlternating(bool wantStandardColor) } -enum class CudAction -{ - doNothing, - create, - update, - destroy, -}; std::pair<CudAction, SelectSide> getCudAction(SyncOperation so) { switch (so) { //*INDENT-OFF* - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: case SO_MOVE_LEFT_TO: return {CudAction::create, SelectSide::left}; - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: case SO_MOVE_RIGHT_TO: return {CudAction::create, SelectSide::right}; case SO_DELETE_LEFT: - case SO_MOVE_LEFT_FROM: return {CudAction::destroy, SelectSide::left}; + case SO_MOVE_LEFT_FROM: return {CudAction::delete_, SelectSide::left}; case SO_DELETE_RIGHT: - case SO_MOVE_RIGHT_FROM: return {CudAction::destroy, SelectSide::right}; + case SO_MOVE_RIGHT_FROM: return {CudAction::delete_, SelectSide::right}; case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: return {CudAction::update, SelectSide::left}; + case SO_RENAME_LEFT: return {CudAction::update, SelectSide::left}; case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: return {CudAction::update, SelectSide::right}; + case SO_RENAME_RIGHT: return {CudAction::update, SelectSide::right}; case SO_DO_NOTHING: case SO_EQUAL: - case SO_UNRESOLVED_CONFLICT: return {CudAction::doNothing, SelectSide::left}; + case SO_UNRESOLVED_CONFLICT: return {CudAction::noChange, SelectSide::left}; //*INDENT-ON* } assert(false); - return {CudAction::doNothing, SelectSide::left}; + return {CudAction::noChange, SelectSide::left}; } @@ -146,20 +139,20 @@ wxColor getBackGroundColorSyncAction(SyncOperation so) { switch (so) { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: case SO_OVERWRITE_LEFT: case SO_DELETE_LEFT: case SO_MOVE_LEFT_FROM: case SO_MOVE_LEFT_TO: - case SO_COPY_METADATA_TO_LEFT: + case SO_RENAME_LEFT: return getColorSyncBlue(false /*faint*/); - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: case SO_OVERWRITE_RIGHT: case SO_DELETE_RIGHT: case SO_MOVE_RIGHT_FROM: case SO_MOVE_RIGHT_TO: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_RIGHT: return getColorSyncGreen(false /*faint*/); case SO_DO_NOTHING: @@ -178,19 +171,19 @@ wxColor getBackGroundColorCmpDifference(CompareFileResult cmpResult) switch (cmpResult) { //*INDENT-OFF* - case FILE_LEFT_SIDE_ONLY: return getColorSyncBlue(false /*faint*/); + case FILE_EQUAL: + break; //usually white + case FILE_LEFT_ONLY: return getColorSyncBlue(false /*faint*/); case FILE_LEFT_NEWER: return getColorSyncBlue(true /*faint*/); - case FILE_RIGHT_SIDE_ONLY: return getColorSyncGreen(false /*faint*/); + case FILE_RIGHT_ONLY: return getColorSyncGreen(false /*faint*/); case FILE_RIGHT_NEWER: return getColorSyncGreen(true /*faint*/); case FILE_DIFFERENT_CONTENT: return getColorDifferentBackground(false /*faint*/); - case FILE_EQUAL: - break; //usually white - + case FILE_RENAMED: //similar to both "equal" and "conflict": give hint via background color + case FILE_TIME_INVALID: case FILE_CONFLICT: - case FILE_DIFFERENT_METADATA: //= sub-category of equal, but hint via background that sync direction follows conflict-setting return getColorConflictBackground(false /*faint*/); //*INDENT-ON* } @@ -348,8 +341,6 @@ public: return it->second; } - int getGroupItemNamesWidth(wxDC& dc, const FileView::PathDrawInfo& pdi); - private: size_t getRowCount() const override { return getDataView().rowsOnView(); } @@ -580,6 +571,9 @@ private: itemNamesWidth = std::max(itemNamesWidth, *itPercentile); } assert(itemNamesWidth >= 0); + + //Note: A better/faster solution would be to get 80th percentile of all std::wstring::size(), then do a *single* getTextExtentBuffered()! + // However, we need all the getTextExtentBuffered(itemName) later anyway, so above is fine. } return itemNamesWidth; } @@ -784,7 +778,7 @@ private: if (getViewType() == GridViewType::action) if (!enabled || !selected) if (const auto& [cudAction, cudSide] = getCudAction(syncOp); - cudAction != CudAction::doNothing && side == cudSide) + cudAction != CudAction::noChange && side == cudSide) { rectCud.width = gapSize_ + IconBuffer::getSize(IconBuffer::IconSize::small); //fixed-size looks fine for all icon sizes! use same width even if file icons are disabled! @@ -856,10 +850,10 @@ private: //too much clutter? => drawIcon(getIconManager().getPlusOverlayIcon(), rectIcon, // true /*drawActive: [!] e.g. disabled folder, exists left only, where child item is copied*/); break; - case CudAction::destroy: + case CudAction::delete_: drawIcon(getIconManager().getMinusOverlayIcon(), rectIcon, true /*drawActive: [!]*/); break; - case CudAction::doNothing: + case CudAction::noChange: case CudAction::update: break; }; @@ -986,7 +980,7 @@ private: rectGroupItems.width -= 2 * gapSize_; wxDCPenChanger dummy(dc, wxPen(getColorGridLine(), fastFromDIP(1))); - dc.DrawLine(rectGroupItems.GetTopLeft(), rectGroupItems.GetBottomLeft() + wxPoint(0, 1)); //doesn't draw last pixel! + dc.DrawLine(rectGroupItems.GetTopLeft(), rectGroupItems.GetBottomLeft() + wxPoint(0, 1)); //DrawLine() doesn't draw last pixel! rectGroupItems.x += fastFromDIP(1); rectGroupItems.width -= fastFromDIP(1); @@ -1629,14 +1623,15 @@ private: { switch (fsObj->getCategory()) { - case FILE_LEFT_SIDE_ONLY: return "cat_left_only"; - case FILE_RIGHT_SIDE_ONLY: return "cat_right_only"; - case FILE_LEFT_NEWER: return "cat_left_newer"; - case FILE_RIGHT_NEWER: return "cat_right_newer"; - case FILE_DIFFERENT_CONTENT: return "cat_different"; - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: return "cat_equal"; //= sub-category of equal - case FILE_CONFLICT: return "cat_conflict"; + case FILE_RENAMED: //similar to both "equal" and "conflict" + case FILE_EQUAL: return "cat_equal"; + case FILE_LEFT_ONLY: return "cat_left_only"; + case FILE_RIGHT_ONLY: return "cat_right_only"; + case FILE_LEFT_NEWER: return "cat_left_newer"; + case FILE_RIGHT_NEWER: return "cat_right_newer"; + case FILE_DIFFERENT_CONTENT: return "cat_different"; + case FILE_TIME_INVALID: + case FILE_CONFLICT: return "cat_conflict"; } assert(false); return ""; @@ -1652,21 +1647,21 @@ private: { switch (fsObj->getSyncOperation()) { - case SO_CREATE_NEW_LEFT: return "so_create_left"; - case SO_CREATE_NEW_RIGHT: return "so_create_right"; - case SO_DELETE_LEFT: return "so_delete_left"; - case SO_DELETE_RIGHT: return "so_delete_right"; - case SO_MOVE_LEFT_FROM: return "so_move_left_source"; - case SO_MOVE_LEFT_TO: return "so_move_left_target"; - case SO_MOVE_RIGHT_FROM: return "so_move_right_source"; - case SO_MOVE_RIGHT_TO: return "so_move_right_target"; - case SO_OVERWRITE_LEFT: return "so_update_left"; - case SO_OVERWRITE_RIGHT: return "so_update_right"; - case SO_COPY_METADATA_TO_LEFT: return "so_move_left"; - case SO_COPY_METADATA_TO_RIGHT: return "so_move_right"; - case SO_DO_NOTHING: return "so_none"; - case SO_EQUAL: return "cat_equal"; - case SO_UNRESOLVED_CONFLICT: return "cat_conflict"; + case SO_CREATE_LEFT: return "so_create_left"; + case SO_CREATE_RIGHT: return "so_create_right"; + case SO_DELETE_LEFT: return "so_delete_left"; + case SO_DELETE_RIGHT: return "so_delete_right"; + case SO_MOVE_LEFT_FROM: return "so_move_left_source"; + case SO_MOVE_LEFT_TO: return "so_move_left_target"; + case SO_MOVE_RIGHT_FROM: return "so_move_right_source"; + case SO_MOVE_RIGHT_TO: return "so_move_right_target"; + case SO_OVERWRITE_LEFT: return "so_update_left"; + case SO_OVERWRITE_RIGHT: return "so_update_right"; + case SO_RENAME_LEFT: return "so_move_left"; + case SO_RENAME_RIGHT: return "so_move_right"; + case SO_DO_NOTHING: return "so_none"; + case SO_EQUAL: return "cat_equal"; + case SO_UNRESOLVED_CONFLICT: return "cat_conflict"; }; assert(false); return ""; @@ -2191,21 +2186,21 @@ wxImage fff::getSyncOpImage(SyncOperation syncOp) switch (syncOp) //evaluate comparison result and sync direction { //*INDENT-OFF* - case SO_CREATE_NEW_LEFT: return loadImage("so_create_left_sicon"); - case SO_CREATE_NEW_RIGHT: return loadImage("so_create_right_sicon"); - case SO_DELETE_LEFT: return loadImage("so_delete_left_sicon"); - case SO_DELETE_RIGHT: return loadImage("so_delete_right_sicon"); - case SO_MOVE_LEFT_FROM: return loadImage("so_move_left_source_sicon"); - case SO_MOVE_LEFT_TO: return loadImage("so_move_left_target_sicon"); - case SO_MOVE_RIGHT_FROM: return loadImage("so_move_right_source_sicon"); - case SO_MOVE_RIGHT_TO: return loadImage("so_move_right_target_sicon"); - case SO_OVERWRITE_LEFT: return loadImage("so_update_left_sicon"); - case SO_OVERWRITE_RIGHT: return loadImage("so_update_right_sicon"); - case SO_COPY_METADATA_TO_LEFT: return loadImage("so_move_left_sicon"); - case SO_COPY_METADATA_TO_RIGHT: return loadImage("so_move_right_sicon"); - case SO_DO_NOTHING: return loadImage("so_none_sicon"); - case SO_EQUAL: return loadImage("cat_equal_sicon"); - case SO_UNRESOLVED_CONFLICT: return loadImage("cat_conflict_small"); + case SO_CREATE_LEFT: return loadImage("so_create_left_sicon"); + case SO_CREATE_RIGHT: return loadImage("so_create_right_sicon"); + case SO_DELETE_LEFT: return loadImage("so_delete_left_sicon"); + case SO_DELETE_RIGHT: return loadImage("so_delete_right_sicon"); + case SO_MOVE_LEFT_FROM: return loadImage("so_move_left_source_sicon"); + case SO_MOVE_LEFT_TO: return loadImage("so_move_left_target_sicon"); + case SO_MOVE_RIGHT_FROM: return loadImage("so_move_right_source_sicon"); + case SO_MOVE_RIGHT_TO: return loadImage("so_move_right_target_sicon"); + case SO_OVERWRITE_LEFT: return loadImage("so_update_left_sicon"); + case SO_OVERWRITE_RIGHT: return loadImage("so_update_right_sicon"); + case SO_RENAME_LEFT: return loadImage("so_move_left_sicon"); + case SO_RENAME_RIGHT: return loadImage("so_move_right_sicon"); + case SO_DO_NOTHING: return loadImage("so_none_sicon"); + case SO_EQUAL: return loadImage("cat_equal_sicon"); + case SO_UNRESOLVED_CONFLICT: return loadImage("cat_conflict_small"); //*INDENT-ON* } assert(false); @@ -2218,14 +2213,15 @@ wxImage fff::getCmpResultImage(CompareFileResult cmpResult) switch (cmpResult) { //*INDENT-OFF* - case FILE_LEFT_SIDE_ONLY: return loadImage("cat_left_only_sicon"); - case FILE_RIGHT_SIDE_ONLY: return loadImage("cat_right_only_sicon"); - case FILE_LEFT_NEWER: return loadImage("cat_left_newer_sicon"); - case FILE_RIGHT_NEWER: return loadImage("cat_right_newer_sicon"); - case FILE_DIFFERENT_CONTENT: return loadImage("cat_different_sicon"); - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: return loadImage("cat_equal_sicon"); //= sub-category of equal - case FILE_CONFLICT: return loadImage("cat_conflict_small"); + case FILE_RENAMED: //similar to both "equal" and "conflict" + case FILE_EQUAL: return loadImage("cat_equal_sicon"); + case FILE_LEFT_ONLY: return loadImage("cat_left_only_sicon"); + case FILE_RIGHT_ONLY: return loadImage("cat_right_only_sicon"); + case FILE_LEFT_NEWER: return loadImage("cat_left_newer_sicon"); + case FILE_RIGHT_NEWER: return loadImage("cat_right_newer_sicon"); + case FILE_DIFFERENT_CONTENT: return loadImage("cat_different_sicon"); + case FILE_TIME_INVALID: + case FILE_CONFLICT: return loadImage("cat_conflict_small"); //*INDENT-ON* } assert(false); diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp index 67544c68..6bcc4a2a 100644 --- a/FreeFileSync/Source/ui/file_view.cpp +++ b/FreeFileSync/Source/ui/file_view.cpp @@ -16,15 +16,15 @@ using namespace fff; namespace { -void serializeHierarchy(ContainerObject& hierObj, std::vector<FileSystemObject::ObjectId>& output) +void serializeHierarchy(ContainerObject& conObj, std::vector<FileSystemObject::ObjectId>& output) { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) output.push_back(file.getId()); - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) output.push_back(symlink.getId()); - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) { output.push_back(folder.getId()); serializeHierarchy(folder, output); //add recursion here to list sub-objects directly below parent! @@ -135,9 +135,9 @@ ptrdiff_t FileView::findRowDirect(FileSystemObject::ObjectIdConst objId) const } -ptrdiff_t FileView::findRowFirstChild(const ContainerObject* hierObj) const +ptrdiff_t FileView::findRowFirstChild(const ContainerObject* conObj) const { - auto it = rowPositionsFirstChild_.find(hierObj); + auto it = rowPositionsFirstChild_.find(conObj); return it != rowPositionsFirstChild_.end() ? it->second : -1; } @@ -213,9 +213,9 @@ FileView::DifferenceViewStats FileView::applyDifferenceFilter(bool showExcluded, switch (fsObj.getCategory()) { - case FILE_LEFT_SIDE_ONLY: + case FILE_LEFT_ONLY: return categorize(showLeftOnly, stats.leftOnly); - case FILE_RIGHT_SIDE_ONLY: + case FILE_RIGHT_ONLY: return categorize(showRightOnly, stats.rightOnly); case FILE_LEFT_NEWER: return categorize(showLeftNewer, stats.leftNewer); @@ -224,9 +224,10 @@ FileView::DifferenceViewStats FileView::applyDifferenceFilter(bool showExcluded, case FILE_DIFFERENT_CONTENT: return categorize(showDifferent, stats.different); case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal return categorize(showEqual, stats.equal); + case FILE_RENAMED: case FILE_CONFLICT: + case FILE_TIME_INVALID: return categorize(showConflict, stats.conflict); } assert(false); @@ -273,22 +274,22 @@ FileView::ActionViewStats FileView::applyActionFilter(bool showExcluded, //maps switch (fsObj.getSyncOperation()) //evaluate comparison result and sync direction { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: return categorize(showCreateLeft, stats.createLeft); - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: return categorize(showCreateRight, stats.createRight); case SO_DELETE_LEFT: return categorize(showDeleteLeft, stats.deleteLeft); case SO_DELETE_RIGHT: return categorize(showDeleteRight, stats.deleteRight); case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: //no extra filter button + case SO_RENAME_LEFT: return categorize(showUpdateLeft, stats.updateLeft); case SO_MOVE_LEFT_FROM: case SO_MOVE_LEFT_TO: return categorize(showUpdateLeft, moveLeft); case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: //no extra filter button + case SO_RENAME_RIGHT: return categorize(showUpdateRight, stats.updateRight); case SO_MOVE_RIGHT_FROM: case SO_MOVE_RIGHT_TO: @@ -414,12 +415,11 @@ bool lessFileName(const FileSystemObject& lhs, const FileSystemObject& rhs) else if (isDirectoryPair(rhs)) return true; - //sort directories and files/symlinks by short name return zen::makeSortDirection(LessNaturalSort() /*even on Linux*/, std::bool_constant<ascending>())(lhs.getItemName<side>(), rhs.getItemName<side>()); } -template <bool ascending> inline //side currently unused! +template <bool ascending, SelectSide side> inline bool lessFilePath(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs, const std::unordered_map<const void* /*BaseFolderPair*/, size_t /*position*/>& sortedPos, std::vector<const FolderPair*>& tempBuf) @@ -490,7 +490,7 @@ bool lessFilePath(const FileSystemObject::ObjectId& lhs, const FileSystemObject: else if (folderL) return true; - return zen::makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(fsObjL->getItemNameAny(), fsObjR->getItemNameAny()); + return zen::makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(fsObjL->getItemName<side>(), fsObjR->getItemName<side>()); } else return true; @@ -499,7 +499,7 @@ bool lessFilePath(const FileSystemObject::ObjectId& lhs, const FileSystemObject: return false; //different components... - if (const std::weak_ordering cmp = compareNatural((*itL)->getItemNameAny(), (*itR)->getItemNameAny()); + if (const std::weak_ordering cmp = compareNatural((*itL)->getItemName<side>(), (*itR)->getItemName<side>()); cmp != std::weak_ordering::equivalent) { if constexpr (ascending) @@ -642,7 +642,7 @@ struct LessFullPath bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - return lessFilePath<ascending>(lhs, rhs, sortedPos_.ref(), tempBuf_); + return lessFilePath<ascending, side>(lhs, rhs, sortedPos_.ref(), tempBuf_); } private: @@ -651,7 +651,7 @@ private: }; -template <bool ascending> +template <bool ascending, SelectSide side> struct LessRelativeFolder { LessRelativeFolder(const std::vector<std::tuple<const void* /*BaseFolderPair*/, AbstractPath, AbstractPath>>& folderPairs) @@ -664,7 +664,7 @@ struct LessRelativeFolder bool operator()(const FileSystemObject::ObjectId& lhs, const FileSystemObject::ObjectId& rhs) const { - return lessFilePath<ascending>(lhs, rhs, sortedPos_.ref(), tempBuf_); + return lessFilePath<ascending, side>(lhs, rhs, sortedPos_.ref(), tempBuf_); } private: @@ -791,42 +791,44 @@ void FileView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, switch (pathFmt) { case ItemPathFormat::name: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName<true, SelectSide::left>()); + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName<true, SelectSide::left >()); else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName<true, SelectSide::right>()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName<false, SelectSide::left>()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName<false, SelectSide::left >()); else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFileName<false, SelectSide::right>()); break; case ItemPathFormat::relative: - if ( ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder<true >(folderPairs_)); - else if (!ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder<false>(folderPairs_)); + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder<true, SelectSide::left >(folderPairs_)); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder<true, SelectSide::right>(folderPairs_)); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder<false, SelectSide::left >(folderPairs_)); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder<false, SelectSide::right>(folderPairs_)); break; case ItemPathFormat::full: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath<true, SelectSide::left>(folderPairs_)); + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath<true, SelectSide::left >(folderPairs_)); else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath<true, SelectSide::right>(folderPairs_)); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath<false, SelectSide::left>(folderPairs_)); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath<false, SelectSide::left >(folderPairs_)); else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath<false, SelectSide::right>(folderPairs_)); break; } break; case ColumnTypeRim::size: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize<true, SelectSide::left>()); + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize<true, SelectSide::left >()); else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize<true, SelectSide::right>()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize<false, SelectSide::left>()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize<false, SelectSide::left >()); else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize<false, SelectSide::right>()); break; case ColumnTypeRim::date: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime<true, SelectSide::left>()); + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime<true, SelectSide::left >()); else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime<true, SelectSide::right>()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime<false, SelectSide::left>()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime<false, SelectSide::left >()); else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime<false, SelectSide::right>()); break; case ColumnTypeRim::extension: - if ( ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension<true, SelectSide::left>()); + if ( ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension<true, SelectSide::left >()); else if ( ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension<true, SelectSide::right>()); - else if (!ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension<false, SelectSide::left>()); + else if (!ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension<false, SelectSide::left >()); else if (!ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension<false, SelectSide::right>()); break; } diff --git a/FreeFileSync/Source/ui/file_view.h b/FreeFileSync/Source/ui/file_view.h index 45baf2d7..9891f9e9 100644 --- a/FreeFileSync/Source/ui/file_view.h +++ b/FreeFileSync/Source/ui/file_view.h @@ -119,8 +119,8 @@ public: const SortInfo* getSortConfig() const { return zen::get(currentSort_); } //return nullptr if currently not sorted ptrdiff_t findRowDirect(FileSystemObject::ObjectIdConst objId) const; //find an object's row position on view list directly, return < 0 if not found - ptrdiff_t findRowFirstChild(const ContainerObject* hierObj) const; //find first child of FolderPair or BaseFolderPair *on sorted sub view* - //"hierObj" may be invalid, it is NOT dereferenced, return < 0 if not found + ptrdiff_t findRowFirstChild(const ContainerObject* conObj) const; //find first child of FolderPair or BaseFolderPair *on sorted sub view* + //"conObj" may be invalid, it is NOT dereferenced, return < 0 if not found //count non-empty pairs to distinguish single/multiple folder pair cases size_t getEffectiveFolderPairCount() const; diff --git a/FreeFileSync/Source/ui/folder_pair.h b/FreeFileSync/Source/ui/folder_pair.h index eff628ec..c7e26164 100644 --- a/FreeFileSync/Source/ui/folder_pair.h +++ b/FreeFileSync/Source/ui/folder_pair.h @@ -66,7 +66,7 @@ private: setImage(*basicPanel_.m_bpButtonLocalSyncCfg, greyScaleIfDisabled(imgSync_, !!localSyncCfg_)); basicPanel_.m_bpButtonLocalSyncCfg->SetToolTip(localSyncCfg_ ? - _("Local synchronization settings") + L" (" + getVariantName(localSyncCfg_->directionCfg.var) + L')' : + _("Local synchronization settings") + L" (" + getVariantName(getSyncVariant(localSyncCfg_->directionCfg)) + L')' : _("Local synchronization settings")); setImage(*basicPanel_.m_bpButtonLocalFilter, greyScaleIfDisabled(imgFilter_, !isNullFilter(localFilter_))); diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index 02d078e2..a17154f0 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -113,10 +113,6 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_menuItemCheckVersionNow = new wxMenuItem( m_menuHelp, wxID_ANY, wxString( _("&Check for updates now") ) , wxEmptyString, wxITEM_NORMAL ); m_menuHelp->Append( m_menuItemCheckVersionNow ); - m_menuItemCheckVersionAuto = new wxMenuItem( m_menuHelp, wxID_ANY, wxString( _("Check &automatically once a week") ) , wxEmptyString, wxITEM_CHECK ); - m_menuHelp->Append( m_menuItemCheckVersionAuto ); - m_menuItemCheckVersionAuto->Check( true ); - m_menuHelp->AppendSeparator(); m_menuItemAbout = new wxMenuItem( m_menuHelp, wxID_ABOUT, wxString( _("&About") ) + wxT('\t') + wxT("Shift+F1"), wxEmptyString, wxITEM_NORMAL ); @@ -1130,7 +1126,6 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_menuTools->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuResetLayout ), this, m_menuItemResetLayout->GetId()); m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onShowHelp ), this, m_menuItemHelp->GetId()); m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuCheckVersion ), this, m_menuItemCheckVersionNow->GetId()); - m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuCheckVersionAutomatically ), this, m_menuItemCheckVersionAuto->GetId()); m_menuHelp->Bind(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::onMenuAbout ), this, m_menuItemAbout->GetId()); m_buttonCompare->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCompare ), NULL, this ); m_bpButtonCmpConfig->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::onCmpSettings ), NULL, this ); @@ -1332,7 +1327,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizerHeaderCompSettings = new wxBoxSizer( wxVERTICAL ); - m_staticTextMainCompSettings = new wxStaticText( m_panelCompSettingsTab, wxID_ANY, _("Main settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMainCompSettings = new wxStaticText( m_panelCompSettingsTab, wxID_ANY, _("Common settings:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextMainCompSettings->Wrap( -1 ); bSizerHeaderCompSettings->Add( m_staticTextMainCompSettings, 0, wxALL, 10 ); @@ -1468,7 +1463,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w wxBoxSizer* bSizer1733; bSizer1733 = new wxBoxSizer( wxVERTICAL ); - m_staticText112 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("&Ignore time shift [hh:mm]"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText112 = new wxStaticText( m_panelComparisonSettings, wxID_ANY, _("&Ignore exact time shift [hh:mm]"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText112->Wrap( -1 ); bSizer1733->Add( m_staticText112, 0, wxALL, 5 ); @@ -1650,7 +1645,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelCompSettingsTab->SetSizer( bSizer275 ); m_panelCompSettingsTab->Layout(); bSizer275->Fit( m_panelCompSettingsTab ); - m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), true ); + m_notebook->AddPage( m_panelCompSettingsTab, _("dummy"), false ); m_panelFilterSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panelFilterSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); @@ -1659,7 +1654,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizerHeaderFilterSettings = new wxBoxSizer( wxVERTICAL ); - m_staticTextMainFilterSettings = new wxStaticText( m_panelFilterSettingsTab, wxID_ANY, _("Main settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMainFilterSettings = new wxStaticText( m_panelFilterSettingsTab, wxID_ANY, _("Common settings:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextMainFilterSettings->Wrap( -1 ); bSizerHeaderFilterSettings->Add( m_staticTextMainFilterSettings, 0, wxALL, 10 ); @@ -1901,7 +1896,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizerHeaderSyncSettings = new wxBoxSizer( wxVERTICAL ); - m_staticTextMainSyncSettings = new wxStaticText( m_panelSyncSettingsTab, wxID_ANY, _("Main settings:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMainSyncSettings = new wxStaticText( m_panelSyncSettingsTab, wxID_ANY, _("Common settings:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextMainSyncSettings->Wrap( -1 ); bSizerHeaderSyncSettings->Add( m_staticTextMainSyncSettings, 0, wxALL, 10 ); @@ -1974,19 +1969,93 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer237->Add( 10, 0, 0, 0, 5 ); - wxBoxSizer* bSizer238; - bSizer238 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer312; + bSizer312 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer311; + bSizer311 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapDatabase = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); + m_bitmapDatabase->SetToolTip( _("sync.ffs_db") ); + + bSizer311->Add( m_bitmapDatabase, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + + m_checkBoxUseDatabase = new wxCheckBox( m_panelSyncSettings, wxID_ANY, _("Use database file to detect changes"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer311->Add( m_checkBoxUseDatabase, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer312->Add( bSizer311, 0, wxTOP|wxRIGHT|wxLEFT, 10 ); + + + bSizer312->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_staticTextSyncVarDescription = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); + m_staticTextSyncVarDescription->Wrap( -1 ); + m_staticTextSyncVarDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer312->Add( m_staticTextSyncVarDescription, 0, wxALL, 10 ); + + + bSizer312->Add( 0, 0, 1, wxEXPAND, 5 ); + + wxBoxSizer* bSizer310; + bSizer310 = new wxBoxSizer( wxVERTICAL ); + + m_staticline431 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer310->Add( m_staticline431, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer201; + bSizer201 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticline72 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer201->Add( m_staticline72, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer3121; + bSizer3121 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapMoveLeft = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); + m_bitmapMoveLeft->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + + bSizer3121->Add( m_bitmapMoveLeft, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_bitmapMoveRight = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); + m_bitmapMoveRight->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + + bSizer3121->Add( m_bitmapMoveRight, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + + m_staticTextDetectMove = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Detect moved files"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextDetectMove->Wrap( -1 ); + m_staticTextDetectMove->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + + bSizer3121->Add( m_staticTextDetectMove, 0, wxALIGN_CENTER_VERTICAL, 5 ); - bSizer238->Add( 0, 0, 1, wxEXPAND, 5 ); + bSizer201->Add( bSizer3121, 0, wxALL, 10 ); + + m_hyperlink242 = new wxHyperlinkCtrl( m_panelSyncSettings, wxID_ANY, _("More information"), wxT("https://freefilesync.org/manual.php?topic=synchronization-settings"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + m_hyperlink242->SetToolTip( _("https://freefilesync.org/manual.php?topic=synchronization-settings") ); + + bSizer201->Add( m_hyperlink242, 0, wxTOP|wxBOTTOM|wxRIGHT|wxALIGN_CENTER_VERTICAL, 10 ); + + m_staticline721 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer201->Add( m_staticline721, 0, wxEXPAND, 5 ); + + + bSizer310->Add( bSizer201, 0, 0, 5 ); + + + bSizer312->Add( bSizer310, 0, 0, 5 ); + + + bSizer237->Add( bSizer312, 0, wxEXPAND, 5 ); bSizerSyncDirHolder = new wxBoxSizer( wxHORIZONTAL ); - bSizerSyncDirections = new wxBoxSizer( wxVERTICAL ); + bSizerSyncDirsDiff = new wxBoxSizer( wxVERTICAL ); m_staticText184 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Difference"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText184->Wrap( -1 ); - bSizerSyncDirections->Add( m_staticText184, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + bSizerSyncDirsDiff->Add( m_staticText184, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); ffgSizer11 = new wxFlexGridSizer( 2, 0, 5, 5 ); ffgSizer11->SetFlexibleDirection( wxBOTH ); @@ -2007,11 +2076,6 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w ffgSizer11->Add( m_bitmapDifferent, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_bitmapConflict = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); - m_bitmapConflict->SetToolTip( _("Conflict/item cannot be categorized") ); - - ffgSizer11->Add( m_bitmapConflict, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_bitmapRightNewer = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); m_bitmapRightNewer->SetToolTip( _("Right side is newer") ); @@ -2031,9 +2095,6 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_bpButtonDifferent = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); ffgSizer11->Add( m_bpButtonDifferent, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_bpButtonConflict = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); - ffgSizer11->Add( m_bpButtonConflict, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_bpButtonRightNewer = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); ffgSizer11->Add( m_bpButtonRightNewer, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); @@ -2041,75 +2102,73 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w ffgSizer11->Add( m_bpButtonRightOnly, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizerSyncDirections->Add( ffgSizer11, 0, 0, 5 ); + bSizerSyncDirsDiff->Add( ffgSizer11, 0, 0, 5 ); m_staticText120 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Action"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText120->Wrap( -1 ); - bSizerSyncDirections->Add( m_staticText120, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 5 ); + bSizerSyncDirsDiff->Add( m_staticText120, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 5 ); - bSizerSyncDirHolder->Add( bSizerSyncDirections, 0, 0, 5 ); - - bSizerDatabase = new wxBoxSizer( wxVERTICAL ); - - m_bitmapDatabase = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); - bSizerDatabase->Add( m_bitmapDatabase, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); - + bSizerSyncDirHolder->Add( bSizerSyncDirsDiff, 0, wxALIGN_CENTER_VERTICAL, 5 ); - bSizerDatabase->Add( 0, 3, 0, 0, 5 ); + bSizerSyncDirsChanges = new wxBoxSizer( wxVERTICAL ); - m_staticText145 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("sync.ffs_db"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticText145->Wrap( -1 ); - m_staticText145->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); - m_staticText145->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + ffgSizer111 = new wxFlexGridSizer( 0, 3, 5, 5 ); + ffgSizer111->SetFlexibleDirection( wxBOTH ); + ffgSizer111->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); - bSizerDatabase->Add( m_staticText145, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + m_staticText12011 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Create:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12011->Wrap( -1 ); + ffgSizer111->Add( m_staticText12011, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + m_bpButtonLeftCreate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonLeftCreate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizerSyncDirHolder->Add( bSizerDatabase, 0, wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); + m_bpButtonRightCreate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonRightCreate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_staticTextSyncVarDescription = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); - m_staticTextSyncVarDescription->Wrap( -1 ); - m_staticTextSyncVarDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + m_staticText12012 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Update:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12012->Wrap( -1 ); + ffgSizer111->Add( m_staticText12012, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); - bSizerSyncDirHolder->Add( m_staticTextSyncVarDescription, 0, wxALL|wxALIGN_CENTER_VERTICAL, 10 ); + m_bpButtonLeftUpdate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonLeftUpdate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + m_bpButtonRightUpdate = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonRightUpdate, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer238->Add( bSizerSyncDirHolder, 0, wxTOP|wxBOTTOM|wxRIGHT, 10 ); + m_staticText12013 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Delete:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText12013->Wrap( -1 ); + ffgSizer111->Add( m_staticText12013, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + m_bpButtonLeftDelete = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonLeftDelete, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer238->Add( 0, 0, 1, wxEXPAND, 5 ); + m_bpButtonRightDelete = new wxBitmapButton( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), wxBU_AUTODRAW|0 ); + ffgSizer111->Add( m_bpButtonRightDelete, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_staticline431 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer238->Add( m_staticline431, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer201; - bSizer201 = new wxBoxSizer( wxHORIZONTAL ); + ffgSizer111->Add( 0, 0, 0, 0, 5 ); - m_staticline72 = new wxStaticLine( m_panelSyncSettings, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); - bSizer201->Add( m_staticline72, 0, wxEXPAND, 5 ); - - wxBoxSizer* bSizer249; - bSizer249 = new wxBoxSizer( wxHORIZONTAL ); + m_staticText1201 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Left"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1201->Wrap( -1 ); + ffgSizer111->Add( m_staticText1201, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); - m_checkBoxDetectMove = new wxCheckBox( m_panelSyncSettings, wxID_ANY, _("Detect moved files"), wxDefaultPosition, wxDefaultSize, 0 ); - m_checkBoxDetectMove->SetToolTip( _("- Not supported by all file systems\n- Requires and creates database files\n- Detection not available for first sync") ); + m_staticText1202 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("Right"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText1202->Wrap( -1 ); + ffgSizer111->Add( m_staticText1202, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - bSizer249->Add( m_checkBoxDetectMove, 0, wxALL|wxEXPAND, 5 ); - - m_hyperlink242 = new wxHyperlinkCtrl( m_panelSyncSettings, wxID_ANY, _("More information"), wxT("https://freefilesync.org/manual.php?topic=synchronization-settings"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); - m_hyperlink242->SetToolTip( _("https://freefilesync.org/manual.php?topic=synchronization-settings") ); - bSizer249->Add( m_hyperlink242, 0, wxTOP|wxBOTTOM|wxRIGHT, 5 ); + bSizerSyncDirsChanges->Add( ffgSizer111, 0, 0, 5 ); - bSizer201->Add( bSizer249, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + bSizerSyncDirHolder->Add( bSizerSyncDirsChanges, 0, wxALIGN_CENTER_VERTICAL, 5 ); - bSizer238->Add( bSizer201, 0, 0, 5 ); + bSizer237->Add( bSizerSyncDirHolder, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 5 ); - bSizer237->Add( bSizer238, 1, wxEXPAND, 5 ); + bSizer237->Add( 0, 0, 1, 0, 5 ); bSizer232->Add( bSizer237, 0, wxEXPAND, 5 ); @@ -2503,7 +2562,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelSyncSettingsTab->SetSizer( bSizer276 ); m_panelSyncSettingsTab->Layout(); bSizer276->Fit( m_panelSyncSettingsTab ); - m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), false ); + m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), true ); bSizer190->Add( m_notebook, 1, wxEXPAND, 5 ); @@ -2602,13 +2661,18 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_buttonUpdate->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onSyncUpdateDouble ), NULL, this ); m_buttonCustom->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onSyncCustom ), NULL, this ); m_buttonCustom->Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( ConfigDlgGenerated::onSyncCustomDouble ), NULL, this ); - m_bpButtonLeftOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onExLeftSideOnly ), NULL, this ); + m_checkBoxUseDatabase->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleUseDatabase ), NULL, this ); + m_bpButtonLeftOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftOnly ), NULL, this ); m_bpButtonLeftNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftNewer ), NULL, this ); m_bpButtonDifferent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDifferent ), NULL, this ); - m_bpButtonConflict->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onConflict ), NULL, this ); m_bpButtonRightNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightNewer ), NULL, this ); - m_bpButtonRightOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onExRightSideOnly ), NULL, this ); - m_checkBoxDetectMove->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onToggleDetectMovedFiles ), NULL, this ); + m_bpButtonRightOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightOnly ), NULL, this ); + m_bpButtonLeftCreate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftCreate ), NULL, this ); + m_bpButtonRightCreate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightCreate ), NULL, this ); + m_bpButtonLeftUpdate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftUpdate ), NULL, this ); + m_bpButtonRightUpdate->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightUpdate ), NULL, this ); + m_bpButtonLeftDelete->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onLeftDelete ), NULL, this ); + m_bpButtonRightDelete->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onRightDelete ), NULL, this ); m_buttonRecycler->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDeletionRecycler ), NULL, this ); m_buttonPermanent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDeletionPermanent ), NULL, this ); m_buttonVersioning->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( ConfigDlgGenerated::onDeletionVersioning ), NULL, this ); @@ -2901,7 +2965,7 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_checkBoxShowPassword = new wxCheckBox( m_panelAuth, wxID_ANY, _("&Show password"), wxDefaultPosition, wxDefaultSize, 0 ); bSizerPassword->Add( m_checkBoxShowPassword, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); - m_checkBoxPasswordPrompt = new wxCheckBox( m_panelAuth, wxID_ANY, _("Enter at login"), wxDefaultPosition, wxDefaultSize, 0 ); + m_checkBoxPasswordPrompt = new wxCheckBox( m_panelAuth, wxID_ANY, _("Prompt during login"), wxDefaultPosition, wxDefaultSize, 0 ); bSizerPassword->Add( m_checkBoxPasswordPrompt, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); @@ -4593,6 +4657,15 @@ RenameDlgGenerated::RenameDlgGenerated( wxWindow* parent, wxWindowID id, const w m_gridRenamePreview->SetScrollRate( 5, 5 ); bSizer242->Add( m_gridRenamePreview, 1, wxEXPAND, 5 ); + m_staticlinePreview = new wxStaticLine( m_panel31, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer242->Add( m_staticlinePreview, 0, wxEXPAND, 5 ); + + m_staticTextPlaceholderDescription = new wxStaticText( m_panel31, wxID_ANY, _("Placeholders represent differences between the names."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextPlaceholderDescription->Wrap( -1 ); + m_staticTextPlaceholderDescription->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizer242->Add( m_staticTextPlaceholderDescription, 0, wxTOP|wxRIGHT|wxLEFT|wxALIGN_CENTER_HORIZONTAL, 10 ); + m_textCtrlNewName = new wxTextCtrl( m_panel31, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); bSizer242->Add( m_textCtrlNewName, 0, wxEXPAND|wxALL, 10 ); @@ -4607,7 +4680,7 @@ RenameDlgGenerated::RenameDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); - m_buttonOK = new wxButton( this, wxID_OK, _("Rename"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); + m_buttonOK = new wxButton( this, wxID_OK, _("&Rename"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); m_buttonOK->SetDefault(); m_buttonOK->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); @@ -4629,7 +4702,6 @@ RenameDlgGenerated::RenameDlgGenerated( wxWindow* parent, wxWindowID id, const w // Connect Events this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( RenameDlgGenerated::onClose ) ); - m_textCtrlNewName->Connect( wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler( RenameDlgGenerated::onTypingName ), NULL, this ); m_buttonOK->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( RenameDlgGenerated::onOkay ), NULL, this ); m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( RenameDlgGenerated::onCancel ), NULL, this ); } @@ -5322,13 +5394,13 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS m_staticFfsTextVersion = new wxStaticText( m_panel41, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticFfsTextVersion->Wrap( -1 ); - bSizer298->Add( m_staticFfsTextVersion, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer298->Add( m_staticFfsTextVersion, 0, wxALIGN_BOTTOM, 5 ); m_staticTextFfsVariant = new wxStaticText( m_panel41, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextFfsVariant->Wrap( -1 ); m_staticTextFfsVariant->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer298->Add( m_staticTextFfsVariant, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 10 ); + bSizer298->Add( m_staticTextFfsVariant, 0, wxLEFT|wxALIGN_BOTTOM, 10 ); bSizerMainSection->Add( bSizer298, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); @@ -5457,8 +5529,8 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); - m_buttonShowDonationDetails = new wxButton( this, wxID_ANY, _("Thank you, %x, for your support!"), wxDefaultPosition, wxDefaultSize, 0 ); - bSizerStdButtons->Add( m_buttonShowDonationDetails, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + m_buttonShowSupporterDetails = new wxButton( this, wxID_ANY, _("Thank you, %x, for your support!"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizerStdButtons->Add( m_buttonShowSupporterDetails, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); bSizerStdButtons->Add( 0, 0, 1, 0, 5 ); @@ -5489,7 +5561,7 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS m_buttonDonate1->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onDonate ), NULL, this ); m_bpButtonForum->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onOpenForum ), NULL, this ); m_bpButtonEmail->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onSendEmail ), NULL, this ); - m_buttonShowDonationDetails->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onShowDonationDetails ), NULL, this ); + m_buttonShowSupporterDetails->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onShowSupporterDetails ), NULL, this ); m_buttonDonate2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onDonate ), NULL, this ); m_buttonClose->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( AboutDlgGenerated::onOkay ), NULL, this ); } @@ -5766,7 +5838,7 @@ ActivationDlgGenerated::ActivationDlgGenerated( wxWindow* parent, wxWindowID id, m_staticline82 = new wxStaticLine( m_panel35, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizer172->Add( m_staticline82, 0, wxEXPAND, 5 ); - m_staticTextMain = new wxStaticText( m_panel35, wxID_ANY, _("Activate the FreeFileSync Donation Edition by one of the following methods:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain = new wxStaticText( m_panel35, wxID_ANY, _("Activate FreeFileSync by one of the following methods:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextMain->Wrap( -1 ); bSizer172->Add( m_staticTextMain, 0, wxALL, 10 ); diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h index 3378e36e..42b1f9d5 100644 --- a/FreeFileSync/Source/ui/gui_generated.h +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -97,7 +97,6 @@ class MainDialogGenerated : public wxFrame wxMenu* m_menuHelp; wxMenuItem* m_menuItemHelp; wxMenuItem* m_menuItemCheckVersionNow; - wxMenuItem* m_menuItemCheckVersionAuto; wxMenuItem* m_menuItemAbout; wxBoxSizer* bSizerPanelHolder; wxPanel* m_panelTopButtons; @@ -235,7 +234,6 @@ class MainDialogGenerated : public wxFrame virtual void onMenuResetLayout( wxCommandEvent& event ) { event.Skip(); } virtual void onShowHelp( wxCommandEvent& event ) { event.Skip(); } virtual void onMenuCheckVersion( wxCommandEvent& event ) { event.Skip(); } - virtual void onMenuCheckVersionAutomatically( wxCommandEvent& event ) { event.Skip(); } virtual void onMenuAbout( wxCommandEvent& event ) { event.Skip(); } virtual void onCompSettingsContextMouse( wxMouseEvent& event ) { event.Skip(); } virtual void onCompSettingsContext( wxCommandEvent& event ) { event.Skip(); } @@ -420,31 +418,43 @@ class ConfigDlgGenerated : public wxDialog zen::ToggleButton* m_buttonMirror; zen::ToggleButton* m_buttonUpdate; zen::ToggleButton* m_buttonCustom; + wxStaticBitmap* m_bitmapDatabase; + wxCheckBox* m_checkBoxUseDatabase; + wxStaticText* m_staticTextSyncVarDescription; + wxStaticLine* m_staticline431; + wxStaticLine* m_staticline72; + wxStaticBitmap* m_bitmapMoveLeft; + wxStaticBitmap* m_bitmapMoveRight; + wxStaticText* m_staticTextDetectMove; + wxHyperlinkCtrl* m_hyperlink242; + wxStaticLine* m_staticline721; wxBoxSizer* bSizerSyncDirHolder; - wxBoxSizer* bSizerSyncDirections; + wxBoxSizer* bSizerSyncDirsDiff; wxStaticText* m_staticText184; wxFlexGridSizer* ffgSizer11; wxStaticBitmap* m_bitmapLeftOnly; wxStaticBitmap* m_bitmapLeftNewer; wxStaticBitmap* m_bitmapDifferent; - wxStaticBitmap* m_bitmapConflict; wxStaticBitmap* m_bitmapRightNewer; wxStaticBitmap* m_bitmapRightOnly; wxBitmapButton* m_bpButtonLeftOnly; wxBitmapButton* m_bpButtonLeftNewer; wxBitmapButton* m_bpButtonDifferent; - wxBitmapButton* m_bpButtonConflict; wxBitmapButton* m_bpButtonRightNewer; wxBitmapButton* m_bpButtonRightOnly; wxStaticText* m_staticText120; - wxBoxSizer* bSizerDatabase; - wxStaticBitmap* m_bitmapDatabase; - wxStaticText* m_staticText145; - wxStaticText* m_staticTextSyncVarDescription; - wxStaticLine* m_staticline431; - wxStaticLine* m_staticline72; - wxCheckBox* m_checkBoxDetectMove; - wxHyperlinkCtrl* m_hyperlink242; + wxFlexGridSizer* ffgSizer111; + wxStaticText* m_staticText12011; + wxBitmapButton* m_bpButtonLeftCreate; + wxBitmapButton* m_bpButtonRightCreate; + wxStaticText* m_staticText12012; + wxBitmapButton* m_bpButtonLeftUpdate; + wxBitmapButton* m_bpButtonRightUpdate; + wxStaticText* m_staticText12013; + wxBitmapButton* m_bpButtonLeftDelete; + wxBitmapButton* m_bpButtonRightDelete; + wxStaticText* m_staticText1201; + wxStaticText* m_staticText1202; wxStaticLine* m_staticline54; wxBoxSizer* bSizer2361; wxStaticText* m_staticText87; @@ -529,13 +539,18 @@ class ConfigDlgGenerated : public wxDialog virtual void onSyncUpdateDouble( wxMouseEvent& event ) { event.Skip(); } virtual void onSyncCustom( wxCommandEvent& event ) { event.Skip(); } virtual void onSyncCustomDouble( wxMouseEvent& event ) { event.Skip(); } - virtual void onExLeftSideOnly( wxCommandEvent& event ) { event.Skip(); } + virtual void onToggleUseDatabase( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftOnly( wxCommandEvent& event ) { event.Skip(); } virtual void onLeftNewer( wxCommandEvent& event ) { event.Skip(); } virtual void onDifferent( wxCommandEvent& event ) { event.Skip(); } - virtual void onConflict( wxCommandEvent& event ) { event.Skip(); } virtual void onRightNewer( wxCommandEvent& event ) { event.Skip(); } - virtual void onExRightSideOnly( wxCommandEvent& event ) { event.Skip(); } - virtual void onToggleDetectMovedFiles( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightOnly( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftCreate( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightCreate( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftUpdate( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightUpdate( wxCommandEvent& event ) { event.Skip(); } + virtual void onLeftDelete( wxCommandEvent& event ) { event.Skip(); } + virtual void onRightDelete( wxCommandEvent& event ) { event.Skip(); } virtual void onDeletionRecycler( wxCommandEvent& event ) { event.Skip(); } virtual void onDeletionPermanent( wxCommandEvent& event ) { event.Skip(); } virtual void onDeletionVersioning( wxCommandEvent& event ) { event.Skip(); } @@ -555,6 +570,7 @@ class ConfigDlgGenerated : public wxDialog public: wxStaticBitmap* m_bitmapRetryErrors; wxStaticText* m_staticTextFilterDescr; + wxBoxSizer* bSizerSyncDirsChanges; wxBitmapButton* m_bpButtonSelectVersioningAltFolder; wxBitmapButton* m_bpButtonShowLogFolder; fff::FolderHistoryBox* m_logFolderPath; @@ -1059,6 +1075,8 @@ class RenameDlgGenerated : public wxDialog wxStaticLine* m_staticline91; wxPanel* m_panel31; zen::Grid* m_gridRenamePreview; + wxStaticLine* m_staticlinePreview; + wxStaticText* m_staticTextPlaceholderDescription; wxTextCtrl* m_textCtrlNewName; wxStaticLine* m_staticline9; wxBoxSizer* bSizerStdButtons; @@ -1067,7 +1085,6 @@ class RenameDlgGenerated : public wxDialog // Virtual event handlers, override them in your derived class virtual void onClose( wxCloseEvent& event ) { event.Skip(); } - virtual void onTypingName( wxCommandEvent& event ) { event.Skip(); } virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } @@ -1264,7 +1281,7 @@ class AboutDlgGenerated : public wxDialog wxFlexGridSizer* fgSizerTranslators; wxStaticLine* m_staticline36; wxBoxSizer* bSizerStdButtons; - wxButton* m_buttonShowDonationDetails; + wxButton* m_buttonShowSupporterDetails; zen::BitmapTextButton* m_buttonDonate2; wxButton* m_buttonClose; @@ -1273,7 +1290,7 @@ class AboutDlgGenerated : public wxDialog virtual void onDonate( wxCommandEvent& event ) { event.Skip(); } virtual void onOpenForum( wxCommandEvent& event ) { event.Skip(); } virtual void onSendEmail( wxCommandEvent& event ) { event.Skip(); } - virtual void onShowDonationDetails( wxCommandEvent& event ) { event.Skip(); } + virtual void onShowSupporterDetails( wxCommandEvent& event ) { event.Skip(); } virtual void onOkay( wxCommandEvent& event ) { event.Skip(); } diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp index 43d39981..60a836cc 100644 --- a/FreeFileSync/Source/ui/gui_status_handler.cpp +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -514,7 +514,7 @@ StatusHandlerFloatingDialog::DlgOptions StatusHandlerFloatingDialog::showResult( } if (suspend) //*before* showing results dialog - try + try { suspendSystem(); //throw FileError } diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp index bd291972..2e453b06 100644 --- a/FreeFileSync/Source/ui/log_panel.cpp +++ b/FreeFileSync/Source/ui/log_panel.cpp @@ -202,7 +202,7 @@ public: }(); if (drawBottomLine) - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); + dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); //DrawLine() doesn't draw last pixel! //-------------------------------------------------------- } diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 935d5d0f..54ba9497 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -200,7 +200,14 @@ public: LocalPairConfig getValues() const { - return LocalPairConfig(folderSelectorLeft_.getPath(), folderSelectorRight_.getPath(), this->getCompConfig(), this->getSyncConfig(), this->getFilterConfig()); + return + { + folderSelectorLeft_ .getPath(), + folderSelectorRight_.getPath(), + this->getCompConfig(), + this->getSyncConfig(), + this->getFilterConfig() + }; } private: @@ -1181,8 +1188,6 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings) auiMgr_.GetPane(m_panelSearch).Hide(); //no need to show it on startup auiMgr_.GetPane(m_panelLog ).Hide(); // - m_menuItemCheckVersionAuto->Check(updateCheckActive(globalCfg_.lastUpdateCheck)); - auiMgr_.Update(); } @@ -1288,14 +1293,14 @@ std::vector<FileSystemObject*> expandSelectionForPartialSync(const std::vector<F assert(dynamic_cast<FilePair*>(output.back())->getMoveRef() == file.getId()); break; - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: case SO_DELETE_LEFT: case SO_DELETE_RIGHT: case SO_OVERWRITE_LEFT: case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: case SO_UNRESOLVED_CONFLICT: case SO_DO_NOTHING: case SO_EQUAL: @@ -1465,14 +1470,34 @@ std::vector<FileSystemObject*> MainDialog::getTreeSelection() const void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& selectionL, const std::vector<FileSystemObject*>& selectionR) { - if (std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }) && - /**/std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); })) - /**/return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + std::vector<const FileSystemObject*> copyLeft; + std::vector<const FileSystemObject*> copyRight; + + for (const FileSystemObject* fsObj : selectionL) + if (!fsObj->isEmpty<SelectSide::left>()) + copyLeft.push_back(fsObj); + + for (const FileSystemObject* fsObj : selectionR) + if (!fsObj->isEmpty<SelectSide::right>()) + copyRight.push_back(fsObj); + + if (copyLeft.empty() && copyRight.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + const int itemCount = static_cast<int>(copyLeft.size() + copyRight.size()); + std::wstring itemList; + + for (const FileSystemObject* fsObj : copyLeft) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::left>()) + L'\n'; + + for (const FileSystemObject* fsObj : copyRight) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::right>()) + L'\n'; + //------------------------------------------------------------------ FocusPreserver fp; if (showCopyToDialog(this, - selectionL, selectionR, + itemList, itemCount, globalCfg_.mainDlg.copyToCfg.targetFolderPath, globalCfg_.mainDlg.copyToCfg.targetFolderLastSelected, globalCfg_.mainDlg.copyToCfg.folderHistory, globalCfg_.folderHistoryMax, @@ -1494,7 +1519,7 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel globalCfg_.soundFileAlertPending); try { - fff::copyToAlternateFolder(selectionL, selectionR, + fff::copyToAlternateFolder(copyLeft, copyRight, globalCfg_.mainDlg.copyToCfg.targetFolderPath, globalCfg_.mainDlg.copyToCfg.keepRelPaths, globalCfg_.mainDlg.copyToCfg.overwriteIfExists, @@ -1515,13 +1540,26 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selectionL, const std::vector<FileSystemObject*>& selectionR, bool moveToRecycler) { - if (std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }) && - /**/std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); })) - /**/return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + std::vector<FileSystemObject*> deleteLeft = selectionL; + std::vector<FileSystemObject*> deleteRight = selectionR; - FocusPreserver fp; + std::erase_if(deleteLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }); + std::erase_if(deleteRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); }); + + if (deleteLeft.empty() && deleteRight.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + + const int itemCount = static_cast<int>(deleteLeft.size() + deleteRight.size()); + std::wstring itemList; - const auto& [itemList, itemCount] = getSelectedItemsAsString(selectionL, selectionR); + for (const FileSystemObject* fsObj : deleteLeft) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::left>()) + L'\n'; + + for (const FileSystemObject* fsObj : deleteRight) + itemList += AFS::getDisplayPath(fsObj->getAbstractPath<SelectSide::right>()) + L'\n'; + //------------------------------------------------------------------ + + FocusPreserver fp; if (showDeleteDialog(this, itemList, itemCount, moveToRecycler) != ConfirmationButton::accept) @@ -1541,11 +1579,11 @@ void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selec globalCfg_.soundFileAlertPending); try { - deleteFromGridAndHD(selectionL, selectionR, - extractDirectionCfg(folderCmp_, getConfig().mainCfg), - moveToRecycler, - globalCfg_.warnDlgs.warnRecyclerMissing, - statusHandler); //throw CancelProcess + deleteFiles(deleteLeft, deleteRight, + extractDirectionCfg(folderCmp_, getConfig().mainCfg), + moveToRecycler, + globalCfg_.warnDlgs.warnRecyclerMissing, + statusHandler); //throw CancelProcess } catch (CancelProcess&) {} @@ -1565,27 +1603,26 @@ void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selec void MainDialog::renameSelectedFiles(const std::vector<FileSystemObject*>& selectionL, const std::vector<FileSystemObject*>& selectionR) { - warn_static("finish") - if (std::all_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }) && - /**/std::all_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); })) - /**/return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + std::vector<FileSystemObject*> renameLeft = selectionL; + std::vector<FileSystemObject*> renameRight = selectionR; + + std::erase_if(renameLeft, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::left >(); }); + std::erase_if(renameRight, [](const FileSystemObject* fsObj) { return fsObj->isEmpty<SelectSide::right>(); }); + + if (renameLeft.empty() && renameRight.empty()) + return; //harmonize with onGridContextRim(): this function should be a no-op iff context menu option is disabled! + //------------------------------------------------------------------ FocusPreserver fp; - std::vector<Zstring> fileNamesOld - { - Zstr("Season 1, Episode 21 - The Arsenal of Freedom.mkv"), - Zstr("Season 1, Episode 22 - Symbiosis.mkv"), - Zstr("Season 1, Episode 23 - Skin of Evil.mkv"), - Zstr("Season 1, Episode 24 - We'll Always Have Paris.mkv"), - Zstr("Season 1, Episode 25 - Conspiracy.mkv"), - Zstr("Season 1, Episode 26 - The Neutral Zone.mkv"), - Zstr("Season 2, Episode 01 - The Child.mkv"), - Zstr("Season 2, Episode 02 - Where Silence Has Lease.mkv"), - Zstr("Season 2, Episode 03 - Elementary, Dear Data.mkv"), - }; - std::vector<Zstring> fileNamesNew; + std::vector<Zstring> fileNamesOld; + for (const FileSystemObject* fsObj : renameLeft) + fileNamesOld.push_back(fsObj->getItemName<SelectSide::left>()); + for (const FileSystemObject* fsObj : renameRight) + fileNamesOld.push_back(fsObj->getItemName<SelectSide::right>()); + + std::vector<Zstring> fileNamesNew; if (showRenameDialog(this, fileNamesOld, fileNamesNew) != ConfirmationButton::accept) return; @@ -1603,12 +1640,10 @@ void MainDialog::renameSelectedFiles(const std::vector<FileSystemObject*>& selec globalCfg_.soundFileAlertPending); try { - - //deleteFromGridAndHD(selectionL, selectionR, - // extractDirectionCfg(folderCmp_, getConfig().mainCfg), - // moveToRecycler, - // globalCfg_.warnDlgs.warnRecyclerMissing, - // statusHandler); //throw CancelProcess + renameItems(renameLeft, {fileNamesNew.data(), renameLeft.size()}, + renameRight, {fileNamesNew.data() + renameLeft.size(), fileNamesNew.size() - renameLeft.size()}, + extractDirectionCfg(folderCmp_, getConfig().mainCfg), + statusHandler); //throw CancelProcess } catch (CancelProcess&) {} @@ -1618,9 +1653,6 @@ void MainDialog::renameSelectedFiles(const std::vector<FileSystemObject*>& selec append(fullSyncLog_->log, r.errorLog.ref()); fullSyncLog_->totalTime += r.summary.totalTime; - ////remove rows that are empty: just a beautification, invalid rows shouldn't cause issues - //filegrid::getDataView(*m_gridMainC).removeInvalidRows(); - updateGui(); } @@ -2222,6 +2254,11 @@ void MainDialog::onTreeKeyEvent(wxKeyEvent& event) else switch (keyCode) { + case WXK_F2: + case WXK_NUMPAD_F2: + renameSelectedFiles(selection, selection); + return; + case WXK_RETURN: case WXK_NUMPAD_ENTER: startSyncForSelecction(selection); @@ -2317,10 +2354,7 @@ void MainDialog::onGridKeyEvent(wxKeyEvent& event, Grid& grid, bool leftSide) { case WXK_F2: case WXK_NUMPAD_F2: - warn_static("finish") -#if 0 renameSelectedFiles(selectionL, selectionR); -#endif return; case WXK_RETURN: @@ -2519,6 +2553,49 @@ void MainDialog::onTreeGridSelection(GridSelectEvent& event) } +namespace +{ +template <SelectSide side> +std::vector<Zstring> getFilterPhrasesRel(const std::vector<FileSystemObject*>& selection) +{ + std::vector<Zstring> output; + for (const FileSystemObject* fsObj : selection) + { + //#pragma warning(suppress: 6011) -> fsObj bound in this context! + Zstring phrase = FILE_NAME_SEPARATOR + fsObj->getRelativePath<side>(); + + const bool isFolder = dynamic_cast<const FolderPair*>(fsObj) != nullptr; + if (isFolder) + phrase += FILE_NAME_SEPARATOR; + + output.push_back(std::move(phrase)); + } + return output; +} + + +Zstring getFilterPhraseRel(const std::vector<FileSystemObject*>& selectionL, + const std::vector<FileSystemObject*>& selectionR) +{ + std::vector<Zstring> phrases; + append(phrases, getFilterPhrasesRel<SelectSide::left >(selectionL)); + append(phrases, getFilterPhrasesRel<SelectSide::right>(selectionR)); + + removeDuplicatesStable(phrases, [](const Zstring& lhs, const Zstring& rhs) { return compareNoCase(lhs, rhs) < 0; }); + //ignore case, just like path filter + + Zstring relPathPhrase; + for (const Zstring& phrase : phrases) + { + relPathPhrase += phrase; + relPathPhrase += Zstr('\n'); + } + + return trimCpy(relPathPhrase); +} +} + + void MainDialog::onTreeGridContext(GridContextMenuEvent& event) { const std::vector<FileSystemObject*>& selection = getTreeSelection(); //referenced by lambdas! @@ -2549,32 +2626,62 @@ void MainDialog::onTreeGridContext(GridContextMenuEvent& event) //---------------------------------------------------------------------------------------------------- auto addFilterMenu = [&](const std::wstring& label, const wxImage& img, bool include) { - - if (selection.size() == 1) + if (selection.empty()) + menu.addItem(label, nullptr, img, false /*enabled*/); + else if (selection.size() == 1) { ContextMenu submenu; const bool isFolder = dynamic_cast<const FolderPair*>(selection[0]) != nullptr; - //by short name - Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getItemNameAny(); + const Zstring& relPathL = selection[0]->getRelativePath<SelectSide::left >(); + const Zstring& relPathR = selection[0]->getRelativePath<SelectSide::right>(); + + //by extension + const Zstring extensionL = getFileExtension(relPathL); + const Zstring extensionR = getFileExtension(relPathR); + if (!extensionL.empty()) + submenu.addItem(L"*." + utfTo<wxString>(extensionL), + [this, extensionL, include] { addFilterPhrase(Zstr("*.") + extensionL, include, false /*requireNewLine*/); }); + + if (!extensionR.empty() && !equalNoCase(extensionL, extensionR)) //rare, but possible (e.g. after manual rename) + submenu.addItem(L"*." + utfTo<wxString>(extensionR), + [this, extensionR, include] { addFilterPhrase(Zstr("*.") + extensionR, include, false /*requireNewLine*/); }); + + //by file name + Zstring filterPhraseNameL = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPathL); + Zstring filterPhraseNameR = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPathR); if (isFolder) - labelShort += FILE_NAME_SEPARATOR; - submenu.addItem(utfTo<wxString>(labelShort), [this, &selection, include] { filterShortname(*selection[0], include); }); + { + filterPhraseNameL += FILE_NAME_SEPARATOR; + filterPhraseNameR += FILE_NAME_SEPARATOR; + } + + submenu.addItem(utfTo<wxString>(filterPhraseNameL), + [this, filterPhraseNameL, include] { addFilterPhrase(filterPhraseNameL, include, true /*requireNewLine*/); }); + + if (!equalNoCase(filterPhraseNameL, filterPhraseNameR)) //rare, but possible (ignore case, just like path filter) + submenu.addItem(utfTo<wxString>(filterPhraseNameR), + [this, filterPhraseNameR, include] { addFilterPhrase(filterPhraseNameR, include, true /*requireNewLine*/); }); //by relative path - Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getRelativePathAny(); + Zstring filterPhraseRelL = FILE_NAME_SEPARATOR + relPathL; + Zstring filterPhraseRelR = FILE_NAME_SEPARATOR + relPathR; if (isFolder) - labelRel += FILE_NAME_SEPARATOR; - submenu.addItem(utfTo<wxString>(labelRel), [this, &selection, include] { filterItems(selection, include); }); + { + filterPhraseRelL += FILE_NAME_SEPARATOR; + filterPhraseRelR += FILE_NAME_SEPARATOR; + } + submenu.addItem(utfTo<wxString>(filterPhraseRelL), [this, filterPhraseRelL, include] { addFilterPhrase(filterPhraseRelL, include, true /*requireNewLine*/); }); + + if (!equalNoCase(filterPhraseRelL, filterPhraseRelR)) //rare, but possible + submenu.addItem(utfTo<wxString>(filterPhraseRelR), [this, filterPhraseRelR, include] { addFilterPhrase(filterPhraseRelR, include, true /*requireNewLine*/); }); menu.addSubmenu(label, submenu, img); } - else if (selection.size() > 1) //by relative path + else //by relative path menu.addItem(label + L" <" + _("multiple selection") + L">", - [this, &selection, include] { filterItems(selection, include); }, img); - else - menu.addItem(label, nullptr, img, false /*enabled*/); + [this, &selection, include] { addFilterPhrase(getFilterPhraseRel(selection, selection), include, true /*requireNewLine*/); }, img); }; addFilterMenu(_("&Include via filter:"), loadImage("filter_include", getDefaultMenuIconSize()), true); addFilterMenu(_("&Exclude via filter:"), loadImage("filter_exclude", getDefaultMenuIconSize()), false); @@ -2587,38 +2694,26 @@ void MainDialog::onTreeGridContext(GridContextMenuEvent& event) const bool selectionContainsItemsToSync = [&] { for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection)) - switch (fsObj->getSyncOperation()) - { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_DELETE_LEFT: - case SO_DELETE_RIGHT: - case SO_MOVE_LEFT_FROM: - case SO_MOVE_LEFT_TO: - case SO_MOVE_RIGHT_FROM: - case SO_MOVE_RIGHT_TO: - case SO_OVERWRITE_LEFT: - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: - return true; - - case SO_UNRESOLVED_CONFLICT: - case SO_DO_NOTHING: - case SO_EQUAL: - break; - } + if (getEffectiveSyncDir(fsObj->getSyncOperation()) != SyncDirection::none) + return true; return false; }(); menu.addSeparator(); menu.addItem(_("&Synchronize selection") + L"\tEnter", [&] { startSyncForSelecction(selection); }, loadImage("start_sync_selection", getDefaultMenuIconSize()), selectionContainsItemsToSync); //---------------------------------------------------------------------------------------------------- - const bool haveItemsSelected = std::any_of(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left>() || !fsObj->isEmpty<SelectSide::right>(); }); + const ptrdiff_t itemsSelected = + std::count_if(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); }) + + std::count_if(selection.begin(), selection.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); }); + //menu.addSeparator(); - //menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selection, selection); }, wxNullImage, haveItemsSelected); + //menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selection, selection); }, wxNullImage, itemsSelected > 0); //---------------------------------------------------------------------------------------------------- menu.addSeparator(); - menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selection, selection, true /*moveToRecycler*/); }, imgTrashSmall_, haveItemsSelected); + + menu.addItem((itemsSelected > 1 ? _("Multi-&Rename") : _("&Rename")) + L"\tF2", + [&] { renameSelectedFiles(selection, selection); }, loadImage("rename", getDefaultMenuIconSize()), itemsSelected > 0); + + menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selection, selection, true /*moveToRecycler*/); }, imgTrashSmall_, itemsSelected > 0); menu.popup(*m_gridOverview, event.mousePos_); } @@ -2690,40 +2785,46 @@ void MainDialog::onGridContextRim(const std::vector<FileSystemObject*>& selectio //---------------------------------------------------------------------------------------------------- auto addFilterMenu = [&](const wxString& label, const wxImage& img, bool include) { - if (selection.size() == 1) + if (selectionL.empty() && selectionR.empty()) + menu.addItem(label, nullptr, img, false /*enabled*/); + else if (selectionL.size() + selectionR.size() == 1) { ContextMenu submenu; - const bool isFolder = dynamic_cast<const FolderPair*>(selection[0]) != nullptr; + const bool isFolder = dynamic_cast<const FolderPair*>((!selectionL.empty() ? selectionL : selectionR)[0]) != nullptr; + const Zstring& relPath = !selectionL.empty() ? + selectionL[0]->getRelativePath<SelectSide::left >() : + selectionR[0]->getRelativePath<SelectSide::right>(); //by extension - if (!isFolder) + if (const Zstring extension = getFileExtension(relPath); + !extension.empty()) + submenu.addItem(L"*." + utfTo<wxString>(extension), [this, extension, include] { - const Zstring extension = getFileExtension(selection[0]->getItemNameAny()); - if (!extension.empty()) - submenu.addItem(L"*." + utfTo<wxString>(extension), - [this, extension, include] { filterExtension(extension, include); }); - } + addFilterPhrase(Zstr("*.") + extension, include, false /*requireNewLine*/); + }); - //by short name - Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getItemNameAny(); + //by file name + Zstring filterPhraseName = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + getItemName(relPath); if (isFolder) - labelShort += FILE_NAME_SEPARATOR; - submenu.addItem(utfTo<wxString>(labelShort), [this, &selection, include] { filterShortname(*selection[0], include); }); + filterPhraseName += FILE_NAME_SEPARATOR; + + submenu.addItem(utfTo<wxString>(filterPhraseName), [this, filterPhraseName, include] + { + addFilterPhrase(filterPhraseName, include, true /*requireNewLine*/); + }); //by relative path - Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getRelativePathAny(); + Zstring filterPhraseRel = FILE_NAME_SEPARATOR + relPath; if (isFolder) - labelRel += FILE_NAME_SEPARATOR; - submenu.addItem(utfTo<wxString>(labelRel), [this, &selection, include] { filterItems(selection, include); }); + filterPhraseRel += FILE_NAME_SEPARATOR; + submenu.addItem(utfTo<wxString>(filterPhraseRel), [this, filterPhraseRel, include] { addFilterPhrase(filterPhraseRel, include, true /*requireNewLine*/); }); menu.addSubmenu(label, submenu, img); } - else if (selection.size() > 1) //by relative path + else //by relative path menu.addItem(label + L" <" + _("multiple selection") + L">", - [this, &selection, include] { filterItems(selection, include); }, img); - else - menu.addItem(label, nullptr, img, false /*enabled*/); + [this, &selectionL, &selectionR, include] { addFilterPhrase(getFilterPhraseRel(selectionL, selectionR), include, true /*requireNewLine*/); }, img); }; addFilterMenu(_("&Include via filter:"), loadImage("filter_include", getDefaultMenuIconSize()), true); addFilterMenu(_("&Exclude via filter:"), loadImage("filter_exclude", getDefaultMenuIconSize()), false); @@ -2736,27 +2837,8 @@ void MainDialog::onGridContextRim(const std::vector<FileSystemObject*>& selectio const bool selectionContainsItemsToSync = [&] { for (FileSystemObject* fsObj : expandSelectionForPartialSync(selection)) - switch (fsObj->getSyncOperation()) - { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: - case SO_DELETE_LEFT: - case SO_DELETE_RIGHT: - case SO_MOVE_LEFT_FROM: - case SO_MOVE_LEFT_TO: - case SO_MOVE_RIGHT_FROM: - case SO_MOVE_RIGHT_TO: - case SO_OVERWRITE_LEFT: - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: - return true; - - case SO_UNRESOLVED_CONFLICT: - case SO_DO_NOTHING: - case SO_EQUAL: - break; - } + if (getEffectiveSyncDir(fsObj->getSyncOperation()) != SyncDirection::none) + return true; return false; }(); menu.addSeparator(); @@ -2788,21 +2870,19 @@ void MainDialog::onGridContextRim(const std::vector<FileSystemObject*>& selectio } } //---------------------------------------------------------------------------------------------------- - const bool haveItemsSelected = - std::any_of(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); }) || - std::any_of(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); }); + const ptrdiff_t itemsSelected = + std::count_if(selectionL.begin(), selectionL.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::left >(); }) + + std::count_if(selectionR.begin(), selectionR.end(), [](const FileSystemObject* fsObj) { return !fsObj->isEmpty<SelectSide::right>(); }); menu.addSeparator(); - menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selectionL, selectionR); }, wxNullImage, haveItemsSelected); + menu.addItem(_("&Copy to...") + L"\tCtrl+T", [&] { copyToAlternateFolder(selectionL, selectionR); }, wxNullImage, itemsSelected > 0); //---------------------------------------------------------------------------------------------------- menu.addSeparator(); - warn_static("finish") -#if 0 - menu.addItem(_("&Rename") + L"\tF2", [&] { renameSelectedFiles(selectionL, selectionR); }, loadImage("rename", getDefaultMenuIconSize()), haveItemsSelected); -#endif + menu.addItem((itemsSelected > 1 ? _("Multi-&Rename") : _("&Rename")) + L"\tF2", + [&] { renameSelectedFiles(selectionL, selectionR); }, loadImage("rename", getDefaultMenuIconSize()), itemsSelected > 0); - menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selectionL, selectionR, true /*moveToRecycler*/); }, imgTrashSmall_, haveItemsSelected); + menu.addItem(_("&Delete") + L"\t(Shift+)Del", [&] { deleteSelectedFiles(selectionL, selectionR, true /*moveToRecycler*/); }, imgTrashSmall_, itemsSelected > 0); menu.popup(leftSide ? *m_gridMainL : *m_gridMainR, mousePos); } @@ -2861,48 +2941,6 @@ void MainDialog::addFilterPhrase(const Zstring& phrase, bool include, bool requi } -void MainDialog::filterExtension(const Zstring& extension, bool include) -{ - assert(!extension.empty()); - addFilterPhrase(Zstr("*.") + extension, include, false); -} - - -void MainDialog::filterShortname(const FileSystemObject& fsObj, bool include) -{ - Zstring phrase = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + fsObj.getItemNameAny(); - const bool isFolder = dynamic_cast<const FolderPair*>(&fsObj) != nullptr; - if (isFolder) - phrase += FILE_NAME_SEPARATOR; - - addFilterPhrase(phrase, include, true); -} - - -void MainDialog::filterItems(const std::vector<FileSystemObject*>& selection, bool include) -{ - if (!selection.empty()) - { - Zstring phrase; - for (auto it = selection.begin(); it != selection.end(); ++it) - { - FileSystemObject* fsObj = *it; - - if (it != selection.begin()) - phrase += Zstr('\n'); - - //#pragma warning(suppress: 6011) -> fsObj bound in this context! - phrase += FILE_NAME_SEPARATOR + fsObj->getRelativePathAny(); - - const bool isFolder = dynamic_cast<const FolderPair*>(fsObj) != nullptr; - if (isFolder) - phrase += FILE_NAME_SEPARATOR; - } - addFilterPhrase(phrase, include, true); - } -} - - void MainDialog::onGridLabelContextC(GridLabelClickEvent& event) { ContextMenu menu; @@ -3136,11 +3174,11 @@ void MainDialog::onSyncSettingsContext(wxEvent& event) auto setVariant = [&](SyncVariant var) { - currentCfg_.mainCfg.syncCfg.directionCfg.var = var; + currentCfg_.mainCfg.syncCfg.directionCfg = getDefaultSyncCfg(var); applySyncDirections(); }; - const auto activeSyncVar = getConfig().mainCfg.syncCfg.directionCfg.var; + const SyncVariant activeSyncVar = getSyncVariant(getConfig().mainCfg.syncCfg.directionCfg); auto addVariantItem = [&](SyncVariant syncVar, const char* iconName) { @@ -3151,7 +3189,7 @@ void MainDialog::onSyncSettingsContext(wxEvent& event) addVariantItem(SyncVariant::twoWay, "sync_twoway"); addVariantItem(SyncVariant::mirror, "sync_mirror"); addVariantItem(SyncVariant::update, "sync_update"); - addVariantItem(SyncVariant::custom, "sync_custom"); + //addVariantItem(SyncVariant::custom, "sync_custom"); -> doesn't make sense, does it? menu.popup(*m_bpButtonSyncContext, {m_bpButtonSyncContext->GetSize().x, 0}); } @@ -3759,7 +3797,7 @@ void MainDialog::renameSelectedCfgHistoryItem() wxTextEntryDialog cfgRenameDlg(this, _("New name:"), _("Rename Configuration"), utfTo<wxString>(cfgNameOld)); wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST); - inputValidator.SetCharExcludes(LR"(<>:"/\|?*)"); //forbidden chars for file names (at least on Windows) + inputValidator.SetCharExcludes(LR"(<>:"/\|?*)"); //chars forbidden for file names (at least on Windows) //https://docs.microsoft.com/de-de/windows/win32/fileio/naming-a-file#naming-conventions cfgRenameDlg.SetTextValidator(inputValidator); @@ -3841,15 +3879,6 @@ void MainDialog::onCfgGridContext(GridContextMenuEvent& event) assert(false); //-------------------------------------------------------------------------------------------------------- - const bool renameEnabled = [&] - { - if (!selectedRows.empty()) - if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0])) - return !cfg->isLastRunCfg; - return false; - }(); - menu.addItem(_("&Rename...") + L"\tF2", [this] { renameSelectedCfgHistoryItem (); }, wxNullImage, renameEnabled); - //-------------------------------------------------------------------------------------------------------- ContextMenu submenu; auto applyBackColor = [this, &cfgFilePaths](const wxColor& col) @@ -3981,6 +4010,16 @@ void MainDialog::onCfgGridContext(GridContextMenuEvent& event) showInFileManager, imgFileManagerSmall_, !selectedRows.empty()); menu.addSeparator(); //-------------------------------------------------------------------------------------------------------- + const bool renameEnabled = [&] + { + if (!selectedRows.empty()) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(selectedRows[0])) + return !cfg->isLastRunCfg; + return false; + }(); + menu.addItem(_("&Rename") + L"\tF2", [this] { renameSelectedCfgHistoryItem (); }, loadImage("rename", getDefaultMenuIconSize()), renameEnabled); + + //-------------------------------------------------------------------------------------------------------- menu.addItem(_("&Hide") + L"\tDel", [this] { removeSelectedCfgHistoryItems(false /*deleteFromDisk*/); }, wxNullImage, !selectedRows.empty()); menu.addItem(_("&Delete") + L"\tShift+Del", [this] { removeSelectedCfgHistoryItems(true /*deleteFromDisk*/); }, imgTrashSmall_, !selectedRows.empty()); //-------------------------------------------------------------------------------------------------------- @@ -4456,6 +4495,7 @@ void MainDialog::onCompare(wxCommandEvent& event) //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog! FocusPreserver fp; //e.g. keep focus on config panel after pressing F5 + //give nice hint on what's next to do if user manually clicked on compare assert(m_buttonCompare->GetId() != wxID_ANY); if (fp.getFocusId() == m_buttonCompare->GetId()) @@ -4572,8 +4612,8 @@ void MainDialog::updateGui() updateUnsavedCfgStatus(); const auto& mainCfg = getConfig().mainCfg; - const std::optional<CompareVariant> cmpVar = getCompVariant(mainCfg); - const std::optional<SyncVariant> syncVar = getSyncVariant(mainCfg); + const std::optional<CompareVariant> cmpVar = getCommonCompVariant(mainCfg); + const std::optional<SyncVariant> syncVar = getCommonSyncVariant(mainCfg); const char* cmpVarIconName = nullptr; if (cmpVar) @@ -4705,7 +4745,7 @@ void MainDialog::onStartSync(wxCommandEvent& event) bool dontShowAgain = false; if (showSyncConfirmationDlg(this, false /*syncSelection*/, - getSyncVariant(guiCfg.mainCfg), + getCommonSyncVariant(guiCfg.mainCfg), SyncStatistics(folderCmp_), dontShowAgain) != ConfirmationButton::accept) return; @@ -4913,6 +4953,12 @@ void MainDialog::onStartSync(wxCommandEvent& event) updateGui(); + warn_static("fix?") + //"The little statistics box in the lower right corner clears right after the sync + //completes. It used to clear after you close the progress window. I like to know + //the statistics, but usually I just start the sync and walk away. When I come back, + //the statistics are gone." + //--------------------------------------------------------------------------- const StatusHandlerFloatingDialog::DlgOptions dlgOpt = statusHandler.showResult(); @@ -4948,15 +4994,15 @@ void MainDialog::onStartSync(wxCommandEvent& event) namespace { -void appendInactive(ContainerObject& hierObj, std::vector<FileSystemObject*>& inactiveItems) +void appendInactive(ContainerObject& conObj, std::vector<FileSystemObject*>& inactiveItems) { - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) if (!file.isActive()) inactiveItems.push_back(&file); - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) if (!symlink.isActive()) inactiveItems.push_back(&symlink); - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) { if (!folder.isActive()) inactiveItems.push_back(&folder); @@ -4976,8 +5022,8 @@ void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& se { switch (fsObj->getSyncOperation()) { - case SO_CREATE_NEW_LEFT: - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_LEFT: + case SO_CREATE_RIGHT: case SO_DELETE_LEFT: case SO_DELETE_RIGHT: case SO_MOVE_LEFT_FROM: @@ -4986,8 +5032,8 @@ void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& se case SO_MOVE_RIGHT_TO: case SO_OVERWRITE_LEFT: case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_LEFT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_LEFT: + case SO_RENAME_RIGHT: basePairsSelect.insert(&fsObj->base()); break; @@ -5051,7 +5097,7 @@ void MainDialog::startSyncForSelecction(const std::vector<FileSystemObject*>& se if (showSyncConfirmationDlg(this, true /*syncSelection*/, - getSyncVariant(guiCfg.mainCfg), + getCommonSyncVariant(guiCfg.mainCfg), SyncStatistics(folderCmpSelect), dontShowAgain) != ConfirmationButton::accept) return; @@ -6198,25 +6244,6 @@ void MainDialog::onMenuCheckVersion(wxCommandEvent& event) } -void MainDialog::onMenuCheckVersionAutomatically(wxCommandEvent& event) -{ - if (updateCheckActive(globalCfg_.lastUpdateCheck)) - disableUpdateCheck(globalCfg_.lastUpdateCheck); - else - globalCfg_.lastUpdateCheck = 0; //reset to GlobalSettings.xml default value! - - m_menuItemCheckVersionAuto->Check(updateCheckActive(globalCfg_.lastUpdateCheck)); - - if (shouldRunAutomaticUpdateCheck(globalCfg_.lastUpdateCheck)) - { - flashStatusInfo(_("Searching for program updates...")); - //synchronous update check is sufficient here: - automaticUpdateCheckEval(*this, globalCfg_.lastUpdateCheck, globalCfg_.lastOnlineVersion, - automaticUpdateCheckRunAsync(automaticUpdateCheckPrepare(*this).ref()).ref()); - } -} - - void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) { //execute just once per startup! @@ -6225,12 +6252,12 @@ void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) auto showNewVersionReminder = [this] { - if (!globalCfg_.lastOnlineVersion.empty() && haveNewerVersionOnline(globalCfg_.lastOnlineVersion)) + if (haveNewerVersionOnline(globalCfg_.lastOnlineVersion)) { auto menu = new wxMenu(); wxMenuItem* newItem = new wxMenuItem(menu, wxID_ANY, _("&Show details")); Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent&) { checkForUpdateNow(*this, globalCfg_.lastOnlineVersion); }, newItem->GetId()); - //show changelog + handle Donation Edition auto-updater (including expiration) + //show changelog + handle Supporter Edition auto-updater (including expiration) menu->Append(newItem); //pass ownership const std::wstring& blackStar = utfTo<std::wstring>("★"); @@ -6238,7 +6265,7 @@ void MainDialog::onStartupUpdateCheck(wxIdleEvent& event) } }; - if (shouldRunAutomaticUpdateCheck(globalCfg_.lastUpdateCheck)) + if (automaticUpdateCheckDue(globalCfg_.lastUpdateCheck)) { flashStatusInfo(_("Searching for program updates...")); diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h index a8759173..d159106f 100644 --- a/FreeFileSync/Source/ui/main_dlg.h +++ b/FreeFileSync/Source/ui/main_dlg.h @@ -237,9 +237,6 @@ private: void setLastOperationLog(const ProcessSummary& summary, const std::shared_ptr<const zen::ErrorLog>& errorLog); void showLogPanel(bool show); - void filterExtension(const Zstring& extension, bool include); - void filterShortname(const FileSystemObject& fsObj, bool include); - void filterItems(const std::vector<FileSystemObject*>& selection, bool include); void addFilterPhrase(const Zstring& phrase, bool include, bool requireNewLine); void onTopFolderPairAdd (wxCommandEvent& event) override; @@ -277,7 +274,6 @@ private: void onMenuResetLayout (wxCommandEvent& event) override { resetLayout(); } void onMenuFindItem (wxCommandEvent& event) override { showFindPanel(true /*show*/); } //CTRL + F void onMenuCheckVersion (wxCommandEvent& event) override; - void onMenuCheckVersionAutomatically(wxCommandEvent& event) override; void onMenuAbout (wxCommandEvent& event) override; void onShowHelp (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/manual.php?topic=freefilesync"); } void onMenuQuit (wxCommandEvent& event) override { Close(); } diff --git a/FreeFileSync/Source/ui/rename_dlg.cpp b/FreeFileSync/Source/ui/rename_dlg.cpp index 50798c69..d1c09cbb 100644 --- a/FreeFileSync/Source/ui/rename_dlg.cpp +++ b/FreeFileSync/Source/ui/rename_dlg.cpp @@ -5,9 +5,11 @@ // ***************************************************************************** #include "rename_dlg.h" -#include "gui_generated.h" +#include <chrono> +#include <wx/valtext.h> #include <wx+/window_layout.h> #include <wx+/image_resources.h> +#include "gui_generated.h" #include "../base/multi_rename.h" @@ -32,13 +34,50 @@ public: fileNamesOld_(fileNamesOld), renameBuf_(renameBuf) {} - void updatePreview(const std::wstring& renamePhrase) + bool updatePreview(std::wstring_view renamePhrase, size_t selectBegin, size_t selectEnd) //support polling { - fileNamesNew_ = resolvePlaceholderPhrase(renamePhrase, renameBuf_.ref()); - assert(fileNamesNew_.size() == fileNamesOld_.size()); + //normalize input: trim and adapt selection + { + const std::wstring_view renamePhraseTrm = trimCpy(renamePhrase); + + if (selectBegin <= selectEnd && selectEnd <= renamePhrase.size()) + { + selectBegin -= std::min(selectBegin, makeUnsigned(renamePhraseTrm.data() - renamePhrase.data())); //careful: + selectEnd -= std::min(selectEnd, makeUnsigned(renamePhraseTrm.data() - renamePhrase.data())); //avoid underflow + + selectBegin = std::min(selectBegin, renamePhraseTrm.size()); + selectEnd = std::min(selectEnd, renamePhraseTrm.size()); + } + else + { + assert(false); + selectBegin = selectEnd = 0; + } + + renamePhrase = renamePhraseTrm; + } + + auto currentPhrase = std::make_tuple(renamePhrase, selectBegin, selectEnd); + if (currentPhrase != lastUsedPhrase_) //only update when needed + { + lastUsedPhrase_ = currentPhrase; + + fileNamesNewSelectBefore_ = resolvePlaceholderPhrase(renamePhrase.substr(0, selectBegin), renameBuf_.ref()); + fileNamesNewSelected_ = resolvePlaceholderPhrase(renamePhrase.substr(selectBegin, selectEnd - selectBegin), renameBuf_.ref()); + fileNamesNewSelectAfter_ = resolvePlaceholderPhrase(renamePhrase.substr(selectEnd), renameBuf_.ref()); + + assert(fileNamesNewSelectBefore_.size() == fileNamesOld_.size()); + assert(fileNamesNewSelected_ .size() == fileNamesOld_.size()); + assert(fileNamesNewSelectAfter_ .size() == fileNamesOld_.size()); + + previewChangeTime_ = std::chrono::steady_clock::now(); + return true; + } + else + return false; } - const std::vector<std::wstring>& getNewNames() const { return fileNamesNew_; } + std::vector<std::wstring> getNewNames() const { return resolvePlaceholderPhrase(std::get<std::wstring>(lastUsedPhrase_), renameBuf_.ref()); } size_t getRowCount() const override { return fileNamesOld_.size(); } @@ -51,7 +90,7 @@ public: return fileNamesOld_[row]; case ColumnTypeRename::newName: - return fileNamesNew_[row]; + return fileNamesNewSelectBefore_[row] + fileNamesNewSelected_[row] + fileNamesNewSelectAfter_[row]; } return std::wstring(); } @@ -69,7 +108,42 @@ public: rectTmp.x += getColumnGapLeft(); rectTmp.width -= getColumnGapLeft(); - drawCellText(dc, rectTmp, getValue(row, colType)); + + switch (static_cast<ColumnTypeRename>(colType)) + { + case ColumnTypeRename::oldName: + drawCellText(dc, rectTmp, getValue(row, colType)); + break; + + case ColumnTypeRename::newName: + { + const wxSize extentBefore = dc.GetTextExtent(fileNamesNewSelectBefore_[row]); + drawCellText(dc, rectTmp, fileNamesNewSelectBefore_[row], wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &extentBefore); + rectTmp.x += extentBefore.GetWidth(); + rectTmp.width -= extentBefore.GetWidth(); + + if (!fileNamesNewSelected_[row].empty()) //highlight text selection: + { + const wxSize extentSelect = dc.GetTextExtent(fileNamesNewSelected_[row]); + clearArea(dc, {rectTmp.x, rectTmp.y, extentSelect.GetWidth(), rectTmp.height}, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); + + wxDCTextColourChanger textColor(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); //accessibility: always set both foreground AND background colors! + drawCellText(dc, rectTmp, fileNamesNewSelected_[row], wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, &extentSelect); + rectTmp.x += extentSelect.GetWidth(); + rectTmp.width -= extentSelect.GetWidth(); + } + + drawCellText(dc, rectTmp, fileNamesNewSelectAfter_[row]); + + if (fileNamesNewSelected_[row].empty() && //draw input cursor + (showCursor_ || std::chrono::steady_clock::now() < previewChangeTime_ + std::chrono::milliseconds(400))) + { + wxDCPenChanger dummy(dc, wxPen(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), fastFromDIP(1))); + dc.DrawLine(rectTmp.GetTopLeft(), rectTmp.GetBottomLeft() + wxPoint(0, 1)); //DrawLine() doesn't draw last pixel! + } + } + break; + } } int getBestSize(wxDC& dc, size_t row, ColumnType colType) override @@ -93,9 +167,20 @@ public: return std::wstring(); } + void setCursorShown(bool show) { showCursor_ = show; } + private: const std::vector<std::wstring> fileNamesOld_; - std::vector<std::wstring> fileNamesNew_; + + std::tuple<std::wstring /*renamePhrase*/, size_t /*selectBegin*/, size_t /*selectEnd*/> lastUsedPhrase_; + + std::vector<std::wstring> fileNamesNewSelectBefore_{fileNamesOld_.size()}; + std::vector<std::wstring> fileNamesNewSelected_ {fileNamesOld_.size()}; + std::vector<std::wstring> fileNamesNewSelectAfter_ {fileNamesOld_.size()}; + + bool showCursor_ = false; + std::chrono::steady_clock::time_point previewChangeTime_ = std::chrono::steady_clock::now(); + const SharedRef<const RenameBuf> renameBuf_; }; @@ -106,17 +191,24 @@ public: RenameDialog(wxWindow* parent, const std::vector<std::wstring>& fileNamesOld, std::vector<Zstring>& fileNamesNew); private: - void onTypingName(wxCommandEvent& event) override { updatePreview(); } - void onOkay (wxCommandEvent& event) override; - void onCancel (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } - void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onOkay (wxCommandEvent& event) override; + void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } + void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onLocalKeyEvent(wxKeyEvent& event); void updatePreview() { - getDataView().updatePreview(copyStringTo<std::wstring>(trimCpy(m_textCtrlNewName->GetValue()))); - m_gridRenamePreview->Refresh(); + const std::wstring renamePhrase = copyStringTo<std::wstring>(m_textCtrlNewName->GetValue()); + + long selectBegin = 0; + long selectEnd = 0; + m_textCtrlNewName->GetSelection(&selectBegin, &selectEnd); + + assert(selectBegin == m_textCtrlNewName->GetInsertionPoint()); //apparently this is true for all Win/macOS/Linux + + if (getDataView().updatePreview(renamePhrase, selectBegin, selectEnd)) + m_gridRenamePreview->Refresh(); } GridDataRename& getDataView() @@ -126,6 +218,9 @@ private: throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] m_gridRenamePreview was not initialized."); } + wxTimer timer_; //poll for text selection changes + wxTimer timerCursor_; //second timer just for cursor blinking + //output-only parameters: std::vector<Zstring>& fileNamesNewOut_; }; @@ -146,33 +241,118 @@ RenameDialog::RenameDialog(wxWindow* parent, m_staticTextHeader->SetLabelText(_P("Do you really want to rename the following item?", "Do you really want to rename the following %x items?", fileNamesOld.size())); - m_staticTextHeader->Wrap(fastFromDIP(460)); //needs to be reapplied after SetLabel() - m_buttonOK->SetLabelText(wxControl::RemoveMnemonics(_("&Rename"))); //no access key needed: use ENTER! - const auto& [renamePhrase, renameBuf] = getPlaceholderPhrase(fileNamesOld); + auto [renamePhrase, renameBuf] = getPlaceholderPhrase(fileNamesOld); + trim(renamePhrase); //leading/trailing whitespace makes no sense for file names + + + std::wstring placeholders; + for (const wchar_t c : renamePhrase) + if (isRenamePlaceholderChar(c)) + placeholders += c; + + m_staticTextPlaceholderDescription->SetLabelText(placeholders + L": " + m_staticTextPlaceholderDescription->GetLabelText()); //----------------------------------------------------------- + m_gridRenamePreview->setDataProvider(std::make_shared<GridDataRename>(fileNamesOld, renameBuf)); m_gridRenamePreview->showRowLabel(false); m_gridRenamePreview->setRowHeight(m_gridRenamePreview->getMainWin().GetCharHeight() + fastFromDIP(1) /*extra space*/); - m_gridRenamePreview->setColumnConfig( + + //----------------------------------------------------------- + if (fileNamesOld.size() > 1) //calculate reasonable default preview grid size { - {static_cast<ColumnType>(ColumnTypeRename::oldName), 0, 1, true}, - {static_cast<ColumnType>(ColumnTypeRename::newName), 0, 1, true}, - }); + //quick and dirty: get (likely) maximum string width while avoiding excessive wxDC::GetTextExtent() calls + std::vector<std::wstring> names = fileNamesOld; + auto itMax10 = names.end() - std::min<size_t>(10, names.size()); //find the 10 longest strings according to std::wstring::size() + if (itMax10 != names.begin()) + std::nth_element(names.begin(), itMax10, names.end(), + /**/[](const std::wstring& lhs, const std::wstring& rhs) { return lhs.size() < rhs.size(); }); //complexity: O(n) + + wxMemoryDC dc; //the context used for bitmaps + setScaleFactor(dc, getDisplayScaleFactor()); + dc.SetFont(m_gridRenamePreview->GetFont()); //the font parameter of GetTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + + int maxStringWidth = 0; + std::for_each(itMax10, names.end(), [&](const std::wstring& str) + { + maxStringWidth = std::max(maxStringWidth, dc.GetTextExtent(str).GetWidth()); + }); - m_gridRenamePreview->setDataProvider(std::make_shared<GridDataRename>(fileNamesOld, renameBuf)); + const int defaultColWidthOld = maxStringWidth + 2 * GridData::getColumnGapLeft() + fastFromDIP(1) /*border*/ + fastFromDIP(10) /*extra space: less cramped*/; + const int defaultColWidthNew = maxStringWidth + 2 * GridData::getColumnGapLeft() + fastFromDIP(1) /*border*/ + fastFromDIP(50) /*extra space: for longer new name*/; + + m_gridRenamePreview->setColumnConfig( + { + {static_cast<ColumnType>(ColumnTypeRename::oldName), defaultColWidthOld, 0, true}, //"old name" is fixed => + {static_cast<ColumnType>(ColumnTypeRename::newName), -defaultColWidthOld, 1, true}, //stretch "new name" only + }); - warn_static("make smarter!") - m_gridRenamePreview->SetMinSize({fastFromDIP(500), fastFromDIP(200)}); + const int previewDefaultWidth = std::min(defaultColWidthOld + defaultColWidthNew + fastFromDIP(25), //scroll bar width (guess!) + fastFromDIP(900)); + + const int previewDefaultHeight = std::min(m_gridRenamePreview->getColumnLabelHeight() + + static_cast<int>(fileNamesOld.size()) * m_gridRenamePreview->getRowHeight(), + fastFromDIP(400)); + + m_gridRenamePreview->SetMinSize({previewDefaultWidth, previewDefaultHeight}); + + m_staticTextHeader->Wrap(std::max(previewDefaultWidth, fastFromDIP(400))); //needs to be reapplied after SetLabel() + } + else //renaming single file + { + m_gridRenamePreview ->Hide(); + m_staticlinePreview ->Hide(); + m_staticTextPlaceholderDescription->Hide(); + + wxMemoryDC dc; //the context used for bitmaps + setScaleFactor(dc, getDisplayScaleFactor()); + dc.SetFont(m_textCtrlNewName->GetFont()); //the font parameter of GetTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + + const int textCtrlDefaultWidth = std::min(dc.GetTextExtent(renamePhrase).GetWidth() + 20 /*borders (non-DIP!)*/ + + fastFromDIP(50) /*extra space: for longer new name*/, + fastFromDIP(900)); + m_textCtrlNewName->SetMinSize({textCtrlDefaultWidth, -1}); + + m_staticTextHeader->Wrap(std::max(textCtrlDefaultWidth, fastFromDIP(400))); //needs to be reapplied after SetLabel() + } //----------------------------------------------------------- + wxTextValidator inputValidator(wxFILTER_EXCLUDE_CHAR_LIST); + inputValidator.SetCharExcludes(LR"(<>:"/\|?*)"); //chars forbidden for file names (at least on Windows) + //https://docs.microsoft.com/de-de/windows/win32/fileio/naming-a-file#naming-conventions + m_textCtrlNewName->SetValidator(inputValidator); m_textCtrlNewName->ChangeValue(renamePhrase); - updatePreview(); + + if (fileNamesOld.size() > 1) + { + timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { updatePreview(); }); //poll to detect text selection changes + timer_.Start(100 /*unit: [ms]*/); + + timerCursor_.Bind(wxEVT_TIMER, [this, show = true](wxTimerEvent& event) mutable //trigger blinking cursor + { + getDataView().setCursorShown(show); + m_gridRenamePreview->Refresh(); + show = !show; + }); + timerCursor_.Start(wxCaret::GetBlinkTime() /*unit: [ms]*/); + } + + m_textCtrlNewName->Bind(wxEVT_COMMAND_TEXT_UPDATED, [this, renamePhraseOld = renamePhrase, needPreview = fileNamesOld.size() > 1](wxCommandEvent& event) + { + if (needPreview) + updatePreview(); //(almost?) redundant, considering timer_ is doing the same!? + + //disable OK button, until user changes input + const std::wstring renamePhraseNew = trimCpy(copyStringTo<std::wstring>(m_textCtrlNewName->GetValue())); + m_buttonOK->Enable(!renamePhraseNew.empty() && renamePhraseNew != renamePhraseOld); //supports polling + }); + m_buttonOK->Disable(); Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //enable dialog-specific key events + //----------------------------------------------------------- GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() #ifdef __WXGTK3__ Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088 @@ -181,16 +361,28 @@ RenameDialog::RenameDialog(wxWindow* parent, Center(); //needs to be re-applied after a dialog size change! m_textCtrlNewName->SetFocus(); //[!] required *before* SetSelection() on wxGTK + //----------------------------------------------------------- + + //macOS issue: the *whole* text control is selected by default, unless we SetSelection() *after* wxDialog::Show()! + CallAfter([this, nameCount = fileNamesOld.size(), renamePhrase = renamePhrase] + { + //pre-select name part that user will most likely change + //assert(contains(renamePhrase, L'\u2776') == nameCount > 1); -> fails, if user selects same item on left and right grid + auto it = std::find_if(renamePhrase.begin(), renamePhrase.end(), isRenamePlaceholderChar); //❶ + if (it == renamePhrase.end()) + it = findLast(renamePhrase.begin(), renamePhrase.end(), L'.'); //select everything except file extension + + if (it == renamePhrase.end()) + m_textCtrlNewName->SelectAll(); + else + { + const long selectEnd = static_cast<long>(it - renamePhrase.begin()); + m_textCtrlNewName->SetSelection(0, selectEnd); + } + + updatePreview(); //consider new selection + }); - //pre-select name part that user will most likely change - assert(contains(renamePhrase, L'\u2776') == fileNamesOld.size() > 1); - auto it = fileNamesOld.size() == 1 ? - findLast (renamePhrase.begin(), renamePhrase.end(), L'.') : //select everything except file extension - std::find(renamePhrase.begin(), renamePhrase.end(), L'\u2776'); //❶ - if (it == renamePhrase.end()) - m_textCtrlNewName->SelectAll(); - else - m_textCtrlNewName->SetSelection(0, static_cast<long>(it - renamePhrase.begin())); } @@ -202,6 +394,8 @@ void RenameDialog::onLocalKeyEvent(wxKeyEvent& event) void RenameDialog::onOkay(wxCommandEvent& event) { + updatePreview(); //ensure GridDataRename::getNewNames() is current + fileNamesNewOut_.clear(); for (const std::wstring& newName : getDataView().getNewNames()) fileNamesNewOut_.push_back(utfTo<Zstring>(newName)); @@ -217,7 +411,7 @@ ConfirmationButton fff::showRenameDialog(wxWindow* parent, { std::vector<std::wstring> namesOld; for (const Zstring& name : fileNamesOld) - namesOld.push_back(utfTo<std::wstring>(name)); + namesOld.push_back(utfTo<std::wstring>(getUnicodeNormalForm(name))); //[!] don't care about Normalization form differences! RenameDialog dlg(parent, namesOld, fileNamesNew); return static_cast<ConfirmationButton>(dlg.ShowModal()); diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 16140cca..2724de44 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -78,7 +78,6 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) setBitmapTextLabel(*m_bpButtonEmail, loadImage("ffs_email"), wxString() + L"zenju@" + /*don't leave full email in either source or binary*/ L"freefilesync.org"); m_bpButtonEmail->SetToolTip( wxString() + L"mailto:zenju@" + /*don't leave full email in either source or binary*/ L"freefilesync.org"); - wxString build = utfTo<wxString>(ffsVersion); const wchar_t* const SPACED_BULLET = L" \u2022 "; @@ -108,7 +107,7 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) setRelativeFontSize(*m_buttonDonate1, 1.25); setBitmapTextLabel(*m_buttonDonate1, loadImage("ffs_heart", fastFromDIP(28)), m_buttonDonate1->GetLabelText()); - m_buttonShowDonationDetails->Hide(); + m_buttonShowSupporterDetails->Hide(); m_buttonDonate2->Hide(); } @@ -896,8 +895,7 @@ class CopyToDialog : public CopyToDlgGenerated { public: CopyToDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, + const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector<Zstring>& folderHistory, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, @@ -922,8 +920,7 @@ private: CopyToDialog::CopyToDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, + const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector<Zstring>& folderHistory, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, @@ -955,8 +952,6 @@ CopyToDialog::CopyToDialog(wxWindow* parent, => another Unity problem like the following? https://github.com/wxWidgets/wxWidgets/issues/14823 "Menu not disabled when showing modal dialogs in wxGTK under Unity" */ - const auto& [itemList, itemCount] = getSelectedItemsAsString(rowsOnLeft, rowsOnRight); - m_staticTextHeader->SetLabelText(_P("Copy the following item to another folder?", "Copy the following %x items to another folder?", itemCount)); m_staticTextHeader->Wrap(fastFromDIP(460)); //needs to be reapplied after SetLabel() @@ -1011,15 +1006,14 @@ void CopyToDialog::onOkay(wxCommandEvent& event) } ConfirmationButton fff::showCopyToDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, + const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector<Zstring>& folderHistory, size_t folderHistoryMax, Zstring& sftpKeyFileLastSelected, bool& keepRelPaths, bool& overwriteIfExists) { - CopyToDialog dlg(parent, rowsOnLeft, rowsOnRight, targetFolderPath, targetFolderLastSelected, folderHistory, folderHistoryMax, sftpKeyFileLastSelected, keepRelPaths, overwriteIfExists); + CopyToDialog dlg(parent, itemList, itemCount, targetFolderPath, targetFolderLastSelected, folderHistory, folderHistoryMax, sftpKeyFileLastSelected, keepRelPaths, overwriteIfExists); return static_cast<ConfirmationButton>(dlg.ShowModal()); } @@ -2061,13 +2055,15 @@ ActivationDlg::ActivationDlg(wxWindow* parent, { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setCancel(m_buttonCancel)); - SetTitle(L"FreeFileSync " + utfTo<std::wstring>(ffsVersion) + L" [" + _("Donation Edition") + L']'); + std::wstring title = L"FreeFileSync " + utfTo<std::wstring>(ffsVersion); + SetTitle(title); + + //setMainInstructionFont(*m_staticTextMain); m_richTextLastError ->SetMinSize({-1, m_richTextLastError ->GetCharHeight() * 7}); m_richTextManualActivationUrl->SetMinSize({-1, m_richTextManualActivationUrl->GetCharHeight() * 4}); m_textCtrlOfflineActivationKey->SetMinSize({fastFromDIP(260), -1}); - //setMainInstructionFont(*m_staticTextMain); - + setImage(*m_bitmapActivation, loadImage("internet")); m_textCtrlOfflineActivationKey->ForceUpper(); diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h index 81a3dd0d..2ed304d6 100644 --- a/FreeFileSync/Source/ui/small_dlgs.h +++ b/FreeFileSync/Source/ui/small_dlgs.h @@ -20,8 +20,7 @@ namespace fff void showAboutDialog(wxWindow* parent); zen::ConfirmationButton showCopyToDialog(wxWindow* parent, - std::span<const FileSystemObject* const> rowsOnLeft, - std::span<const FileSystemObject* const> rowsOnRight, + const std::wstring& itemList, int itemCount, Zstring& targetFolderPath, Zstring& targetFolderLastSelected, std::vector<Zstring>& folderPathHistory, size_t folderPathHistoryMax, Zstring& sftpKeyFileLastSelected, diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index f1ab6afd..0a2a7baa 100644 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -281,13 +281,13 @@ private: EnumDescrList<UnitSize> enumSizeDescr_; //------------- synchronization panel ----------------- - void onSyncTwoWay(wxCommandEvent& event) override { directionCfg_.var = SyncVariant::twoWay; updateSyncGui(); } - void onSyncMirror(wxCommandEvent& event) override { directionCfg_.var = SyncVariant::mirror; updateSyncGui(); } - void onSyncUpdate(wxCommandEvent& event) override { directionCfg_.var = SyncVariant::update; updateSyncGui(); } - void onSyncCustom(wxCommandEvent& event) override { directionCfg_.var = SyncVariant::custom; updateSyncGui(); } + void onSyncTwoWay(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::twoWay); updateSyncGui(); } + void onSyncMirror(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::mirror); updateSyncGui(); } + void onSyncUpdate(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::update); updateSyncGui(); } + void onSyncCustom(wxCommandEvent& event) override { directionsCfg_ = getDefaultSyncCfg(SyncVariant::custom); updateSyncGui(); } void onToggleLocalSyncSettings(wxCommandEvent& event) override { updateSyncGui(); } - void onToggleDetectMovedFiles (wxCommandEvent& event) override { directionCfg_.detectMovedFiles = !directionCfg_.detectMovedFiles; updateSyncGui(); } //parameter NOT owned by checkbox! + void onToggleUseDatabase (wxCommandEvent& event) override; void onChanegVersioningStyle (wxCommandEvent& event) override { updateSyncGui(); } void onToggleVersioningLimit (wxCommandEvent& event) override { updateSyncGui(); } @@ -296,16 +296,24 @@ private: void onSyncUpdateDouble(wxMouseEvent& event) override; void onSyncCustomDouble(wxMouseEvent& event) override; - void onExLeftSideOnly (wxCommandEvent& event) override; - void onExRightSideOnly(wxCommandEvent& event) override; - void onLeftNewer (wxCommandEvent& event) override; - void onRightNewer (wxCommandEvent& event) override; - void onDifferent (wxCommandEvent& event) override; - void onConflict (wxCommandEvent& event) override; - - void onDeletionPermanent (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::permanent; updateSyncGui(); } - void onDeletionRecycler (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::recycler; updateSyncGui(); } - void onDeletionVersioning (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::versioning; updateSyncGui(); } + void onLeftOnly (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByDiff::leftOnly); } + void onRightOnly (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByDiff::rightOnly); } + void onLeftNewer (wxCommandEvent& event) override; + void onRightNewer(wxCommandEvent& event) override; + void onDifferent (wxCommandEvent& event) override; + void toggleSyncDirButton(SyncDirection DirectionByDiff::* dir); + + void onLeftCreate (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::create); } + void onLeftUpdate (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::update); } + void onLeftDelete (wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::left, &DirectionByChange::Changes::delete_); } + void onRightCreate(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::create); } + void onRightUpdate(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::update); } + void onRightDelete(wxCommandEvent& event) override { toggleSyncDirButton(&DirectionByChange::right, &DirectionByChange::Changes::delete_); } + void toggleSyncDirButton(DirectionByChange::Changes DirectionByChange::* side, SyncDirection DirectionByChange::Changes::* dir); + + void onDeletionPermanent (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::permanent; updateSyncGui(); } + void onDeletionRecycler (wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::recycler; updateSyncGui(); } + void onDeletionVersioning(wxCommandEvent& event) override { deletionVariant_ = DeletionVariant::versioning; updateSyncGui(); } void onToggleMiscOption(wxCommandEvent& event) override { updateMiscGui(); } void onToggleMiscEmail (wxCommandEvent& event) override @@ -323,11 +331,13 @@ private: std::optional<SyncConfig> getSyncConfig() const; void setSyncConfig(const SyncConfig* syncCfg); + bool leftRightNewerCombined() const; + void updateSyncGui(); //----------------------------------------------------- //parameters with ownership NOT within GUI controls! - SyncDirectionConfig directionCfg_; + SyncDirectionConfig directionsCfg_; DeletionVariant deletionVariant_ = DeletionVariant::recycler; //use Recycler, delete permanently or move to user-defined location const std::function<size_t(const Zstring& folderPathPhrase)> getDeviceParallelOps_; @@ -368,7 +378,7 @@ private: std::vector<LocalPairConfig> localPairCfg_; int selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED; - static const int EMPTY_PAIR_INDEX_SELECTED = -2; + static constexpr int EMPTY_PAIR_INDEX_SELECTED = -2; bool showNotesPanel_ = false; @@ -581,12 +591,12 @@ globalLogFolderPhrase_(globalLogFolderPhrase) m_buttonUpdate->SetToolTip(getSyncVariantDescription(SyncVariant::update)); m_buttonCustom->SetToolTip(getSyncVariantDescription(SyncVariant::custom)); - setImage(*m_bitmapLeftOnly, mirrorIfRtl(greyScale(loadImage("cat_left_only" )))); - setImage(*m_bitmapRightOnly, mirrorIfRtl(greyScale(loadImage("cat_right_only" )))); - setImage(*m_bitmapLeftNewer, mirrorIfRtl(greyScale(loadImage("cat_left_newer" )))); - setImage(*m_bitmapRightNewer, mirrorIfRtl(greyScale(loadImage("cat_right_newer")))); - setImage(*m_bitmapDifferent, mirrorIfRtl(greyScale(loadImage("cat_different" )))); - setImage(*m_bitmapConflict, mirrorIfRtl(greyScale(loadImage("cat_conflict" )))); + const int catSizeMax = loadImage("cat_left_only").GetWidth() * 8 / 10; + setImage(*m_bitmapLeftOnly, mirrorIfRtl(greyScale(loadImage("cat_left_only", catSizeMax)))); + setImage(*m_bitmapRightOnly, mirrorIfRtl(greyScale(loadImage("cat_right_only", catSizeMax)))); + setImage(*m_bitmapLeftNewer, mirrorIfRtl(greyScale(loadImage("cat_left_newer", catSizeMax)))); + setImage(*m_bitmapRightNewer, mirrorIfRtl(greyScale(loadImage("cat_right_newer", catSizeMax)))); + setImage(*m_bitmapDifferent, mirrorIfRtl(greyScale(loadImage("cat_different", catSizeMax)))); setRelativeFontSize(*m_buttonTwoWay, 1.25); setRelativeFontSize(*m_buttonMirror, 1.25); @@ -684,7 +694,7 @@ globalLogFolderPhrase_(globalLogFolderPhrase) //temporarily set main config as reference for window min size calculations: globalPairCfg_ = GlobalPairConfig(); - globalPairCfg_.syncCfg.directionCfg.var = SyncVariant::mirror; + globalPairCfg_.syncCfg.directionCfg = getDefaultSyncCfg(SyncVariant::twoWay); globalPairCfg_.syncCfg.deletionVariant = DeletionVariant::versioning; globalPairCfg_.syncCfg.versioningFolderPhrase = Zstr("dummy"); globalPairCfg_.syncCfg.versioningStyle = VersioningStyle::timestampFile; @@ -702,8 +712,8 @@ globalLogFolderPhrase_(globalLogFolderPhrase) #endif Center(); //needs to be re-applied after a dialog size change! - //keep stable sizer height: "two way" description is smaller than grid of sync directions - bSizerSyncDirHolder ->SetMinSize(-1, bSizerSyncDirections ->GetSize().y); + //keep stable sizer height: change-based directions are taller than difference-based ones => init with SyncVariant::twoWay + bSizerSyncDirHolder ->SetMinSize(-1, bSizerSyncDirsChanges ->GetSize().y); bSizerVersioningHolder->SetMinSize(-1, bSizerVersioningHolder->GetSize().y); unselectFolderPairConfig(false /*validateParams*/); @@ -934,13 +944,15 @@ FilterConfig ConfigDialog::getFilterConfig() const const Zstring& includeFilter = utfTo<Zstring>(m_textCtrlInclude->GetValue()); const Zstring& exludeFilter = utfTo<Zstring>(m_textCtrlExclude->GetValue()); - return FilterConfig(includeFilter, exludeFilter, - m_spinCtrlTimespan->GetValue(), - getEnumVal(enumTimeDescr_, *m_choiceUnitTimespan), - m_spinCtrlMinSize->GetValue(), - getEnumVal(enumSizeDescr_, *m_choiceUnitMinSize), - m_spinCtrlMaxSize->GetValue(), - getEnumVal(enumSizeDescr_, *m_choiceUnitMaxSize)); + return + { + includeFilter, exludeFilter, + makeUnsigned(m_spinCtrlTimespan->GetValue()), + getEnumVal(enumTimeDescr_, *m_choiceUnitTimespan), + makeUnsigned(m_spinCtrlMinSize->GetValue()), + getEnumVal(enumSizeDescr_, *m_choiceUnitMinSize), + makeUnsigned(m_spinCtrlMaxSize->GetValue()), + getEnumVal(enumSizeDescr_, *m_choiceUnitMaxSize)}; } @@ -982,6 +994,19 @@ void ConfigDialog::updateFilterGui() } +void ConfigDialog::onToggleUseDatabase(wxCommandEvent& event) +{ + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs)) + directionsCfg_.dirs = getChangesDirDefault(*diffDirs); + else + { + const DirectionByChange& changeDirs = std::get<DirectionByChange>(directionsCfg_.dirs); + directionsCfg_.dirs = getDiffDirDefault(changeDirs); + } + updateSyncGui(); +} + + void ConfigDialog::onSyncTwoWayDouble(wxMouseEvent& event) { wxCommandEvent dummy; @@ -1031,145 +1056,116 @@ void toggleSyncDirection(SyncDirection& current) } -void toggleCustomSyncConfig(SyncDirectionConfig& directionCfg, SyncDirection& custSyncDir) +void ConfigDialog::toggleSyncDirButton(SyncDirection DirectionByDiff::* dir) { - switch (directionCfg.var) - { - case SyncVariant::twoWay: - assert(false); - break; - case SyncVariant::mirror: - case SyncVariant::update: - directionCfg.custom = extractDirections(directionCfg); - break; - case SyncVariant::custom: - break; - } - SyncDirection syncDirOld = custSyncDir; - toggleSyncDirection(custSyncDir); - - //some config optimization: if custom settings happen to match "mirror" or "update", just switch variant - const DirectionSet mirrorSet = [] - { - SyncDirectionConfig mirrorCfg; - mirrorCfg.var = SyncVariant::mirror; - return extractDirections(mirrorCfg); - }(); - - const DirectionSet updateSet = [] - { - SyncDirectionConfig updateCfg; - updateCfg.var = SyncVariant::update; - return extractDirections(updateCfg); - }(); - - if (directionCfg.custom == mirrorSet) - { - directionCfg.var = SyncVariant::mirror; - custSyncDir = syncDirOld; - } - else if (directionCfg.custom == updateSet) + if (DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs)) { - directionCfg.var = SyncVariant::update; - custSyncDir = syncDirOld; + toggleSyncDirection(diffDirs->*dir); + updateSyncGui(); } - else - directionCfg.var = SyncVariant::custom; + else assert(false); } -void ConfigDialog::onExLeftSideOnly(wxCommandEvent& event) +void ConfigDialog::onLeftNewer(wxCommandEvent& event) { - toggleCustomSyncConfig(directionCfg_, directionCfg_.custom.exLeftSideOnly); - updateSyncGui(); + toggleSyncDirButton(&DirectionByDiff::leftNewer); + assert(!leftRightNewerCombined()); } -void ConfigDialog::onExRightSideOnly(wxCommandEvent& event) +void ConfigDialog::onRightNewer(wxCommandEvent& event) { - toggleCustomSyncConfig(directionCfg_, directionCfg_.custom.exRightSideOnly); - updateSyncGui(); + toggleSyncDirButton(&DirectionByDiff::rightNewer); + assert(!leftRightNewerCombined()); } -void ConfigDialog::onLeftNewer(wxCommandEvent& event) +void ConfigDialog::onDifferent(wxCommandEvent& event) { - toggleCustomSyncConfig(directionCfg_, directionCfg_.custom.leftNewer); - updateSyncGui(); + toggleSyncDirButton(&DirectionByDiff::leftNewer); + + if (DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs)) + //simulate category "different" as leftNewer/rightNewer combined: + diffDirs->rightNewer = diffDirs->leftNewer; + else assert(false); + assert(leftRightNewerCombined()); } -void ConfigDialog::onRightNewer(wxCommandEvent& event) +void ConfigDialog::toggleSyncDirButton(DirectionByChange::Changes DirectionByChange::* side, SyncDirection DirectionByChange::Changes::* dir) { - toggleCustomSyncConfig(directionCfg_, directionCfg_.custom.rightNewer); - updateSyncGui(); + if (DirectionByChange* changeDirs = std::get_if<DirectionByChange>(&directionsCfg_.dirs)) + { + toggleSyncDirection(changeDirs->*side.*dir); + updateSyncGui(); + } + else assert(false); } -void ConfigDialog::onDifferent(wxCommandEvent& event) +namespace { - toggleCustomSyncConfig(directionCfg_, directionCfg_.custom.different); - updateSyncGui(); +auto updateDirButton(wxBitmapButton& button, SyncDirection dir, + const char* imgNameLeft, const char* imgNameNone, const char* imgNameRight, + SyncOperation opLeft, SyncOperation opNone, SyncOperation opRight) +{ + const char* imgName = nullptr; + switch (dir) + { + case SyncDirection::left: + imgName = imgNameLeft; + button.SetToolTip(getSyncOpDescription(opLeft)); + break; + case SyncDirection::none: + imgName = imgNameNone; + button.SetToolTip(getSyncOpDescription(opNone)); + break; + case SyncDirection::right: + imgName = imgNameRight; + button.SetToolTip(getSyncOpDescription(opRight)); + break; + } + wxImage img = mirrorIfRtl(loadImage(imgName)); + button.SetBitmapLabel (toScaledBitmap( img)); + button.SetBitmapDisabled(toScaledBitmap(greyScale(img))); //fix wxWidgets' all-too-clever multi-state! + //=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets! } -void ConfigDialog::onConflict(wxCommandEvent& event) +void updateDiffDirButtons(const DirectionByDiff& diffDirs, + wxBitmapButton& buttonLeftOnly, + wxBitmapButton& buttonRightOnly, + wxBitmapButton& buttonLeftNewer, + wxBitmapButton& buttonRightNewer, + wxBitmapButton& buttonDifferent) { - toggleCustomSyncConfig(directionCfg_, directionCfg_.custom.conflict); - updateSyncGui(); + updateDirButton(buttonLeftOnly, diffDirs.leftOnly, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT); + updateDirButton(buttonRightOnly, diffDirs.rightOnly, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT); + updateDirButton(buttonLeftNewer, diffDirs.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + updateDirButton(buttonRightNewer, diffDirs.rightNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + //simulate category "different" as leftNewer/rightNewer combined: + updateDirButton(buttonDifferent, diffDirs.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); } -void updateSyncDirectionIcons(const SyncDirectionConfig& directionCfg, - wxBitmapButton& buttonLeftOnly, - wxBitmapButton& buttonRightOnly, - wxBitmapButton& buttonLeftNewer, - wxBitmapButton& buttonRightNewer, - wxBitmapButton& buttonDifferent, - wxBitmapButton& buttonConflict) +void updateChangeDirButtons(const DirectionByChange& changeDirs, + wxBitmapButton& buttonLeftCreate, + wxBitmapButton& buttonLeftUpdate, + wxBitmapButton& buttonLeftDelete, + wxBitmapButton& buttonRightCreate, + wxBitmapButton& buttonRightUpdate, + wxBitmapButton& buttonRightDelete) { - if (directionCfg.var != SyncVariant::twoWay) //automatic mode needs no sync-directions - { - auto updateButton = [](wxBitmapButton& button, SyncDirection dir, - const char* imgNameLeft, const char* imgNameNone, const char* imgNameRight, - SyncOperation opLeft, SyncOperation opNone, SyncOperation opRight) - { - const char* imgName = nullptr; - switch (dir) - { - case SyncDirection::left: - imgName = imgNameLeft; - button.SetToolTip(getSyncOpDescription(opLeft)); - break; - case SyncDirection::none: - imgName = imgNameNone; - button.SetToolTip(getSyncOpDescription(opNone)); - break; - case SyncDirection::right: - imgName = imgNameRight; - button.SetToolTip(getSyncOpDescription(opRight)); - break; - } - wxImage img = mirrorIfRtl(loadImage(imgName)); - button.SetBitmapLabel (toScaledBitmap( img)); - button.SetBitmapDisabled(toScaledBitmap(greyScale(img))); //fix wxWidgets' all-too-clever multi-state! - //=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets! - }; - - const DirectionSet dirCfg = extractDirections(directionCfg); + updateDirButton(buttonLeftCreate, changeDirs.left.create, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT); + updateDirButton(buttonLeftUpdate, changeDirs.left.update, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + updateDirButton(buttonLeftDelete, changeDirs.left.delete_, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT); - updateButton(buttonLeftOnly, dirCfg.exLeftSideOnly, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_NEW_RIGHT); - updateButton(buttonRightOnly, dirCfg.exRightSideOnly, "so_create_left", "so_none", "so_delete_right", SO_CREATE_NEW_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT ); - updateButton(buttonLeftNewer, dirCfg.leftNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT ); - updateButton(buttonRightNewer, dirCfg.rightNewer, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT ); - updateButton(buttonDifferent, dirCfg.different, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT ); - - updateButton(buttonConflict, dirCfg.conflict, "so_update_left", "cat_conflict", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT ); - if (dirCfg.conflict == SyncDirection::none) - buttonConflict.SetToolTip(_("Leave as unresolved conflict")); //silent dependency from algorithm.cpp::Redetermine!!! - } + updateDirButton(buttonRightCreate, changeDirs.right.create, "so_create_left", "so_none", "so_delete_right", SO_CREATE_LEFT, SO_DO_NOTHING, SO_DELETE_RIGHT); + updateDirButton(buttonRightUpdate, changeDirs.right.update, "so_update_left", "so_none", "so_update_right", SO_OVERWRITE_LEFT, SO_DO_NOTHING, SO_OVERWRITE_RIGHT); + updateDirButton(buttonRightDelete, changeDirs.right.delete_, "so_delete_left", "so_none", "so_create_right", SO_DELETE_LEFT, SO_DO_NOTHING, SO_CREATE_RIGHT); +} } - void ConfigDialog::onShowLogFolder(wxCommandEvent& event) { @@ -1187,13 +1183,21 @@ void ConfigDialog::onShowLogFolder(wxCommandEvent& event) } +bool ConfigDialog::leftRightNewerCombined() const +{ + assert(std::get_if<DirectionByDiff>(&directionsCfg_.dirs)); + const CompareVariant activeCmpVar = m_checkBoxUseLocalCmpOptions->GetValue() ? localCmpVar_ : globalPairCfg_.cmpCfg.compareVar; + return activeCmpVar == CompareVariant::content || activeCmpVar == CompareVariant::size; +} + + std::optional<SyncConfig> ConfigDialog::getSyncConfig() const { if (!m_checkBoxUseLocalSyncOptions->GetValue()) return {}; SyncConfig syncCfg; - syncCfg.directionCfg = directionCfg_; + syncCfg.directionCfg = directionsCfg_; syncCfg.deletionVariant = deletionVariant_; syncCfg.versioningFolderPhrase = versioningFolder_.getPath(); syncCfg.versioningStyle = getEnumVal(enumVersioningStyle_, *m_choiceVersioningStyle); @@ -1203,6 +1207,12 @@ std::optional<SyncConfig> ConfigDialog::getSyncConfig() const syncCfg.versionCountMin = m_checkBoxVersionCountMin->GetValue() && m_checkBoxVersionMaxDays->GetValue() ? m_spinCtrlVersionCountMin->GetValue() : 0; syncCfg.versionCountMax = m_checkBoxVersionCountMax->GetValue() ? m_spinCtrlVersionCountMax->GetValue() : 0; } + + //simulate category "different" as leftNewer/rightNewer combined: + if (DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&syncCfg.directionCfg.dirs)) + if (leftRightNewerCombined()) + diffDirs->rightNewer = diffDirs->leftNewer; + return syncCfg; } @@ -1214,7 +1224,7 @@ void ConfigDialog::setSyncConfig(const SyncConfig* syncCfg) //when local settings are inactive, display (current) global settings instead: const SyncConfig tmpCfg = syncCfg ? *syncCfg : globalPairCfg_.syncCfg; - directionCfg_ = tmpCfg.directionCfg; //make working copy; ownership *not* on GUI + directionsCfg_ = tmpCfg.directionCfg; //make working copy; ownership *not* on GUI deletionVariant_ = tmpCfg.deletionVariant; versioningFolder_.setPath(tmpCfg.versioningFolderPhrase); setEnumVal(enumVersioningStyle_, *m_choiceVersioningStyle, tmpCfg.versioningStyle); @@ -1242,51 +1252,71 @@ void ConfigDialog::updateSyncGui() m_notebook->SetPageImage(static_cast<size_t>(SyncConfigPanel::sync), static_cast<int>(syncOptionsEnabled ? ConfigTypeImage::sync: ConfigTypeImage::syncGrey)); - updateSyncDirectionIcons(directionCfg_, + const bool setDirsByDifferences = std::get_if<DirectionByDiff>(&directionsCfg_.dirs); + + m_checkBoxUseDatabase->SetValue(!setDirsByDifferences); + + //display only relevant sync options + bSizerSyncDirsDiff ->Show( setDirsByDifferences); + bSizerSyncDirsChanges->Show(!setDirsByDifferences); + + if (const DirectionByDiff* diffDirs = std::get_if<DirectionByDiff>(&directionsCfg_.dirs)) //sync directions by differences + { + updateDiffDirButtons(*diffDirs, *m_bpButtonLeftOnly, *m_bpButtonRightOnly, *m_bpButtonLeftNewer, *m_bpButtonRightNewer, - *m_bpButtonDifferent, - *m_bpButtonConflict); - - //selecting "detect move files" does not always make sense: - m_checkBoxDetectMove->Enable(detectMovedFilesSelectable(directionCfg_)); - m_checkBoxDetectMove->SetValue(detectMovedFilesEnabled(directionCfg_)); //parameter NOT owned by checkbox! + *m_bpButtonDifferent); - //display only relevant sync options - bSizerDatabase ->Show(directionCfg_.var == SyncVariant::twoWay); - bSizerSyncDirections->Show(directionCfg_.var != SyncVariant::twoWay); + //simulate category "different" as leftNewer/rightNewer combined: + const bool haveLeftRightNewerCombined = leftRightNewerCombined(); + m_bitmapLeftNewer ->Show(!haveLeftRightNewerCombined); + m_bpButtonLeftNewer ->Show(!haveLeftRightNewerCombined); + m_bitmapRightNewer ->Show(!haveLeftRightNewerCombined); + m_bpButtonRightNewer->Show(!haveLeftRightNewerCombined); - if (directionCfg_.var == SyncVariant::twoWay) - setImage(*m_bitmapDatabase, greyScaleIfDisabled(loadImage("database"), syncOptionsEnabled)); - else + m_bitmapDifferent ->Show(haveLeftRightNewerCombined); + m_bpButtonDifferent->Show(haveLeftRightNewerCombined); + } + else //sync directions by changes { - const CompareVariant activeCmpVar = m_checkBoxUseLocalCmpOptions->GetValue() ? localCmpVar_ : globalPairCfg_.cmpCfg.compareVar; + const DirectionByChange& changeDirs = std::get<DirectionByChange>(directionsCfg_.dirs); + + updateChangeDirButtons(changeDirs, + *m_bpButtonLeftCreate, + *m_bpButtonLeftUpdate, + *m_bpButtonLeftDelete, + *m_bpButtonRightCreate, + *m_bpButtonRightUpdate, + *m_bpButtonRightDelete); + } - m_bitmapLeftNewer ->Show(activeCmpVar == CompareVariant::timeSize); - m_bpButtonLeftNewer ->Show(activeCmpVar == CompareVariant::timeSize); - m_bitmapRightNewer ->Show(activeCmpVar == CompareVariant::timeSize); - m_bpButtonRightNewer->Show(activeCmpVar == CompareVariant::timeSize); + const bool useDatabaseFile = std::get_if<DirectionByChange>(&directionsCfg_.dirs); - m_bitmapDifferent ->Show(activeCmpVar == CompareVariant::content || activeCmpVar == CompareVariant::size); - m_bpButtonDifferent->Show(activeCmpVar == CompareVariant::content || activeCmpVar == CompareVariant::size); - } + setImage(*m_bitmapDatabase, greyScaleIfDisabled(loadImage("database", fastFromDIP(22)), useDatabaseFile && syncOptionsEnabled)); + + //"detect move files" is always active iff database is used: + setImage(*m_bitmapMoveLeft, greyScaleIfDisabled(loadImage("so_move_left", fastFromDIP(20)), useDatabaseFile && syncOptionsEnabled)); + setImage(*m_bitmapMoveRight, greyScaleIfDisabled(loadImage("so_move_right", fastFromDIP(20)), useDatabaseFile && syncOptionsEnabled)); + m_staticTextDetectMove->Enable(useDatabaseFile); + + const SyncVariant syncVar = getSyncVariant(directionsCfg_); //active variant description: - setText(*m_staticTextSyncVarDescription, getSyncVariantDescription(directionCfg_.var)); + setText(*m_staticTextSyncVarDescription, getSyncVariantDescription(syncVar)); m_staticTextSyncVarDescription->Wrap(fastFromDIP(CFG_DESCRIPTION_WIDTH_DIP)); //needs to be reapplied after SetLabel() //update toggle buttons -> they have no parameter-ownership at all! - m_buttonTwoWay->setActive(SyncVariant::twoWay == directionCfg_.var && syncOptionsEnabled); - m_buttonMirror->setActive(SyncVariant::mirror == directionCfg_.var && syncOptionsEnabled); - m_buttonUpdate->setActive(SyncVariant::update == directionCfg_.var && syncOptionsEnabled); - m_buttonCustom->setActive(SyncVariant::custom == directionCfg_.var && syncOptionsEnabled); + m_buttonTwoWay->setActive(SyncVariant::twoWay == syncVar && syncOptionsEnabled); + m_buttonMirror->setActive(SyncVariant::mirror == syncVar && syncOptionsEnabled); + m_buttonUpdate->setActive(SyncVariant::update == syncVar && syncOptionsEnabled); + m_buttonCustom->setActive(SyncVariant::custom == syncVar && syncOptionsEnabled); //syncOptionsEnabled: nudge wxWidgets to render inactive config state (needed on Windows, NOT on Linux!) - m_buttonRecycler ->setActive(DeletionVariant::recycler == deletionVariant_ && syncOptionsEnabled); - m_buttonPermanent ->setActive(DeletionVariant::permanent == deletionVariant_ && syncOptionsEnabled); - m_buttonVersioning->setActive(DeletionVariant::versioning == deletionVariant_ && syncOptionsEnabled); + m_buttonRecycler ->setActive(DeletionVariant::recycler == deletionVariant_ && syncOptionsEnabled); + m_buttonPermanent ->setActive(DeletionVariant::permanent == deletionVariant_ && syncOptionsEnabled); + m_buttonVersioning->setActive(DeletionVariant::versioning == deletionVariant_ && syncOptionsEnabled); switch (deletionVariant_) //unconditionally update image, including "local options off" { @@ -1376,7 +1406,6 @@ void ConfigDialog::updateSyncGui() MiscSyncConfig ConfigDialog::getMiscSyncOptions() const { - assert(selectedPairIndexToShow_ == -1); MiscSyncConfig miscCfg; // Avoid "fake" changed configs! => @@ -1392,8 +1421,8 @@ MiscSyncConfig ConfigDialog::getMiscSyncOptions() const ++i; } //---------------------------------------------------------------------------- - miscCfg.ignoreErrors = m_checkBoxIgnoreErrors ->GetValue(); - miscCfg.autoRetryCount = m_checkBoxAutoRetry ->GetValue() ? m_spinCtrlAutoRetryCount->GetValue() : 0; + miscCfg.ignoreErrors = m_checkBoxIgnoreErrors->GetValue(); + miscCfg.autoRetryCount = m_checkBoxAutoRetry ->GetValue() ? m_spinCtrlAutoRetryCount->GetValue() : 0; miscCfg.autoRetryDelay = std::chrono::seconds(m_spinCtrlAutoRetryDelay->GetValue()); //---------------------------------------------------------------------------- miscCfg.postSyncCommand = m_comboBoxPostSyncCommand->getValue(); @@ -1418,8 +1447,6 @@ MiscSyncConfig ConfigDialog::getMiscSyncOptions() const void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg) { - assert(selectedPairIndexToShow_ == -1); - // Avoid "fake" changed configs! => //- when editting, consider only the deviceParallelOps items corresponding to the currently-used folder paths //- keep parallel ops == 1 only temporarily during edit @@ -1487,70 +1514,72 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg) void ConfigDialog::updateMiscGui() { - const MiscSyncConfig miscCfg = getMiscSyncOptions(); - - setImage(*m_bitmapIgnoreErrors, greyScaleIfDisabled(loadImage("error_ignore_active"), miscCfg.ignoreErrors)); - setImage(*m_bitmapRetryErrors, greyScaleIfDisabled(loadImage("error_retry"), miscCfg.autoRetryCount > 0 )); + if (selectedPairIndexToShow_ == -1) + { + const MiscSyncConfig miscCfg = getMiscSyncOptions(); - fgSizerAutoRetry->Show(miscCfg.autoRetryCount > 0); + setImage(*m_bitmapIgnoreErrors, greyScaleIfDisabled(loadImage("error_ignore_active"), miscCfg.ignoreErrors)); + setImage(*m_bitmapRetryErrors, greyScaleIfDisabled(loadImage("error_retry"), miscCfg.autoRetryCount > 0 )); - m_panelComparisonSettings->Layout(); //showing "retry count" can affect bSizerPerformance! - //---------------------------------------------------------------------------- - const bool sendEmailEnabled = m_checkBoxSendEmail->GetValue(); - setImage(*m_bitmapEmail, greyScaleIfDisabled(loadImage("email"), sendEmailEnabled)); - m_comboBoxEmail->Show(sendEmailEnabled); - - auto updateButton = [successIcon = loadImage("msg_success", getDefaultMenuIconSize()), - warningIcon = loadImage("msg_warning", getDefaultMenuIconSize()), - errorIcon = loadImage("msg_error", getDefaultMenuIconSize()), - sendEmailEnabled, this] (wxBitmapButton& button, ResultsNotification notifyCondition) - { - button.Show(sendEmailEnabled); - if (sendEmailEnabled) - { - wxString tooltip = _("Error"); - wxImage label = errorIcon; + fgSizerAutoRetry->Show(miscCfg.autoRetryCount > 0); - if (notifyCondition == ResultsNotification::always || - notifyCondition == ResultsNotification::errorWarning) - { - tooltip += (L" | ") + _("Warning"); - label = stackImages(label, warningIcon, ImageStackLayout::horizontal, ImageStackAlignment::center); - } - else - label = resizeCanvas(label, {label.GetWidth() + warningIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT); + m_panelComparisonSettings->Layout(); //showing "retry count" can affect bSizerPerformance! + //---------------------------------------------------------------------------- + const bool sendEmailEnabled = m_checkBoxSendEmail->GetValue(); + setImage(*m_bitmapEmail, greyScaleIfDisabled(loadImage("email"), sendEmailEnabled)); + m_comboBoxEmail->Show(sendEmailEnabled); - if (notifyCondition == ResultsNotification::always) + auto updateButton = [successIcon = loadImage("msg_success", getDefaultMenuIconSize()), + warningIcon = loadImage("msg_warning", getDefaultMenuIconSize()), + errorIcon = loadImage("msg_error", getDefaultMenuIconSize()), + sendEmailEnabled, this](wxBitmapButton& button, ResultsNotification notifyCondition) + { + button.Show(sendEmailEnabled); + if (sendEmailEnabled) { - tooltip += (L" | ") + _("Success"); - label = stackImages(label, successIcon, ImageStackLayout::horizontal, ImageStackAlignment::center); + wxString tooltip = _("Error"); + wxImage label = errorIcon; + + if (notifyCondition == ResultsNotification::always || + notifyCondition == ResultsNotification::errorWarning) + { + tooltip += (L" | ") + _("Warning"); + label = stackImages(label, warningIcon, ImageStackLayout::horizontal, ImageStackAlignment::center); + } + else + label = resizeCanvas(label, {label.GetWidth() + warningIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT); + + if (notifyCondition == ResultsNotification::always) + { + tooltip += (L" | ") + _("Success"); + label = stackImages(label, successIcon, ImageStackLayout::horizontal, ImageStackAlignment::center); + } + else + label = resizeCanvas(label, {label.GetWidth() + successIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT); + + button.SetToolTip(tooltip); + button.SetBitmapLabel (toScaledBitmap(notifyCondition == emailNotifyCondition_ && sendEmailEnabled ? label : greyScale(label))); + button.SetBitmapDisabled(toScaledBitmap(greyScale(label))); //fix wxWidgets' all-too-clever multi-state! + //=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets! } - else - label = resizeCanvas(label, {label.GetWidth() + successIcon.GetWidth(), label.GetHeight()}, wxALIGN_LEFT); - - button.SetToolTip(tooltip); - button.SetBitmapLabel (toScaledBitmap(notifyCondition == emailNotifyCondition_ && sendEmailEnabled ? label : greyScale(label))); - button.SetBitmapDisabled(toScaledBitmap(greyScale(label))); //fix wxWidgets' all-too-clever multi-state! - //=> the disabled bitmap is generated during first SetBitmapLabel() call but never updated again by wxWidgets! - } - }; - updateButton(*m_bpButtonEmailAlways, ResultsNotification::always); - updateButton(*m_bpButtonEmailErrorWarning, ResultsNotification::errorWarning); - updateButton(*m_bpButtonEmailErrorOnly, ResultsNotification::errorOnly); - - m_hyperlinkPerfDeRequired2->Show(!enableExtraFeatures_); //required after each bSizerSyncMisc->Show() + }; + updateButton(*m_bpButtonEmailAlways, ResultsNotification::always); + updateButton(*m_bpButtonEmailErrorWarning, ResultsNotification::errorWarning); + updateButton(*m_bpButtonEmailErrorOnly, ResultsNotification::errorOnly); - //---------------------------------------------------------------------------- - setImage(*m_bitmapLogFile, greyScaleIfDisabled(loadImage("log_file", fastFromDIP(20)), m_checkBoxOverrideLogPath->GetValue())); - m_logFolderPath ->Enable(m_checkBoxOverrideLogPath->GetValue()); // - m_buttonSelectLogFolder ->Show(m_checkBoxOverrideLogPath->GetValue()); //enabled status can't be derived from resolved config! - m_bpButtonSelectAltLogFolder->Show(m_checkBoxOverrideLogPath->GetValue()); // + m_hyperlinkPerfDeRequired2->Show(!enableExtraFeatures_); //required after each bSizerSyncMisc->Show() - m_panelSyncSettings->Layout(); //after showing/hiding m_buttonSelectLogFolder + //---------------------------------------------------------------------------- + setImage(*m_bitmapLogFile, greyScaleIfDisabled(loadImage("log_file", fastFromDIP(20)), m_checkBoxOverrideLogPath->GetValue())); + m_logFolderPath ->Enable(m_checkBoxOverrideLogPath->GetValue()); // + m_buttonSelectLogFolder ->Show(m_checkBoxOverrideLogPath->GetValue()); //enabled status can't be derived from resolved config! + m_bpButtonSelectAltLogFolder->Show(m_checkBoxOverrideLogPath->GetValue()); // - m_panelSyncSettings->Refresh(); //removes a few artifacts when toggling email notifications - m_panelLogfile ->Refresh();// + m_panelSyncSettings->Layout(); //after showing/hiding m_buttonSelectLogFolder + m_panelSyncSettings->Refresh(); //removes a few artifacts when toggling email notifications + m_panelLogfile ->Refresh();// + } //---------------------------------------------------------------------------- m_buttonAddNotes->Show(!showNotesPanel_); m_panelNotes ->Show(showNotesPanel_); @@ -1611,10 +1640,9 @@ void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow) addDevicePath(globalPairCfg_.syncCfg.versioningFolderPhrase); //--------------------------------------------------------------------------------------------------------------- - setCompConfig (&globalPairCfg_.cmpCfg); - setSyncConfig (&globalPairCfg_.syncCfg); - setFilterConfig (globalPairCfg_.filter); - setMiscSyncOptions(globalPairCfg_.miscCfg); + setCompConfig (&globalPairCfg_.cmpCfg); + setSyncConfig (&globalPairCfg_.syncCfg); + setFilterConfig(globalPairCfg_.filter); } else { @@ -1622,6 +1650,7 @@ void ConfigDialog::selectFolderPairConfig(int newPairIndexToShow) setSyncConfig(get(localPairCfg_[selectedPairIndexToShow_].localSyncCfg)); setFilterConfig (localPairCfg_[selectedPairIndexToShow_].localFilter); } + setMiscSyncOptions(globalPairCfg_.miscCfg); m_panelCompSettingsTab ->Layout(); //fix comp panel glitch on Win 7 125% font size + perf panel m_panelFilterSettingsTab->Layout(); @@ -1637,9 +1666,7 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) std::optional<SyncConfig> syncCfg = getSyncConfig(); FilterConfig filterCfg = getFilterConfig(); - std::optional<MiscSyncConfig> miscCfg; - if (selectedPairIndexToShow_ < 0) - miscCfg = getMiscSyncOptions(); + MiscSyncConfig miscCfg = getMiscSyncOptions(); //some "misc" options are always visible, e.g. "notes" //------- parameter validation (BEFORE writing output!) ------- if (validateParams) @@ -1692,18 +1719,18 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) if (selectedPairIndexToShow_ < 0) { - if (AFS::isNullPath(createAbstractPath(miscCfg->altLogFolderPathPhrase)) && - !miscCfg->altLogFolderPathPhrase.empty()) + if (AFS::isNullPath(createAbstractPath(miscCfg.altLogFolderPathPhrase)) && + !miscCfg.altLogFolderPathPhrase.empty()) { m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a folder path."))); m_logFolderPath->SetFocus(); return false; } - m_logFolderPath->getHistory()->addItem(miscCfg->altLogFolderPathPhrase); + m_logFolderPath->getHistory()->addItem(miscCfg.altLogFolderPathPhrase); - if (!miscCfg->emailNotifyAddress.empty() && - !isValidEmail(trimCpy(miscCfg->emailNotifyAddress))) + if (!miscCfg.emailNotifyAddress.empty() && + !isValidEmail(trimCpy(miscCfg.emailNotifyAddress))) { m_notebook->ChangeSelection(static_cast<size_t>(SyncConfigPanel::sync)); showNotificationDialog(this, DialogInfoType::info, PopupDialogCfg().setMainInstructions(_("Please enter a valid email address."))); @@ -1721,7 +1748,6 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) globalPairCfg_.cmpCfg = *compCfg; globalPairCfg_.syncCfg = *syncCfg; globalPairCfg_.filter = filterCfg; - globalPairCfg_.miscCfg = *miscCfg; } else { @@ -1729,6 +1755,7 @@ bool ConfigDialog::unselectFolderPairConfig(bool validateParams) localPairCfg_[selectedPairIndexToShow_].localSyncCfg = syncCfg; localPairCfg_[selectedPairIndexToShow_].localFilter = filterCfg; } + globalPairCfg_.miscCfg = miscCfg; selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED; //m_listBoxFolderPair->SetSelection(wxNOT_FOUND); not needed, selectedPairIndexToShow has parameter ownership diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp index b6b2db66..6f32a746 100644 --- a/FreeFileSync/Source/ui/tree_grid.cpp +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -31,6 +31,16 @@ const int TREE_GRID_GAP_SIZE_DIP = 2; inline wxColor getColorPercentBorder () { return {198, 198, 198}; } inline wxColor getColorPercentBackground() { return {0xf8, 0xf8, 0xf8}; } + + +Zstring getFolderPairName(const FolderPair& folder) +{ + if (folder.hasEquivalentItemNames()) + return folder.getItemName<SelectSide::left>(); + else + return folder.getItemName<SelectSide::left >() + Zstr(" | ") + + folder.getItemName<SelectSide::right>(); +} } @@ -62,31 +72,30 @@ void TreeView::compressNode(Container& cont) //remove single-element sub-trees - template <class Function> //(const FileSystemObject&) -> bool -void TreeView::extractVisibleSubtree(ContainerObject& hierObj, //in +void TreeView::extractVisibleSubtree(ContainerObject& conObj, //in TreeView::Container& cont, //out Function pred) { auto getBytes = [](const FilePair& file) //MSVC screws up miserably if we put this lambda into std::for_each { - ////give accumulated bytes the semantics of a sync preview! - //if (file.isActive()) - // switch (file.getSyncDir()) - // { - // case SyncDirection::left: - // return file.getFileSize<SelectSide::right>(); - // case SyncDirection::right: - // return file.getFileSize<SelectSide::left>(); - // case SyncDirection::none: - // break; - // } - +#if 0 //give accumulated bytes the semantics of a sync preview? + switch (getEffectiveSyncDir(file.getSyncOperation())) + { + //*INDENT-OFF* + case SyncDirection::none: break; + case SyncDirection::left: return file.getFileSize<SelectSide::right>(); + case SyncDirection::right: return file.getFileSize<SelectSide::left>(); + //*INDENT-ON* + } +#endif //prefer file-browser semantics over sync preview (=> always show useful numbers, even for SyncDirection::none) //discussion: https://freefilesync.org/forum/viewtopic.php?t=1595 - return std::max(file.getFileSize<SelectSide::left>(), file.getFileSize<SelectSide::right>()); + return std::max(file.isEmpty<SelectSide::left >() ? 0 : file.getFileSize<SelectSide::left>(), + file.isEmpty<SelectSide::right>() ? 0 : file.getFileSize<SelectSide::right>()); }; cont.firstFileId = nullptr; - for (FilePair& file : hierObj.refSubFiles()) + for (FilePair& file : conObj.refSubFiles()) if (pred(file)) { cont.bytesNet += getBytes(file); @@ -96,7 +105,7 @@ void TreeView::extractVisibleSubtree(ContainerObject& hierObj, //in cont.firstFileId = file.getId(); } - for (SymlinkPair& symlink : hierObj.refSubLinks()) + for (SymlinkPair& symlink : conObj.refSubLinks()) if (pred(symlink)) { ++cont.itemCountNet; @@ -108,9 +117,9 @@ void TreeView::extractVisibleSubtree(ContainerObject& hierObj, //in cont.bytesGross += cont.bytesNet; cont.itemCountGross += cont.itemCountNet; - cont.subDirs.reserve(hierObj.refSubFolders().size()); //avoid expensive reallocations! + cont.subDirs.reserve(conObj.refSubFolders().size()); //avoid expensive reallocations! - for (FolderPair& folder : hierObj.refSubFolders()) + for (FolderPair& folder : conObj.refSubFolders()) { const bool included = pred(folder); @@ -195,18 +204,17 @@ struct TreeView::LessShortName return makeSortDirection(LessNaturalSort() /*even on Linux*/, std::bool_constant<ascending>())(utfTo<Zstring>(static_cast<const RootNodeImpl*>(lhs.node)->displayName), utfTo<Zstring>(static_cast<const RootNodeImpl*>(rhs.node)->displayName)); - case NodeType::folder: { const auto* folderL = dynamic_cast<const FolderPair*>(FileSystemObject::retrieve(static_cast<const DirNodeImpl*>(lhs.node)->objId)); const auto* folderR = dynamic_cast<const FolderPair*>(FileSystemObject::retrieve(static_cast<const DirNodeImpl*>(rhs.node)->objId)); - if (!folderL) //might be pathologic, but it's covered + if (!folderL) //might be pathologic, but it's covered return false; else if (!folderR) return true; - return makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(folderL->getItemNameAny(), folderR->getItemNameAny()); + return makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(getFolderPairName(*folderL), getFolderPairName(*folderR)); } case NodeType::files: @@ -321,8 +329,8 @@ void TreeView::applySubView(std::vector<RootNodeImpl>&& newView) auto it = flatTree_.begin(); for (auto itNext = flatTree_.begin() + 1; itNext != flatTree_.end(); ++itNext, ++it) if (it->level < itNext->level) - if (auto hierObj = getHierAlias(*it)) - expandedNodes.insert(hierObj); + if (auto conObj = getHierAlias(*it)) + expandedNodes.insert(conObj); } //update view on full data @@ -363,8 +371,8 @@ void TreeView::applySubView(std::vector<RootNodeImpl>&& newView) { const TreeLine& line = flatTree_[row]; - if (auto hierObj = getHierAlias(line)) - if (expandedNodes.contains(hierObj)) + if (auto conObj = getHierAlias(line)) + if (expandedNodes.contains(conObj)) { std::vector<TreeLine> newLines; getChildren(*line.node, line.level + 1, newLines); @@ -525,9 +533,9 @@ void TreeView::applyDifferenceFilter(bool showExcluded, switch (fsObj.getCategory()) { - case FILE_LEFT_SIDE_ONLY: + case FILE_LEFT_ONLY: return leftOnlyFilesActive; - case FILE_RIGHT_SIDE_ONLY: + case FILE_RIGHT_ONLY: return rightOnlyFilesActive; case FILE_LEFT_NEWER: return leftNewerFilesActive; @@ -536,9 +544,10 @@ void TreeView::applyDifferenceFilter(bool showExcluded, case FILE_DIFFERENT_CONTENT: return differentFilesActive; case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal return equalFilesActive; + case FILE_RENAMED: case FILE_CONFLICT: + case FILE_TIME_INVALID: return conflictFilesActive; } assert(false); @@ -574,21 +583,21 @@ void TreeView::applyActionFilter(bool showExcluded, switch (fsObj.getSyncOperation()) { - case SO_CREATE_NEW_LEFT: + case SO_CREATE_LEFT: return syncCreateLeftActive; - case SO_CREATE_NEW_RIGHT: + case SO_CREATE_RIGHT: return syncCreateRightActive; case SO_DELETE_LEFT: return syncDeleteLeftActive; case SO_DELETE_RIGHT: return syncDeleteRightActive; case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: + case SO_RENAME_RIGHT: case SO_MOVE_RIGHT_FROM: case SO_MOVE_RIGHT_TO: return syncDirOverwRightActive; case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: + case SO_RENAME_LEFT: case SO_MOVE_LEFT_FROM: case SO_MOVE_LEFT_TO: return syncDirOverwLeftActive; @@ -751,7 +760,7 @@ private: if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) return root->displayName; else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get())) - return utfTo<std::wstring>(dir->folder.getItemNameAny()); + return utfTo<std::wstring>(getFolderPairName(dir->folder)); else if (dynamic_cast<const TreeView::FilesNode*>(node.get())) return _("Files"); break; diff --git a/FreeFileSync/Source/ui/tree_grid.h b/FreeFileSync/Source/ui/tree_grid.h index 472739d7..fb721f68 100644 --- a/FreeFileSync/Source/ui/tree_grid.h +++ b/FreeFileSync/Source/ui/tree_grid.h @@ -153,7 +153,7 @@ private: static void compressNode(Container& cont); template <class Function> - static void extractVisibleSubtree(ContainerObject& hierObj, Container& cont, Function includeObject); + static void extractVisibleSubtree(ContainerObject& conObj, Container& cont, Function includeObject); void getChildren(const Container& cont, unsigned int level, std::vector<TreeLine>& output); template <class Predicate> void updateView(Predicate pred); void applySubView(std::vector<RootNodeImpl>&& newView); diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp index ca49d087..de71621d 100644 --- a/FreeFileSync/Source/ui/version_check.cpp +++ b/FreeFileSync/Source/ui/version_check.cpp @@ -5,22 +5,22 @@ // ***************************************************************************** #include "version_check.h" -#include <ctime> +//#include <ctime> #include <zen/crc.h> -#include <zen/string_tools.h> -#include <zen/i18n.h> -#include <zen/utf.h> -#include <zen/scope_guard.h> +//#include <zen/string_tools.h> +//#include <zen/i18n.h> +//#include <zen/utf.h> +//#include <zen/scope_guard.h> #include <zen/build_info.h> -#include <zen/basic_math.h> +//#include <zen/basic_math.h> #include <zen/file_io.h> -#include <zen/file_error.h> +//#include <zen/file_error.h> #include <zen/http.h> #include <zen/process_exec.h> #include <zen/sys_version.h> #include <zen/sys_info.h> -#include <zen/thread.h> -#include <wx+/popup_dlg.h> +//#include <zen/thread.h> +//#include <wx+/popup_dlg.h> #include <wx+/image_resources.h> #include "../ffs_paths.h" #include "../version/version.h" @@ -39,29 +39,6 @@ namespace const Zchar ffsUpdateCheckUserAgent[] = Zstr("FFS-Update-Check"); -time_t getVersionCheckInactiveId() -{ - //use current version to calculate a changing number for the inactive state near UTC begin, in order to always check for updates after installing a new version - //=> interpret version as 11-based *unique* number (this breaks lexicographical version ordering, but that's irrelevant!) - int id = 0; - const char* first = ffsVersion; - const char* last = first + zen::strLength(ffsVersion); - std::for_each(first, last, [&](char c) - { - id *= 11; - if ('0' <= c && c <= '9') - id += c - '0'; - else - { - assert(c == FFS_VERSION_SEPARATOR); - id += 10; - } - }); - assert(0 < id && id < 3600 * 24 * 365); //as long as value is within a year after UTC begin (1970) there's no risk to clash with *current* time - return id; -} - - time_t getVersionCheckCurrentTime() @@ -73,21 +50,20 @@ time_t getVersionCheckCurrentTime() void openBrowserForDownload(wxWindow* parent) { - wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); + wxLaunchDefaultBrowser(L"https://freefilesync.org/get_latest.php"); } } -bool fff::shouldRunAutomaticUpdateCheck(time_t lastUpdateCheck) +bool fff::automaticUpdateCheckDue(time_t lastUpdateCheck) { - if (lastUpdateCheck == getVersionCheckInactiveId()) - return false; - const time_t now = std::time(nullptr); return std::abs(now - lastUpdateCheck) >= 7 * 24 * 3600; //check weekly } +namespace +{ std::wstring getIso639Language() { assert(runningOnMainThread()); //this function is not thread-safe: consider wxWidgets usage @@ -106,8 +82,6 @@ std::wstring getIso639Language() } -namespace -{ std::wstring getIso3166Country() { assert(runningOnMainThread()); //this function is not thread-safe, consider wxWidgets usage @@ -212,6 +186,12 @@ std::string getOnlineVersion(const std::vector<std::pair<std::string, std::strin return response; } + + +std::string getUnknownVersionTag() +{ + return '<' + utfTo<std::string>(_("version unknown")) + '>'; +} } @@ -235,18 +215,6 @@ bool fff::haveNewerVersionOnline(const std::string& onlineVersion) } -bool fff::updateCheckActive(time_t lastUpdateCheck) -{ - return lastUpdateCheck != getVersionCheckInactiveId(); -} - - -void fff::disableUpdateCheck(time_t& lastUpdateCheck) -{ - lastUpdateCheck = getVersionCheckInactiveId(); -} - - void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) { try @@ -266,7 +234,7 @@ void fff::checkForUpdateNow(wxWindow& parent, std::string& lastOnlineVersion) { if (internetIsAlive()) { - lastOnlineVersion = "Unknown"; + lastOnlineVersion = getUnknownVersionTag(); switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). setTitle(_("Check for Program Updates")). @@ -323,8 +291,8 @@ SharedRef<const UpdateCheckResultPrep> fff::automaticUpdateCheckPrepare(wxWindow struct fff::UpdateCheckResult { std::string onlineVersion; - bool internetIsAlive = false; std::optional<SysError> error; + bool internetIsAlive = false; }; SharedRef<const UpdateCheckResult> fff::automaticUpdateCheckRunAsync(const UpdateCheckResultPrep& resultPrep) { @@ -353,37 +321,46 @@ void fff::automaticUpdateCheckEval(wxWindow& parent, time_t& lastUpdateCheck, st if (!result.error) { - lastUpdateCheck = getVersionCheckCurrentTime(); - lastOnlineVersion = result.onlineVersion; + lastUpdateCheck = getVersionCheckCurrentTime(); + + if (lastOnlineVersion != result.onlineVersion) //show new version popup only *once* + { + lastOnlineVersion = result.onlineVersion; - if (haveNewerVersionOnline(result.onlineVersion)) - showUpdateAvailableDialog(&parent, result.onlineVersion); + if (haveNewerVersionOnline(result.onlineVersion)) //beta or development version is newer than online + showUpdateAvailableDialog(&parent, result.onlineVersion); + } } else { if (result.internetIsAlive) { - lastOnlineVersion = "Unknown"; - - switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). - setTitle(_("Check for Program Updates")). - setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). - setDetailInstructions(result.error->toString()), - _("&Check"), _("&Retry"))) + if (lastOnlineVersion != getUnknownVersionTag()) { - case ConfirmationButton2::accept: - openBrowserForDownload(&parent); - break; - case ConfirmationButton2::accept2: //retry - automaticUpdateCheckEval(parent, lastUpdateCheck, lastOnlineVersion, - automaticUpdateCheckRunAsync(automaticUpdateCheckPrepare(parent).ref()).ref()); //note: retry via recursion!!! - break; - case ConfirmationButton2::cancel: - break; + lastOnlineVersion = getUnknownVersionTag(); + + switch (showConfirmationDialog(&parent, DialogInfoType::error, PopupDialogCfg(). + setTitle(_("Check for Program Updates")). + setMainInstructions(_("Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now?")). + setDetailInstructions(result.error->toString()), + _("&Check"), _("&Retry"))) + { + case ConfirmationButton2::accept: + openBrowserForDownload(&parent); + break; + case ConfirmationButton2::accept2: //retry + automaticUpdateCheckEval(parent, lastUpdateCheck, lastOnlineVersion, + automaticUpdateCheckRunAsync(automaticUpdateCheckPrepare(parent).ref()).ref()); //retry via recursion!!! + break; + case ConfirmationButton2::cancel: + break; + } } } - //else: ignore this error + else //no internet connection + { + if (lastOnlineVersion.empty()) + lastOnlineVersion = getUnknownVersionTag(); + } } } - - diff --git a/FreeFileSync/Source/ui/version_check.h b/FreeFileSync/Source/ui/version_check.h index 1a88997b..e7b75f13 100644 --- a/FreeFileSync/Source/ui/version_check.h +++ b/FreeFileSync/Source/ui/version_check.h @@ -7,20 +7,15 @@ #ifndef VERSION_CHECK_H_324872374893274983275 #define VERSION_CHECK_H_324872374893274983275 -//#include <functional> -//#include <memory> #include <wx/window.h> #include <zen/stl_tools.h> namespace fff { -bool updateCheckActive (time_t lastUpdateCheck); -void disableUpdateCheck(time_t& lastUpdateCheck); bool haveNewerVersionOnline(const std::string& onlineVersion); //---------------------------------------------------------------------------- -//periodic update check: -bool shouldRunAutomaticUpdateCheck(time_t lastUpdateCheck); +bool automaticUpdateCheckDue(time_t lastUpdateCheck); struct UpdateCheckResultPrep; struct UpdateCheckResult; diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 3b1d7c53..cb647a02 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "12.5"; //internal linkage! +const char ffsVersion[] = "13.0"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/wx+/grid.cpp b/wx+/grid.cpp index 68f40fb6..8e9fe592 100644 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -171,7 +171,7 @@ void GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& te if (extentTrunc.GetWidth() > rect.width) { //unlike Windows Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideograph encodes to TWO wchar_t: utfTo<std::wstring>("\xf0\xa4\xbd\x9c"); - size_t low = 0; //number of unicode chars! + size_t low = 0; //number of Unicode chars! size_t high = unicodeLength(text); // if (high > 1) for (;;) @@ -1991,6 +1991,9 @@ void Grid::setRowHeight(int height) } +int Grid::getRowHeight() const { return rowLabelWin_->getRowHeight(); } + + void Grid::setColumnConfig(const std::vector<Grid::ColAttributes>& attr) { //hold ownership of non-visible columns @@ -151,6 +151,7 @@ public: size_t getRowCount() const; void setRowHeight(int height); + int getRowHeight() const; struct ColAttributes { diff --git a/wx+/image_tools.cpp b/wx+/image_tools.cpp index 04f7f7ff..7b6d943b 100644 --- a/wx+/image_tools.cpp +++ b/wx+/image_tools.cpp @@ -170,7 +170,7 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const { wxMemoryDC dc; //the context used for bitmaps setScaleFactor(dc, getDisplayScaleFactor()); - dc.SetFont(font); //the font parameter of GetMultiLineTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + dc.SetFont(font); //the font parameter of GetTextExtent() is not evaluated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! std::vector<std::pair<wxString, wxSize>> lineInfo; //text + extent for (const wxString& line : splitCpy(text, L'\n', SplitOnEmpty::allow)) diff --git a/wx+/popup_dlg_generated.cpp b/wx+/popup_dlg_generated.cpp index b7e3d94d..aa5eb234 100644 --- a/wx+/popup_dlg_generated.cpp +++ b/wx+/popup_dlg_generated.cpp @@ -11,88 +11,88 @@ PopupDialogGenerated::PopupDialogGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) { - this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); - this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + this->SetSizeHints( wxSize( -1,-1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); - wxBoxSizer* bSizer24; - bSizer24 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); - m_panel33 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); - m_panel33->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + m_panel33 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel33->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - wxBoxSizer* bSizer165; - bSizer165 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer165; + bSizer165 = new wxBoxSizer( wxHORIZONTAL ); - m_bitmapMsgType = new wxStaticBitmap( m_panel33, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizer165->Add( m_bitmapMsgType, 0, wxALL, 10 ); + m_bitmapMsgType = new wxStaticBitmap( m_panel33, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1,-1 ), 0 ); + bSizer165->Add( m_bitmapMsgType, 0, wxALL, 10 ); - wxBoxSizer* bSizer16; - bSizer16 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer16; + bSizer16 = new wxBoxSizer( wxVERTICAL ); - bSizer16->Add( 0, 10, 0, 0, 5 ); + bSizer16->Add( 0, 10, 0, 0, 5 ); - m_staticTextMain = new wxStaticText( m_panel33, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticTextMain->Wrap( -1 ); - bSizer16->Add( m_staticTextMain, 0, wxBOTTOM|wxRIGHT, 10 ); + m_staticTextMain = new wxStaticText( m_panel33, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( -1 ); + bSizer16->Add( m_staticTextMain, 0, wxBOTTOM|wxRIGHT, 10 ); - m_richTextDetail = new wxRichTextCtrl( m_panel33, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxBORDER_NONE|wxVSCROLL|wxWANTS_CHARS ); - bSizer16->Add( m_richTextDetail, 1, wxEXPAND, 5 ); + m_richTextDetail = new wxRichTextCtrl( m_panel33, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_READONLY|wxBORDER_NONE|wxVSCROLL|wxWANTS_CHARS ); + bSizer16->Add( m_richTextDetail, 1, wxEXPAND, 5 ); - bSizer165->Add( bSizer16, 1, wxEXPAND, 5 ); + bSizer165->Add( bSizer16, 1, wxEXPAND, 5 ); - m_panel33->SetSizer( bSizer165 ); - m_panel33->Layout(); - bSizer165->Fit( m_panel33 ); - bSizer24->Add( m_panel33, 1, wxEXPAND, 5 ); + m_panel33->SetSizer( bSizer165 ); + m_panel33->Layout(); + bSizer165->Fit( m_panel33 ); + bSizer24->Add( m_panel33, 1, wxEXPAND, 5 ); - m_staticline6 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer24->Add( m_staticline6, 0, wxEXPAND, 5 ); + m_staticline6 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline6, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer25; - bSizer25 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer25; + bSizer25 = new wxBoxSizer( wxVERTICAL ); - m_checkBoxCustom = new wxCheckBox( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - bSizer25->Add( m_checkBoxCustom, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + m_checkBoxCustom = new wxCheckBox( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer25->Add( m_checkBoxCustom, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); - bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); - m_buttonAccept = new wxButton( this, wxID_YES, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonAccept = new wxButton( this, wxID_YES, _("dummy"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); - m_buttonAccept->SetDefault(); - bSizerStdButtons->Add( m_buttonAccept, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + m_buttonAccept->SetDefault(); + bSizerStdButtons->Add( m_buttonAccept, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - m_buttonAccept2 = new wxButton( this, wxID_YESTOALL, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonAccept2, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + m_buttonAccept2 = new wxButton( this, wxID_YESTOALL, _("dummy"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); + bSizerStdButtons->Add( m_buttonAccept2, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); - m_buttonDecline = new wxButton( this, wxID_NO, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonDecline, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + m_buttonDecline = new wxButton( this, wxID_NO, _("dummy"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); + bSizerStdButtons->Add( m_buttonDecline, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); - m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1,-1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); - bSizer25->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + bSizer25->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); - bSizer24->Add( bSizer25, 0, wxEXPAND, 5 ); + bSizer24->Add( bSizer25, 0, wxEXPAND, 5 ); - this->SetSizer( bSizer24 ); - this->Layout(); - bSizer24->Fit( this ); + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); - this->Centre( wxBOTH ); + this->Centre( wxBOTH ); - // Connect Events - this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PopupDialogGenerated::onClose ) ); - m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCheckBoxClick ), NULL, this ); - m_buttonAccept->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept ), NULL, this ); - m_buttonAccept2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept2 ), NULL, this ); - m_buttonDecline->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonDecline ), NULL, this ); - m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCancel ), NULL, this ); + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PopupDialogGenerated::onClose ) ); + m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCheckBoxClick ), NULL, this ); + m_buttonAccept->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept ), NULL, this ); + m_buttonAccept2->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonAccept2 ), NULL, this ); + m_buttonDecline->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onButtonDecline ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::onCancel ), NULL, this ); } PopupDialogGenerated::~PopupDialogGenerated() @@ -101,22 +101,22 @@ PopupDialogGenerated::~PopupDialogGenerated() TooltipDlgGenerated::TooltipDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) { - this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); - wxBoxSizer* bSizer158; - bSizer158 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer158; + bSizer158 = new wxBoxSizer( wxHORIZONTAL ); - m_bitmapLeft = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer158->Add( m_bitmapLeft, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + m_bitmapLeft = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer158->Add( m_bitmapLeft, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); - m_staticTextMain = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticTextMain->Wrap( 600 ); - bSizer158->Add( m_staticTextMain, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + m_staticTextMain = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( 600 ); + bSizer158->Add( m_staticTextMain, 0, wxALL|wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - this->SetSizer( bSizer158 ); - this->Layout(); - bSizer158->Fit( this ); + this->SetSizer( bSizer158 ); + this->Layout(); + bSizer158->Fit( this ); } TooltipDlgGenerated::~TooltipDlgGenerated() diff --git a/wx+/popup_dlg_generated.h b/wx+/popup_dlg_generated.h index ce4e2ac8..65c8c11b 100644 --- a/wx+/popup_dlg_generated.h +++ b/wx+/popup_dlg_generated.h @@ -38,35 +38,35 @@ /////////////////////////////////////////////////////////////////////////////// class PopupDialogGenerated : public wxDialog { -private: + private: -protected: - wxPanel* m_panel33; - wxStaticBitmap* m_bitmapMsgType; - wxStaticText* m_staticTextMain; - wxRichTextCtrl* m_richTextDetail; - wxStaticLine* m_staticline6; - wxCheckBox* m_checkBoxCustom; - wxBoxSizer* bSizerStdButtons; - wxButton* m_buttonAccept; - wxButton* m_buttonAccept2; - wxButton* m_buttonDecline; - wxButton* m_buttonCancel; + protected: + wxPanel* m_panel33; + wxStaticBitmap* m_bitmapMsgType; + wxStaticText* m_staticTextMain; + wxRichTextCtrl* m_richTextDetail; + wxStaticLine* m_staticline6; + wxCheckBox* m_checkBoxCustom; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonAccept; + wxButton* m_buttonAccept2; + wxButton* m_buttonDecline; + wxButton* m_buttonCancel; - // Virtual event handlers, override them in your derived class - virtual void onClose( wxCloseEvent& event ) { event.Skip(); } - virtual void onCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } - virtual void onButtonAccept( wxCommandEvent& event ) { event.Skip(); } - virtual void onButtonAccept2( wxCommandEvent& event ) { event.Skip(); } - virtual void onButtonDecline( wxCommandEvent& event ) { event.Skip(); } - virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } + // Virtual event handlers, override them in your derived class + virtual void onClose( wxCloseEvent& event ) { event.Skip(); } + virtual void onCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonAccept( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonAccept2( wxCommandEvent& event ) { event.Skip(); } + virtual void onButtonDecline( wxCommandEvent& event ) { event.Skip(); } + virtual void onCancel( wxCommandEvent& event ) { event.Skip(); } -public: + public: - PopupDialogGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + PopupDialogGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); - ~PopupDialogGenerated(); + ~PopupDialogGenerated(); }; @@ -75,17 +75,17 @@ public: /////////////////////////////////////////////////////////////////////////////// class TooltipDlgGenerated : public wxDialog { -private: + private: -protected: + protected: -public: - wxStaticBitmap* m_bitmapLeft; - wxStaticText* m_staticTextMain; + public: + wxStaticBitmap* m_bitmapLeft; + wxStaticText* m_staticTextMain; - TooltipDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + TooltipDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); - ~TooltipDlgGenerated(); + ~TooltipDlgGenerated(); }; diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp index 19b6ede7..79d4fa1a 100644 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -4,6 +4,7 @@ // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "tooltip.h" +#include <zen/zstring.h> #include <wx/dialog.h> #include <wx/stattext.h> #include <wx/sizer.h> @@ -62,7 +63,7 @@ void Tooltip::show(const wxString& text, wxPoint mousePos, const wxImage* img) const wxImage& newImg = img ? *img : wxNullImage; const bool imgChanged = !newImg.IsSameAs(lastUsedImg_); - const bool txtChanged = text != tipWindow_->staticTextMain_->GetLabelText(); + const bool txtChanged = text != lastUsedText_; if (imgChanged) { @@ -73,7 +74,8 @@ void Tooltip::show(const wxString& text, wxPoint mousePos, const wxImage* img) if (txtChanged) { - tipWindow_->staticTextMain_->SetLabelText(text); + lastUsedText_ = text; + tipWindow_->staticTextMain_->SetLabelText(text); tipWindow_->staticTextMain_->Wrap(fastFromDIP(600)); } diff --git a/wx+/tooltip.h b/wx+/tooltip.h index b496f36c..e20cb3da 100644 --- a/wx+/tooltip.h +++ b/wx+/tooltip.h @@ -28,6 +28,7 @@ private: TooltipDlgGenerated* tipWindow_ = nullptr; wxWindow& parent_; wxImage lastUsedImg_; + wxString lastUsedText_; //needed, considering "SetLabelText(textFixed)" }; } diff --git a/zen/file_access.cpp b/zen/file_access.cpp index 4ce66acf..af19df87 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -273,6 +273,18 @@ void zen::removeDirectoryPlainRecursion(const Zstring& dirPath) //throw FileErro namespace { +std::wstring generateMoveErrorMsg(const Zstring& pathFrom, const Zstring& pathTo) +{ + if (getParentFolderPath(pathFrom) == getParentFolderPath(pathTo)) //pure "rename" + return replaceCpy(replaceCpy(_("Cannot rename %x to %y."), + L"%x", fmtPath(pathFrom)), + L"%y", fmtPath(getItemName(pathTo))); + else //"move" or "move + rename" + return trimCpy(replaceCpy(replaceCpy(_("Cannot move %x to %y."), + L"%x", L'\n' + fmtPath(pathFrom)), + L"%y", L'\n' + fmtPath(pathTo))); +} + /* Usage overview: (avoid circular pattern!) moveAndRenameItem() --> moveAndRenameFileSub() @@ -283,7 +295,7 @@ namespace //wrapper for file system rename function: void moveAndRenameFileSub(const Zstring& pathFrom, const Zstring& pathTo, bool replaceExisting) //throw FileError, ErrorMoveUnsupported, ErrorTargetExisting { - auto getErrorMsg = [&] { return replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L'\n' + fmtPath(pathFrom)), L"%y", L'\n' + fmtPath(pathTo)); }; + auto getErrorMsg = [&] { return generateMoveErrorMsg(pathFrom, pathTo); }; //rename() will never fail with EEXIST, but always (atomically) overwrite! //=> equivalent to SetFileInformationByHandle() + FILE_RENAME_INFO::ReplaceIfExists or ::MoveFileEx() + MOVEFILE_REPLACE_EXISTING @@ -338,6 +350,8 @@ void zen::moveAndRenameItem(const Zstring& pathFrom, const Zstring& pathTo, bool } + + namespace { void setWriteTimeNative(const Zstring& itemPath, const timespec& modTime, ProcSymlink procSl) //throw FileError @@ -714,10 +728,7 @@ FileCopyResult zen::copyNewFile(const Zstring& sourceFile, const Zstring& target macOS: https://freefilesync.org/forum/viewtopic.php?t=356 */ setWriteTimeNative(targetFile, sourceInfo.st_mtim, ProcSymlink::follow); //throw FileError } - catch (const FileError& e) - { - errorModTime = FileError(e.toString()); //avoid slicing - } + catch (const FileError& e) { errorModTime = e; /*might slice derived class?*/ } return { diff --git a/zen/http.cpp b/zen/http.cpp index 8cdcd65c..fcf69fb3 100644 --- a/zen/http.cpp +++ b/zen/http.cpp @@ -265,7 +265,17 @@ std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const Zstring& url, else { if (httpStatus != 200) //HTTP_STATUS_OK - throw SysError(formatHttpError(httpStatus)); //e.g. "HTTP status 404: Not found." + { +#if 0 //beneficial to add error details? + std::wstring errorDetails; + try + { + errorDetails = utfTo<std::wstring>(HttpInputStream(std::move(response)).readAll(nullptr /*notifyUnbufferedIO*/)); //throw SysError + } + catch (const SysError& e) { errorDetails = e.toString(); } +#endif + throw SysError(formatHttpError(httpStatus) /*+ L' ' + errorDetails*/); //e.g. "HTTP status 404: Not found." + } return response; } diff --git a/zen/zstring.h b/zen/zstring.h index 5bcc8b34..bf7ac526 100644 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -84,9 +84,11 @@ 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 NOBREAK_SPACE = L'\u00A0'; +const wchar_t NOBREAK_SPACE = L'\u00A0'; const wchar_t ZERO_WIDTH_SPACE = L'\u200B'; +const wchar_t EN_SPACE = L'\u2002'; + const wchar_t LTR_MARK = L'\u200E'; //UTF-8: E2 80 8E const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F https://www.w3.org/International/questions/qa-bidi-unicode-controls //const wchar_t BIDI_DIR_ISOLATE_RTL = L'\u2067'; //=> not working on Win 10 @@ -94,6 +96,8 @@ 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* const TAB_SPACE = L" "; //4: the only sensible space count for tabs #endif //ZSTRING_H_73425873425789 |