From 48c8efc58c9eb41da96b053806deb395d2e66443 Mon Sep 17 00:00:00 2001 From: Daniel Wilhelm Date: Wed, 9 May 2018 00:07:47 +0200 Subject: 9.7 --- Changelog.txt | 21 +- FreeFileSync/Build/Help/FreeFileSync.hhc | 2 +- FreeFileSync/Build/Help/html/command-line.html | 5 +- FreeFileSync/Build/Help/html/tips-and-tricks.html | 38 +- .../Build/Help/images/command-line-syntax.png | Bin 5944 -> 11836 bytes FreeFileSync/Build/Languages/german.lng | 66 +- FreeFileSync/Build/Languages/slovenian.lng | 512 +++--- FreeFileSync/Build/Languages/turkish.lng | 82 +- FreeFileSync/Build/Resources.zip | Bin 315798 -> 317363 bytes FreeFileSync/Source/Makefile | 7 +- FreeFileSync/Source/RealTimeSync/main_dlg.cpp | 2 +- FreeFileSync/Source/application.cpp | 137 +- FreeFileSync/Source/comparison.cpp | 16 +- FreeFileSync/Source/file_hierarchy.h | 6 +- FreeFileSync/Source/lib/lock_holder.h | 43 +- FreeFileSync/Source/lib/process_xml.cpp | 307 +++- FreeFileSync/Source/lib/process_xml.h | 55 +- FreeFileSync/Source/lib/resolve_path.cpp | 45 +- FreeFileSync/Source/ui/batch_config.cpp | 11 + FreeFileSync/Source/ui/batch_status_handler.cpp | 14 +- FreeFileSync/Source/ui/cfg_grid.cpp | 387 ++++ FreeFileSync/Source/ui/cfg_grid.h | 122 ++ FreeFileSync/Source/ui/column_attr.h | 108 -- FreeFileSync/Source/ui/custom_grid.cpp | 1842 -------------------- FreeFileSync/Source/ui/custom_grid.h | 84 - FreeFileSync/Source/ui/file_grid.cpp | 1817 +++++++++++++++++++ FreeFileSync/Source/ui/file_grid.h | 83 + FreeFileSync/Source/ui/file_grid_attr.h | 91 + FreeFileSync/Source/ui/file_view.cpp | 534 ++++++ FreeFileSync/Source/ui/file_view.h | 203 +++ FreeFileSync/Source/ui/folder_selector.cpp | 8 +- FreeFileSync/Source/ui/folder_selector.h | 2 +- FreeFileSync/Source/ui/grid_view.cpp | 549 ------ FreeFileSync/Source/ui/grid_view.h | 209 --- FreeFileSync/Source/ui/gui_generated.cpp | 124 +- FreeFileSync/Source/ui/gui_generated.h | 41 +- FreeFileSync/Source/ui/gui_status_handler.cpp | 14 +- FreeFileSync/Source/ui/main_dlg.cpp | 934 +++++----- FreeFileSync/Source/ui/main_dlg.h | 42 +- FreeFileSync/Source/ui/progress_indicator.cpp | 283 +-- FreeFileSync/Source/ui/search.cpp | 12 +- FreeFileSync/Source/ui/small_dlgs.cpp | 120 +- FreeFileSync/Source/ui/small_dlgs.h | 3 + FreeFileSync/Source/ui/sync_cfg.cpp | 54 +- FreeFileSync/Source/ui/tree_grid.cpp | 1281 ++++++++++++++ FreeFileSync/Source/ui/tree_grid.h | 185 ++ FreeFileSync/Source/ui/tree_grid_attr.h | 63 + FreeFileSync/Source/ui/tree_view.cpp | 1338 -------------- FreeFileSync/Source/ui/tree_view.h | 191 -- FreeFileSync/Source/version/version.h | 2 +- wx+/file_drop.cpp | 1 + wx+/file_drop.h | 2 + wx+/focus.h | 66 + wx+/grid.cpp | 255 +-- wx+/grid.h | 151 +- wx+/http.cpp | 21 +- zen/file_io.cpp | 10 +- zen/zstring.h | 1 + zenXml/zenxml/cvrt_struc.h | 1 + 59 files changed, 6827 insertions(+), 5776 deletions(-) create mode 100755 FreeFileSync/Source/ui/cfg_grid.cpp create mode 100755 FreeFileSync/Source/ui/cfg_grid.h delete mode 100755 FreeFileSync/Source/ui/column_attr.h delete mode 100755 FreeFileSync/Source/ui/custom_grid.cpp delete mode 100755 FreeFileSync/Source/ui/custom_grid.h create mode 100755 FreeFileSync/Source/ui/file_grid.cpp create mode 100755 FreeFileSync/Source/ui/file_grid.h create mode 100755 FreeFileSync/Source/ui/file_grid_attr.h create mode 100755 FreeFileSync/Source/ui/file_view.cpp create mode 100755 FreeFileSync/Source/ui/file_view.h delete mode 100755 FreeFileSync/Source/ui/grid_view.cpp delete mode 100755 FreeFileSync/Source/ui/grid_view.h create mode 100755 FreeFileSync/Source/ui/tree_grid.cpp create mode 100755 FreeFileSync/Source/ui/tree_grid.h create mode 100755 FreeFileSync/Source/ui/tree_grid_attr.h delete mode 100755 FreeFileSync/Source/ui/tree_view.cpp delete mode 100755 FreeFileSync/Source/ui/tree_view.h create mode 100755 wx+/focus.h diff --git a/Changelog.txt b/Changelog.txt index acf06a4d..60be1b62 100755 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,5 +1,22 @@ -FreeFileSync 9.6 ----------------- +FreeFileSync 9.7 [2018-01-12] +----------------------------- +New configuration management panel +New column showing days since last sync +Support starting FreeFileSync via Windows Send To +Minimized memory operations for I/O buffer +Allow multiple config selections on Linux +New command line option -DirPair +Fixed ENTER key not working for most dialogs (macOS) +Show only one warning about failed directory locks +Show correct synchronization time when resuming from system sleep +Don't resolve symlinks that are dropped via mouse +Detect and notify LCMapString compatibility mode bug +Fixed incorrect file permissions within macOS bundle +Fixed wrong results dialog panel selection (Linux) + + +FreeFileSync 9.6 [2017-12-07] +----------------------------- New installation command line option /disable_updates Fixed crash when closing main dialog during sync Fixed RealTimeSync crash after recursive mutex locking diff --git a/FreeFileSync/Build/Help/FreeFileSync.hhc b/FreeFileSync/Build/Help/FreeFileSync.hhc index 194d8951..1b2ba311 100755 --- a/FreeFileSync/Build/Help/FreeFileSync.hhc +++ b/FreeFileSync/Build/Help/FreeFileSync.hhc @@ -20,7 +20,7 @@
  • - +
  • diff --git a/FreeFileSync/Build/Help/html/command-line.html b/FreeFileSync/Build/Help/html/command-line.html index 3bde31e8..37ebf902 100755 --- a/FreeFileSync/Build/Help/html/command-line.html +++ b/FreeFileSync/Build/Help/html/command-line.html @@ -84,12 +84,11 @@

    3. Customize an existing configuration

    - You can replace the directories of a given ffs_gui or ffs_batch configuration file by using the -LeftDir - and -RightDir parameters: + You can replace the directories of a given ffs_gui or ffs_batch configuration file by using the -DirPair parameter:

    -
    FreeFileSync.exe "D:\Manual Backup.ffs_gui" -leftdir C:\NewSource -rightdir D:\NewTarget
    +
    FreeFileSync.exe "D:\Manual Backup.ffs_gui" -dirpair C:\NewSource D:\NewTarget

    diff --git a/FreeFileSync/Build/Help/html/tips-and-tricks.html b/FreeFileSync/Build/Help/html/tips-and-tricks.html index 06b69e33..838ae087 100755 --- a/FreeFileSync/Build/Help/html/tips-and-tricks.html +++ b/FreeFileSync/Build/Help/html/tips-and-tricks.html @@ -12,15 +12,15 @@
    Change settings with a single mouse click: Press and hold the right mouse button until the context menu is shown, then release while over the selection:
    - Comparison settings context menu - Filter context menu - Synchronization settings context menu
    + Comparison settings context menu + Filter context menu + Synchronization settings context menu
    Select multiple configurations at a time:
    - Select multiple configurations + Select multiple configurations Select a few items via mouse, and refine the selection by holding the Control key while clicking.
    @@ -28,86 +28,86 @@
    Start comparison directly by double-clicking on a configuration:
    - Double-click on configuration + Double-click on configuration
    Synchronize multiple folder pairs at a time with different configurations:
    - Add folder pair + Add folder pair
    Start synchronization directly without clicking on compare first:
    - Start synchronization directly + Start synchronization directly
    Move a window by clicking on a free area and holding the mouse button:
    - Move dialog via mouse + Move dialog via mouse
    Open a batch configuration for edit via the Windows Explorer context menu:
    - Explorer context menu + Explorer context menu
    Drag and drop two folders at a time from Windows Explorer to fill a folder pair in one go:
    - Two-folder drop + Two-folder drop
    Copy files selected on the main dialog to an alternate folder and thereby save a "diff":
    - Copy to alternative path + Copy to alternative path
    Use a volume name instead of a drive letter:
    - Drive letter by volume name + Drive letter by volume name
    Show thumbnail icons via the column header context menu:
    - Show thumbnail icons + Show thumbnail icons
    Save the current view filter selection as default:
    - Save view filter settings + Save view filter settings
    Remove local settings from individual folder pairs:
    - Remove local settings + Remove local settings
    Remove obsolete paths from the folder drop-down list by pressing the Delete key:
    - Remove drop-down path + Remove drop-down path
    Select a time span for files to include via the date column context menu:
    - Select time span + Select time span
    Double-click on comparison and synchronization variants to confirm the dialog:
    - Double-click comparison variant - Double-click synchronization variant + Double-click comparison variant + Double-click synchronization variant
    diff --git a/FreeFileSync/Build/Help/images/command-line-syntax.png b/FreeFileSync/Build/Help/images/command-line-syntax.png index 53f47122..f0d9878d 100755 Binary files a/FreeFileSync/Build/Help/images/command-line-syntax.png and b/FreeFileSync/Build/Help/images/command-line-syntax.png differ diff --git a/FreeFileSync/Build/Languages/german.lng b/FreeFileSync/Build/Languages/german.lng index c39b0536..acc4da40 100755 --- a/FreeFileSync/Build/Languages/german.lng +++ b/FreeFileSync/Build/Languages/german.lng @@ -64,6 +64,9 @@ Syntax error Syntaxfehler +A left and a right directory path are expected after %x. +Ein linker und rechter Verzeichnispfad werden nach %x erwartet. + Cannot find file %x. Die Datei %x wurde nicht gefunden. @@ -456,8 +459,8 @@ Tatsächlich: %y bytes Error parsing file %x, row %y, column %z. Fehler beim Auswerten der Datei %x, Zeile %y, Spalte %z. -Cannot set directory lock for %x. -Die Verzeichnissperre für %x kann nicht gesetzt werden. +Cannot set directory locks for the following folders: +Die Verzeichnissperren können für die folgenden Ordner nicht gesetzt werden: 1 thread @@ -797,6 +800,27 @@ Die Befehlszeile wird ausgelöst, wenn: Serious Error Schwerer Fehler +Last session +Letzte Sitzung + +Today +Heute + + +1 day +%x days + + +1 Tag +%x Tage + + +Name +Name + +Last sync +Letzte Ausführung + Folder Ordner @@ -1324,6 +1348,9 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Activate offline Offline aktivieren +Highlight configurations that have not been run for more than the following number of days: +Konfigurationen hervorheben, die seit mehr als die folgende Anzahl an Tagen nicht mehr ausgeführt wurden: + Save as a Batch Job Als Batchauftrag speichern @@ -1342,6 +1369,9 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. FreeFileSync Donation Edition FreeFileSync Spendenversion +Highlight Configurations +Konfigurationen hervorheben + &Options &Optionen @@ -1465,9 +1495,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Select time span... Zeitspanne auswählen... -Last session -Letzte Sitzung - Folder Comparison and Synchronization Ordnervergleich und Synchronisation @@ -1486,8 +1513,11 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Do&n't save &Nicht speichern -Remove entry from list -Eintrag aus Liste entfernen +Hide configuration +Konfiguration ausblenden + +Highlight... +Hervorheben... Clear filter Filter löschen @@ -1699,9 +1729,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Synchronization Synchronisation -Today -Heute - This week Diese Woche @@ -1771,9 +1798,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Files Dateien -Name -Name - Percentage Prozent @@ -1885,15 +1909,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. %x Stunden - -1 day -%x days - - -1 Tag -%x Tage - - Cannot set privilege %x. Das Privileg %x kann nicht gesetzt werden. @@ -1960,6 +1975,9 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Start menu Startmenü +Send To +Senden an + Registering FreeFileSync file extensions Registriere FreeFileSync Dateiendungen @@ -1987,6 +2005,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. Please choose the local installation type or select a different folder for installation. Bitte wählen Sie den lokalen Installationstyp oder einen anderen Ordner für die Installation. -The silent installation mode is only available in the FreeFileSync Donation Edition. -Der stille Installationsmodus is nur in der FreeFileSync Spendenversion verfügbar. +The %x installation option is only available in the FreeFileSync Donation Edition. +Die %x Installationsoption ist nur in der FreeFileSync Spendenversion verfügbar. diff --git a/FreeFileSync/Build/Languages/slovenian.lng b/FreeFileSync/Build/Languages/slovenian.lng index 28f43e2a..81d99194 100755 --- a/FreeFileSync/Build/Languages/slovenian.lng +++ b/FreeFileSync/Build/Languages/slovenian.lng @@ -1,6 +1,6 @@
    Slovenščina - Karlo Konc + dr.Vinko Kastelic sl_SI flag_slovenia.png 4 @@ -8,19 +8,19 @@
    Both sides have changed since last synchronization. -Obe strani sta se spremenili od zadnje sinhronizacije. +Obe strani sta spremenjeni po zadnji sinhronizaciji. Cannot determine sync-direction: Ne morem določiti sinhronizacijske smeri: No change since last synchronization. -Ni sprememb od zadnje sinhronizacije. +Ni sprememb po zadnji sinhronizaciji. The database entry is not in sync considering current settings. -Glede na trenutne nastavitve vnos v podatkovni bazi ni sinhroniziran. +Vnos podatkovne zbirke ni sinhroniziran ob upoštevanju trenutnih nastavitev. Setting default synchronization directions: Old files will be overwritten with newer files. -Nastavljanje privzetih smeri sinhronizacije: Stare datoteke bodo prepisane z novimi datotekami. +Nastavitve privzetih smeri sinhronizacije: Stare datoteke bodo prepisane z novimi datotekami. Creating file %x Ustvarjam datoteko %x @@ -53,7 +53,7 @@ Preverjam razpoložljivost koša za mapo %x... The recycle bin is not supported by the following folders. Deleted or overwritten files will not be able to be restored: - +Koš ne podpira sledečih map. Izbrisane ali prepisane datoteke ne bo mogoče obnoviti: An exception occurred Prišlo je do izjeme @@ -64,6 +64,9 @@ Syntax error Sintaktična napaka +A left and a right directory path are expected after %x. +Levo in desno pot imenika se pričakuje po %x. + Cannot find file %x. Ne najdem datoteke %x. @@ -77,10 +80,10 @@ Vnešeno je neenako število levih in desnih imenikov. The config file must not contain settings at directory pair level when directories are set via command line. -Konfiguracijska datoteka ne sme vsebovati nastavitev na ravni imeniških parov, ko so imeniki nastavljeni prek ukazne vrstice. +Konfiguracijska datoteka ne sme vsebovati nastavitev na ravni imeniških parov, ko so imeniki nastavljeni preko ukazne vrstice. Directories cannot be set for more than one configuration file. -Imeniki ne morejo biti nastavljeni za več kot eno konfiguracijsko datoteko. +Imenikov ni mogoče nastaviti za več kot eno nastavitveno datoteko. Command line Ukazna vrstica @@ -98,13 +101,13 @@ datoteka z globalnimi konfiguracijami: Any number of FreeFileSync .ffs_gui and/or .ffs_batch configuration files. -Poljubno število FreeFileSync .ffs_gui in/ali .ffs_batch konfigracijskih datotek. +Poljubno število FreeFileSync .ffs_gui in/ali .ffs_batch nastavitvenih datotek. Any number of alternative directory pairs for at most one config file. Poljubno število alternativnih parov imenikov za največ eno nastavitveno datoteko. Open the selected configuration for editing only without executing it. - +Odprite izbrano nastavitev samo za urejanje, ne da bi jo izvedli. Path to an alternate GlobalSettings.xml file. Pot do alternativne datoteke GlobalSettings.xml. @@ -113,7 +116,7 @@ Ne najdem naslednjih map: If this error is ignored the folders will be considered empty. Missing folders are created automatically when needed. -V primeru ignoriranja te napake bo mapa smatrana kot prazna. Manjkajoče mape se ustvarijo avtomatično, ko je to potrebno. +V primeru prezrtja te napake bo mapa smatrana kot prazna. Manjkajoče mape se po potrebi ustvarijo samodejno. File %x has an invalid date. Datoteka %x ima neveljaven datum. @@ -122,16 +125,16 @@ Datum: Files have the same date but a different size. -Datoteke imajo isti datum a različno velikost. +Datoteke imajo isti datum toda različno velikost. Size: Velikost: Content comparison was skipped for excluded files. -Primerjava vsebine je bila preskočena za neizbrane datoteke. +Primerjava vsebine je bila preskočena zaradi izključenih datotek. Items differ in attributes only -Elementi se razlikujejo samo v atributih +Postavke se razlikujejo samo po atributih Resolving symbolic link %x Razrešujem simbolično povezavo %x @@ -143,7 +146,7 @@ Ustvarjam seznam datotek... Fail-safe file copy -Kopiranje datotek varno pred odpovedjo +Pred napako varno kopiranje datotek Enabled Omogočeno @@ -155,7 +158,7 @@ Kopiraj zaklenjene datoteke Copy file access permissions -Kopiraj dovoljenja dostopov datoteke +Kopiraj dovoljenja za dostop do datotek File time tolerance Časovna toleranca datoteke @@ -167,7 +170,7 @@ Zaženi s prioriteto v ozadju Lock directories during sync -Zakleni direktorije med sinhroniziranjem +Zakleni imenike med sinhronizacijo Verify copied files Preveri kopirane datoteke @@ -179,7 +182,7 @@ Začenjam primerjavo A folder input field is empty. -Vnosno polje za mapo je prazno. +Polje za vnos mape je prazno. The corresponding folder will be considered as empty. Ustrezna mapa bo smatrana kot prazna. @@ -194,16 +197,16 @@ Mapa mora biti izvzeta iz sinhronizacije z uporabo filtra. Calculating sync directions... -Preračunavam sinhronizacijske smeri... +Izračunavam smeri sinhronizacije... Out of memory. Ni dovolj pomnilnika. Item exists on left side only -Element obstaja samo na levi strani +Postavka obstaja samo na levi strani Item exists on right side only -Element obstaja samo na desni strani +Postavka obstaja samo na desni strani Left side is newer Leva stran je novejša @@ -212,25 +215,25 @@ Desna stran je novejša Items have different content -Elementi imajo različno vsebino +Postavke imajo različno vsebino Both sides are equal Obe strani sta enaki Conflict/item cannot be categorized -Spora/elementa ni mogoče kategorizirati +Konflikt/postavka ni mogoče kategorizirati Copy new item to left -Kopiraj nov element na levo +Kopiraj novo postavko na levo Copy new item to right -Kopiraj nov element na desno +Kopiraj novo postavko na desno Delete left item -Izbriši levi element +Izbriši postavko na levi strani Delete right item -Izbriši desni element +Izbriši postavko na desni strani Move file on left Premakni datoteko na levo stran @@ -239,10 +242,10 @@ Premakni datoteko na desno stran Update left item -Posodobi levi predmet +Posodobi postavko na levi strani Update right item -Posodobi desni predmet +Posodobi postavko na desni strani Do nothing Ne naredi ničesar @@ -268,10 +271,10 @@ Dejansko: %y bajtov Cannot write permissions of %x. -Ne morem zapisati dovoljenj od %x. +Ne morem zapisati dovoljenj za %x. Operation not supported for different base folder types. -Operacija ni podprta za različne osnovne tipe map. +Operacija ni podprta za različne vrste osnovnih map. Cannot write file %x. Ne morem zapisati datoteke %x. @@ -286,7 +289,7 @@ Dejansko: %y bajtov Ne morem povezati na %x. Failed to get information about server %x. -Ne morem pridobiti informacije o serverju %x. +Ne morem pridobiti informacije o strežniku %x. Cannot open directory %x. Ne morem odpreti imenika %x. @@ -328,7 +331,7 @@ Dejansko: %y bajtov Ne najdem %x. Type of item %x is not supported: -Element tipa %x ni podprt: +Vrsta postavke %x ni podprta: Cannot delete symbolic link %x. Ne morem izbrisati simbolične povezave %x. @@ -343,7 +346,7 @@ Dejansko: %y bajtov Napačna koda %x. The server does not support authentication via %x. -Server ne podpira preverjanja pristnosti preko %x. +Strežnik ne podpira preverjanja pristnosti preko %x. Required: Zahtevano: @@ -453,10 +456,10 @@ Dejansko: %y bajtov Zaznavanje opuščenega zaklepa... Items processed: -Obdelanih elementov: +Obdelanih postavk: Items remaining: -Preostalih elementov: +Preostalih postavk: Total time: Celoten čas: @@ -464,8 +467,8 @@ Dejansko: %y bajtov Error parsing file %x, row %y, column %z. Napaka pri razčlenjevanju datoteke %x, vrstica %y, stolpec %z. -Cannot set directory lock for %x. -Ne morem nastaviti zaklepanja imenikov za %x. +Cannot set directory locks for the following folders: +Ne morem nastaviti zaklepanje imenika za naslednje mape: 1 thread @@ -479,13 +482,13 @@ Dejansko: %y bajtov Scanning: -Pregledujem: +Skeniranje: /sec /sek %x items/sec -%x elementov/s +%x postavk/sek Show in Explorer Prikaži v Raziskovalcu @@ -497,10 +500,10 @@ Dejansko: %y bajtov Brskaj po imeniku Cannot access the Volume Shadow Copy Service. -Ne morem dostopati do Volume Shadov Copy servisa. +Ne morem dostopati do Volume Shadov Copy storitve. Please run the 64-bit version of FreeFileSync to create shadow copies on this system. -Prosim zaženite 64 bitno različico FreeFileSync, da ustvarite samodejne kopije v ozadju na tem sistemu. +Prosimo, zaženite 64-bitno različico FreeFileSync za ustvarjanje senčnih kopij v tem sistemu. Cannot determine volume name for %x. Ne morem določiti ime nosilca za %x. @@ -512,7 +515,7 @@ Dejansko: %y bajtov Zahteva za ustavitev: čakam da se trenutni proces zaključi... Unable to create time stamp for versioning: -Časovnega žiga za verzioniranje ni bilo mogoče ustvariti: +Časovnega žiga za oznčitev ni bilo mogoče ustvariti: Drag && drop Povleci && spusti @@ -539,7 +542,7 @@ Dejansko: %y bajtov &Prikaži pomoč &About -&O programu +&Vizitka &Help &Pomoč @@ -557,7 +560,7 @@ Dejansko: %y bajtov 3. Pritisnite 'Začni'. To get started just import a .ffs_batch file. -Da začnete uvozite datoteko .ffs_batch. +Če želite začeti, uvozite datoteko .ffs_batch. Folders to watch: Mape za pregled: @@ -572,10 +575,10 @@ Dejansko: %y bajtov Brskaj Idle time (in seconds): -Nedejavni čas (v sekundah): +Čas mirovanja (v sekundah): Idle time between last detected change and execution of command -Čas nedejavnosti med zadnjo zaznano spremembo in izvršitvijo ukaza +Čas mirovanja med zadnjo zaznano spremembo in izvedbo ukaza Command line: Ukazna vrstica: @@ -588,32 +591,32 @@ The command is triggered if: Ukaz se sproži če: - se spremenijo datoteke ali podmape -- pridejo nove mape (npr. ob vstavitvi USB ključka) +- pojavijo nove mape (npr. ob vstavitvi USB ključka) Start Začni About -O programu(1) +Vizitka Build: %x -Verzija: %x +Grsdnja: %x All files Vse datoteke Automated Synchronization -Avtomatska sinhnorizacija +Samodejna sinhronizacija The %x protocol does not support directory monitoring: - +Protokol %x ne podpira nadzora imenikov: Directory monitoring active Nadzor imenikov je aktiven Waiting until all directories are available... -Čakam, da so vsi imeniki dostopni... +Čakam, dokler ne bodo na voljo vsi imeniki... &Restore &Obnovi @@ -637,7 +640,7 @@ Ukaz se sproži če: Velikost datoteke Two way -Obojesmerno +Dvosmerno Mirror Zrcalno @@ -691,19 +694,19 @@ Ukaz se sproži če: Ciljna mapa %x že obstaja. Target folder input field must not be empty. -Vnosno polje za ciljno mapo ne sme biti prazno. +Vnosno polje ciljnë mape ne sme biti prazno. Source folder %x not found. Izvorne mape %x ni moč najti. Please enter a target folder for versioning. -Prosim vnesite ciljno mapo za verzioniranje. +Prosim vnesite ciljno mapo za označitev. The following items have unresolved conflicts and will not be synchronized: -Naslednji elementi imajo nerešene konflikte in ne bodo sinhronizirani: +Naslednje postavke imajo nerešene konflikte in ne bodo sinhronizirane: The following folders are significantly different. Please check that the correct folders are selected for synchronization. -Naslednje mape so bistveno različne. Prosimo preverite, da so izbrane pravilne mape za sinhroniziranje. +Naslednje mape so bistveno različne. Prosimo preverite, ali so izbrane pravilne mape za sinhroniziranje. Not enough free disk space available in: Na voljo ni dovolj prostega prostora na disku v: @@ -718,16 +721,16 @@ Ukaz se sproži če: Da bi se izognili sporom, nastavite izključno filtre tako, da je vsaka posodobljena datoteka upoštevana samo v eni osnovni mapi. Versioning folder: -Verzioniranje mape: +Označitev mape: Base folder: Osnovna mapa: The versioning folder is contained in a base folder. - +Mapa različic je vsebovana v osnovni mapi. Synchronizing folder pair: -Sinhroniziram par map: +Sinhroniziram parne mape: Generating database... Ustvarjam podatkovno bazo... @@ -739,19 +742,19 @@ Ukaz se sproži če: naziv opravila Show summary - +Pokaži povzetek Sleep - +Spanje Shut down -Ugasni +Izključi računalnik Synchronization stopped Sinhnorizacija zaustavljena Stopped -Ustavljen +Ustavljeno Synchronization completed with errors Sinhronizacija dokončana z napakami @@ -760,48 +763,48 @@ Ukaz se sproži če: Sinhronizacija dokončana z opozorili Warning -Pozor +Opozorilo Nothing to synchronize -Ni ničesar za sinhroniziranje +Nič za sinhroniziranje Synchronization completed successfully -Sinhronizacija se je uspešno končala +Sinhronizacija je uspešno končana Executing command %x - +Izvedba ukaza %x Cleaning up old log files... -Čistim stare datoteke beleženja... +Čiščenje starih datotek dnevnika... You can switch to FreeFileSync's main window to resolve this issue. -Preklopite na FreeFileSync glavno okno za odpravo težave. +Če želite odpraviti to težavo, lahko preklopite na glavno okno FreeFileSync. &Don't show this warning again -&Ne pokaži več tega opozorila +&Ne prikazuj več tega opozorila &Ignore -&Ignoriraj +&Prezri &Switch &Preklopi Switching to FreeFileSync's main window -Preklopi na FreeFileSync glavno okno +Preklop na glavno okno FreeFileSync Automatic retry in 1 second... Automatic retry in %x seconds... -Ponovni poskus čez %x sekundo... -Ponovni poskus čez %x sekundi... -Ponovni poskus čez %x sekunde... -Ponovni poskus čez %x sekund... +Samodejni poskus znova čez %x sekundo... +Samodejni poskus znova čez %x sekundi... +Samodejni poskus znova čez %x sekunde... +Samodejni poskus znova čez %x sekund... Ignore &all - +Prezri &vse Retrying operation... Ponovni poizkus operacije... @@ -809,6 +812,29 @@ Ukaz se sproži če: Serious Error Resna napaka +Last session +Zadnja seja + +Today +Danes + + +1 day +%x days + + +%x dan +%x dneva +%x dnevi +%x dni + + +Name +Ime + +Last sync +Zadnja sinhronizacija + Folder Mapa @@ -816,13 +842,13 @@ Ukaz se sproži če: Simbolična povezava Full path -Polna pot +Celotna pot Relative path Relativna pot Item name -Ime objekta +Ime postavke Size Velikost @@ -831,7 +857,7 @@ Ukaz se sproži če: Datum Extension -Razširitev +Pripona Category Kategorija @@ -840,10 +866,10 @@ Ukaz se sproži če: Ukrep Local comparison settings -Lokalne primerjalne nastavitve +Lokalne nastavitve primerjave Local synchronization settings -Lokalne sinhnorizacijske nastavitve +Lokalne nastavitve sinhnorizacije Local filter Lokalni filter @@ -870,7 +896,7 @@ Ukaz se sproži če: Izbrana mapa %x ne more biti uprabljena s FreeFileSync. Please select a folder on a local file system, network or an MTP device. -Prosim izberite mapo na lokalnem sistemu datotek, mreži ali na MTP napravi. +Prosim izberite mapo na lokalnem datotečnem sistemu, mreži ali na MTP napravi. &New &Nova @@ -879,13 +905,13 @@ Ukaz se sproži če: &Shrani Save as &batch job... -Shrani kot serijsko op&ravilo... +Shrani kot paketno op&ravilo... Start &comparison Začni &primerjavo C&omparison settings -P&rimerjalne nastavitve +N&astavitve primerjave &Filter settings Nastavitve &filtra @@ -912,13 +938,13 @@ Ukaz se sproži če: &Izvozi seznam datotek... &Reset layout -&Ponastavi razporeditev +&Ponastavi postavitev &Tools &Orodja &Check for updates now -&Preveri posodobitve sedaj +&Preveri, ali so zdaj na voljo posodobitve Check &automatically once a week S&amodejno preveri enkrat tedensko @@ -933,10 +959,10 @@ Ukaz se sproži če: Sinhroniziraj Add folder pair -Dodaj par imenikov +Dodaj pare imenikov Remove folder pair -Odstrani par imenikov +Odstrani pare imenikov Access online storage Dostop do spletnega prostora za shranjevanje @@ -951,7 +977,7 @@ Ukaz se sproži če: Išči: Match case -Ujemanje s primerom +Ujemanje primera New Nova @@ -966,10 +992,10 @@ Ukaz se sproži če: Shrani kot... View type: -Tip pogleda: +Vrsta prikaza: Select view: -Izberi pogled: +Izberi prikaz: Statistics: Statistika: @@ -996,10 +1022,10 @@ Ukaz se sproži če: Uporabi lokalne nastavitve: Select a variant: -Izberi možnost: +Izberi varianto: Include &symbolic links: -Vključuje &simbolične povezave: +Vključi &simbolične povezave: &Follow &Sledi @@ -1014,7 +1040,7 @@ Ukaz se sproži če: &Prezri časovni zamik [hh:mm] List of file time offsets to ignore -Seznam časovnih zamikov datotek, ki bodo prezrte +Seznam časovnih zamikov datotek, ki jih je treba prezreti Example: Primer: @@ -1058,8 +1084,8 @@ Ukaz se sproži če: - Detection not available for first sync -- Ni podprto z vsemi sistemi datotek -- Zahteva in ustvari datoteke v podatkovni bazi +- Ni podprto z vsemi datotečnimi sistemi +- Zahteva in ustvari datoteke v bazi podatkov - Zaznavanje ni na voljo pri prvem sinhroniziranju @@ -1070,37 +1096,37 @@ Ukaz se sproži če: &Koš &Permanent -&Dokončno +&Trajno &Versioning -&Verzioniranje +&Označitev Naming convention: -Konvencija poimenovanja: +Imenovanje konvencije: &Ignore errors - +&Prezri napake Show pop-up on errors or warnings -Prikaži pojavne napaka ali opozorila +Pokaži pojavna okna napak ali opozoril Run a command after synchronization: - +Zaženi ukaz po sinhronizaciji: OK V redu Arrange folder pair -Uredi par map +Uredi pare map Enter your login details: -Vnestite vaše podatke za prijavo: +Vnestite podatke za prijavo: Connection type: -Tip povezave: +Vrsta povezave: Server name or IP address: -Ime serverja ali IP naslova: +Ime strežnika ali naslov IP: Port: Vrata: @@ -1121,37 +1147,37 @@ Ukaz se sproži če: &Geslo &Key file -&Ključna datoteka +&Datoteka ključa &SSH agent - +&SSH agent User name: Uporabniško ime: Private key file: -Zasebni ključ datoteke: +Zasebna datoteka ključa: &Show password &Prikaži geslo Directory on server: -Imenik na serverju: +Imenik na strežniku: Performance improvements: -Izboljšanje izvedbe: +Izboljšave zmogljivosti: How to get best performance? -Kako dobiti najboljšo izvedbo? +Kako doseči najboljšo učinkovitost? Connections for directory reading: - +Povezave za branje imenika: SFTP channels per connection: SFTP kanali za povezavo: Detect server limit -Zaznaj omejitve serverja +Zaznaj omejitve strežnika Select a directory on the server: Izberite imenik na strežniku: @@ -1163,13 +1189,13 @@ Ukaz se sproži če: Zaženem sinhronizacijo zdaj? Variant: -Možnost: +Varianta: &Don't show this dialog again -&Ne pokaži več tega sporočila +&Tega pogovornega okna ne prikazuj znova Items found: -Najdenih elementov: +Najdenih postavk: Time remaining: Preostali čas: @@ -1181,7 +1207,7 @@ Ukaz se sproži če: Bitov Items -Objektov +Postavk Synchronizing... Sinhroniziram... @@ -1193,7 +1219,7 @@ Ukaz se sproži če: Prepisanih bajtov: When finished: - +Po zaključku: Close Zapri @@ -1205,16 +1231,16 @@ Ukaz se sproži če: Ustavi Create a batch file for unattended synchronization. To start, double-click this file or schedule in a task planner: %x -Ustvari skriptno datoteko za nenadzorovano sinhnorizacijo. Za zagon dvojno kliknite to datoteko ali pa jo umestite v razporejevalnik opravil: %x +Ustvari paketno datoteko za nenadzorovano sinhronizacijo. Za začetek dvokliknite to datoteko ali določite v načrtovalniku nalog: %x Run minimized -Začeni minimizirano +Zaženi minimirano &Show error dialog - +&Prikaži pogovorno okno napak &Cancel - +&Prekliči Stop synchronization at first error Ustavi sinhronizacijo ob prvi napaki @@ -1226,10 +1252,10 @@ Ukaz se sproži če: Omejitev: Limit maximum number of log files -Omeji maksimalno število datotek beleženja +Omeji največje število dnevnikov How can I schedule a batch job? -Kako nastavim urnik za serijsko opravilo? +Kako lahko načrtujem opravilo v paketu? &Keep relative paths &Ohrani relativne poti @@ -1246,23 +1272,23 @@ This guarantees a consistent state even in case of a serious error. Kopiraj v začasno datoteko (*.ffs_tmp) preden prepišeš cilj. -To zagotavlja konsistenčnost podatkov v primeru napake. +To zagotavlja dosledno stanje tudi v primeru resne napake. recommended priporočeno Copy shared or locked files using the Volume Shadow Copy Service. -Kopiraj zaklenjene in datoteke v skupni rabi s pomočjo Shadow Copy Service. +Kopiraj skupne ali zaklenjene datoteke s storitvijo Volume Shadow Copy Service. requires administrator rights - +zahteva skrbniške pravice Transfer file and folder permissions. -Prenesi pravice datotek in map. +Prenesi dovoljenja za datoteke in mape. Automatic retry on error: -Ob napaki avtomatsko poskusi znova: +Ob napaki samodejo poskusi znova: Retry count: Število poiskusov: @@ -1271,16 +1297,16 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Zakasnitev (v sekundah): Customize context menu: -Prilagodi vsebinski meni: +Prilagodi kontekstni meni: Description Opis Show hidden dialogs again -Zopet prikaži skrite dilaoge +Ponovno prikaži skrita pogovorna okna Show all permanently hidden dialogs and warning messages again -Znova prikaži vse dokončno skrite dialoge in opozorila +Prikaži vsa trajno skrita pogovorna okna in opozorilna sporočila &Default &Privzeto @@ -1292,13 +1318,13 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Če vam je FreeFileSync všeč: Support with a donation - +Podpora z donacijo Donation details Podrobnosti o donaciji The auto updater was disabled by the administrator. - +Skrbnik je onemogočil samodejno posodabljanje. Feedback and suggestions are welcome Povratne informacije in predlogi so dobrodošli @@ -1316,13 +1342,13 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Zahvala prevajalcem za lokalizacijo: Activate the FreeFileSync Donation Edition by one of the following methods: -Aktivirajte FreeFileSync Donacijsko Verzijo z eno izmed naslednjih metod: +Aktivirajte FreeFileSync Donation Edition na en od naslednjih načinov: 1. Activate via internet now: -1. Aktivirajte z uporabo interneta sedaj: +1. Aktivirajte preko interneta zdaj: Activate online -Aktivirajte na spletu +Aktivirajte po spletu 2. Retrieve an offline activation key from the following URL: 2. Pridobite aktivacijski ključ brez povezave na naslednjem URL: @@ -1336,14 +1362,17 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Activate offline Aktivirajte brez povezave +Highlight configurations that have not been run for more than the following number of days: +Označite konfiguracije, ki se ne izvajajo več kot naslednje število dni: + Save as a Batch Job - +Shrani kot paketno opravilo Delete Items -Izbriši elemente +Izbriši postavke Copy items -Kopiraj elemente +Kopiraj postavke Options Možnosti @@ -1352,7 +1381,10 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Izberi časovno obdobje FreeFileSync Donation Edition -FreeFileSync Donacijska Verzija +FreeFileSync Donation Edition + +Highlight Configurations +Označite konfiguracije &Options &Možnosti @@ -1373,7 +1405,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Konfiguracija Overview -Pregled +Predogled Show "%x" Prikaži "%x" @@ -1382,7 +1414,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. &Pokaži podrobnosti FreeFileSync %x is available! - +FreeFileSync %x je na voljo Installation files are corrupted. Please reinstall FreeFileSync. Namestitvena datoteka je poškodovana. Prosim ponovno naložite FreeFileSync. @@ -1398,10 +1430,10 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Do you really want to execute the command %y for %x items? -Ali res želite izvesti ukaz %y za %x element? -Ali res želite izvesti ukaz %y za %x elementa? -Ali res želite izvesti ukaz %y za %x elemente? -Ali res želite izvesti ukaz %y za %x elementov? +Ali res želite izvesti ukaz %y za %x postavko? +Ali res želite izvesti ukaz %y za %x postavki? +Ali res želite izvesti ukaz %y za %x postavke? +Ali res želite izvesti ukaz %y za %x postavk? &Execute @@ -1453,10 +1485,10 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Izključi preko filtra: Include temporarily -Trenutno vključi +Vključi začasno Exclude temporarily -Začasno izključi +Izključi začasno &Copy to... &Kopiraj v... @@ -1483,10 +1515,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Velika Select time span... -Izberite časovni okvir... - -Last session -Zadnja seja +Izberite časovni razpon... Folder Comparison and Synchronization Primerjava in sinhronizacija mape @@ -1506,8 +1535,11 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Do&n't save Ne shra&ni -Remove entry from list -Odstrani vnos iz seznama +Hide configuration +Skrij konfiguracijo + +Highlight... +Označi... Clear filter Počisti filter @@ -1525,13 +1557,13 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Prikaži novejše datoteke, ki so na desni strani Show files that are equal -Prikaši datoteke, ki so identične +Prikaži datoteke, ki so identične Show files that are different Prikaži datoteke, ki so različne Show conflicts -Prikaži spore +Prikaži konflikte Show files that will be created on the left side Prikaži datoteke, ki bodo ustvarjene na levi strani @@ -1570,10 +1602,10 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Ne najdem %x Move up -Premakni gor +Premakni navzgor Move down -Premakni dol +Premakni navzdol Comma-separated values Vrednosti ločene z vejico @@ -1585,13 +1617,13 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Iščem posodobitve programa... Paused -Začasna ustavitev +Začasno ustavljeno Initializing... Inicializiram... Scanning... -Pregledujem... +Skeniram... Comparing content... Primerjam vsebino... @@ -1618,40 +1650,40 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Najlepša hvala, %x, za vašo donacijo in podporo! Recommended range: - +Priporočeni obseg: Password: Geslo: Key password: -Ključno geslo: +Aktivacijski ključ: Please enter a file path. - +Vnesite pot do datoteke. Copy the following item to another folder? Copy the following %x items to another folder? -Kopiraj sledeč %x element v drugo mapo? -Kopiraj sledeča %x elementa v drugo mapo? -Kopiraj sledeče %x elemente v drugo mapo? -Kopiraj sledečih %x elementov v drugo mapo? +Kopiraj sledečo %x postavko v drugo mapo? +Kopiraj sledeči %x postavki v drugo mapo? +Kopiraj sledeče %x postavke v drugo mapo? +Kopiraj sledečih %x postavk v drugo mapo? Please enter a target folder. -Prosim, navedite ciljno mapo. +Vnesite ciljno mapo. Do you really want to move the following item to the recycle bin? Do you really want to move the following %x items to the recycle bin? -Ali res želite premakniti sledeč %x element v koš? -Ali res želite premakniti sledeča %x elementa v koš? -Ali res želite premakniti sledeče %x elemente v koš? -Ali res želite premakniti sledečih %x elementov v koš? +Ali res želite premakniti sledečo %x postavko v koš? +Ali res želite premakniti sledeči %x postavki v koš? +Ali res želite premakniti sledeče %x postavke v koš? +Ali res želite premakniti sledečih %x postavk v koš? Move @@ -1662,20 +1694,20 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Do you really want to delete the following %x items? -Ali resnično želite izbrisati sledeči %x element? -Ali resnično želite izbrisati naslednja %x elementa? -Ali resnično želite izbrisati naslednje %x elemente? -Ali resnično želite izbrisati naslednjih %x elementov? +Ali res želite izbrisati sledečo %x postavko? +Ali res želite izbrisati sledeči %x postavki? +Ali res želite izbrisati sledeče %x postavke? +Ali res želite izbrisati sledečih %x postavk? Copy DACL, SACL, Owner, Group Kopiraj DACL, SACL, lastnik, skupina Integrate external applications into context menu. The following macros are available: -Integriraj zunanje aplikacije v kontekstni menu. Na voljo so naslednji makri: +Integriraj zunanje aplikacije v kontekstni meni. Na voljo so naslednji makroji: Full file or folder path -Celotna datoteka ali pot do mape +Celotna pot do datoteke ali mape Parent folder path Pot do nadrejene mape @@ -1687,7 +1719,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Parametri za nasprotno stran Show hidden dialogs and warning messages again? -Ponovno prikaži skrite dialoge in obvestila? +Ponovno prikažem skrita pogovorna okna in opozorilna sporočila? &Show &Prikaži @@ -1696,19 +1728,19 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Prenašam posodobitve... Identify equal files by comparing modification time and size. -Določi enake datoteke s primerjavo datuma spremembe in velikosti. +Ugotovi enake datoteke s primerjanjem časa spremembe in velikosti. Identify equal files by comparing the file content. -Določi enake datoteke s primerjavo vsebine. +Ugotovi enake datoteke s primerjanjem vsebine. Identify equal files by comparing their file size. -Identificiraj enake datoteke s primerjavo velikoti teh datotek. +Ugotovi enake datoteke s primerjanjem njihove velikosti. Identify and propagate changes on both sides. Deletions, moves and conflicts are detected automatically using a database. -Identificiraj in razširjaj spremembe na obeh straneh. Izbrisi, premiki in spori so samodejno zaznani z uporabo podatkovne baze. +Ugotovi in izvrši spremembe na obeh straneh. Izbrisi, premiki in spori so samodejno zaznani z uporabo podatkovne baze. Create a mirror backup of the left folder by adapting the right folder to match. -Ustvari zrcalno kopijo levega imenika s prilagoditvijo desnega imenika, tako da se ujemata. +Ustvari zrcalno varnostno kopijo leve mape tako, da prilagodite desno mapo, ki se bo ujemala. Copy new and updated files to the right folder. Kopiraj nove in posodobljene datoteke v desni imenik. @@ -1725,9 +1757,6 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Synchronization Sinhnorizacija -Today -Danes - This week Ta teden @@ -1750,19 +1779,19 @@ To zagotavlja konsistenčnost podatkov v primeru napake. MB Retain deleted and overwritten files in the recycle bin - +Zadrži izbrisane in prepisane datoteke v košu Delete and overwrite files permanently - +Trajni izbris in prepis datotek Move files to a user-defined folder -Premakni datoteke v izbran imenik +Premakni datoteke v uporabniško določeno mapo Replace Zamenjaj Move files and replace if existing -Premakne datoteke in jih zamenja, že obstajajo +Premakni datoteke in jih zamenjaj, če že obstajajo Time stamp Časovna oznaka @@ -1774,10 +1803,10 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Ob zaključku: On errors: - +Ob napakah: On success: - +Ob uspehu: Main config Glavna konfiguracija @@ -1786,7 +1815,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. prazno Leave as unresolved conflict -Pusti kot nerešeni spor +Pusti kot nerešeni konflikt File Datoteka @@ -1797,9 +1826,6 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Files Datoteke -Name -Ime - Percentage Odstotek @@ -1810,22 +1836,22 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Samodejne posodobitve: Requires FreeFileSync Donation Edition -Zahteva FreeFileSync Donacijsko Verzijo +Zahteva FreeFileSync Donation Edition Check for Program Updates -Prevri obstoj nadgradnje programa +Preveri obstoj posodobitve programa Auto-update now or download manually from the FreeFileSync home page? -Avtomatska posodobitev ali ročni prenos iz FreeFileSync domače strani? +Samodejna posodobitev zdaj, ali prenos z domače strani FreeFileSync? &Auto-update -&Avtomatska posodobitev +&Samodejna posodobitev &Home page &Domača stran Download now? -Prenesem sedaj? +Prenesem zdaj? &Download &Prenesi @@ -1834,49 +1860,49 @@ To zagotavlja konsistenčnost podatkov v primeru napake. FreeFileSync je posodobljen. Cannot find current FreeFileSync version number online. A newer version is likely available. Check manually now? -Ne morem najti trenutne številke verzije FreeFileSync na spletu. Novejša verzija je najbrž na voljo. Želite preveriti ročno? +Na spletu ni mogoče najti trenutne številke različice FreeFileSync. Verjetno je na voljo nova različica. Želite preveriti zdaj? &Check &Preveri Consistency check failed for %x. -Preverjanje usklajenosti ni uspelo za %x. +Preverjanje doslednosti ni uspelo za %x. Installation was registered on a different operating system. -Namestitev je bila registrirana na drugem operacisjkem sistemu. +Namestitev je bila registrirana v drugem operacijskem sistemu. Failed to activate FreeFileSync Donation Edition. -Ne morem aktivirati FreeFileSync Donacijske verzije. +Programa FreeFileSync Donation Edition ni bilo mogoče aktivirati. Incorrect activation key. -Nepravilen aktivacisjki ključ. +Nepravilni ključ za aktiviranje. Unable to register to receive system messages. -Ne morem se registrirati za prejem sistemskih sporočil. +Sistemskih sporočil ni mogoče registrirati. Cannot find system function %x. -Ne morem najti sistemske funkcije %x. +Sistemske funkcije ni mogoče najti %x. Unable to register device notifications for %x. -Omogoči registracijo obvestil naprave za %x. +Ne morem registrirati obvestil naprave za %x. Cannot monitor directory %x. -Ne morem nadzorovati imenika %x. +Ne morem nadzirati imenika %x. The file is locked by another process: -Datoteka je zaklenjena s strani drugega procesa: +Datoteko je zaklenil drug proces: Cannot read security context of %x. -Ne morem prebrati varnostnega konteksta od %x. +Ne morem prebrati varnostni kontekst od %x. Cannot write security context of %x. -Ne morem zapisati varnostnega konteksta od %x. +Ne morem zapisati varnostni kontekst od %x. Cannot read permissions of %x. Ne morem prebrati dovoljenja od %x. Cannot copy permissions from %x to %y. -Ne morem kopirati uporabniških pravic iz %x v %y. +Ne morem kopirati dovoljenj iz %x v %y. %x is not a regular directory name. %x ni pravilno ime imenika. @@ -1915,46 +1941,35 @@ To zagotavlja konsistenčnost podatkov v primeru napake. %x ur - -1 day -%x days - - -%x dan -%x dneva -%x dnevi -%x dni - - Cannot set privilege %x. Ne morem nastaviti privilegija %x. Unable to suspend system sleep mode. -Ne morem preprečiti mirovanja sistema. +Ne morem preprečiti način mirovanja sistema. Cannot change process I/O priorities. -Ne morem spremeniti V/I prioritet procesa. +Ne morem spremeniti prioritet procesa I/O. Unable to shut down the system. - +Sistema ni mogoče zapustiti. Checking recycle bin failed for folder %x. Preverjanje koša za mapo %x ni uspelo. The following XML elements could not be read: -Neaslednji XML elementi niso berljivi: +Neaslednje XML postavke niso berljive: Configuration file %x is incomplete. The missing elements will be set to their default values. -Nastavitvena datoteka %x je nepopolna. Manjkajoči element bo nastavljen na privzete vrednosti. +Nastavitvena datoteka %x je nepopolna. Manjkajoči elementi bodo nastavljeni na privzete vrednosti. Prepare installation Pripravljam namestitev Choose which components you want to install. -Izberite komponente za namestitev. +Izberite komponente, ki jih želite namestiti. Select installation type: -Izberi tip namestitve: +Izberite vrsto namestitve: Local Lokalna @@ -1966,7 +1981,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Shrani nastavitve v "%APPDATA%\FreeFileSync" Register FreeFileSync file extensions -Registracija FreeFileSync končnic datotek +Registriraj FreeFileSync datoteče pripone Create Explorer context menu entries Ustvari vnose v Explorer-jev kontekstni meni @@ -1990,13 +2005,16 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Namizje Start menu -Začetni meni +Meni Start + +Send To +Pošlji Registering FreeFileSync file extensions -Registracija FreeFileSync končnic datotek +Registracija FreeFileSync datotečnih pripon Unregistering FreeFileSync file extensions -Odstranjevanje registracije FreeFileSync končnic datotek +Odstranjevanje registracije FreeFileSync datotečnih pripon FreeFileSync Configuration Nastavitve FreeFileSync @@ -2005,7 +2023,7 @@ To zagotavlja konsistenčnost podatkov v primeru napake. FreeFileSync paketna datoteka FreeFileSync Synchronization Database -FreeFileSync sinhronizacijska baza +FreeFileSync sinhronizacijska baza podatkov RealTimeSync Configuration RealTimeSync nastavitve @@ -2014,11 +2032,11 @@ To zagotavlja konsistenčnost podatkov v primeru napake. Uredi z FreeFileSync The FreeFileSync portable version cannot install into a subfolder of %x. - +Prenosne različice FreeFileSync ni mogoče namestiti v podmapo %x. Please choose the local installation type or select a different folder for installation. -Prosimo izberite tip lokalne namestitve ali pa izberite drugo mapo za namestitev. +Prosimo izberite vrsto lokalne namestitve ali pa izberite drugo mapo za namestitev. -The silent installation mode is only available in the FreeFileSync Donation Edition. -Tihi namestitveni način je na voljo samo v FreeFileSync Donacijski Verziji. +The %x installation option is only available in the FreeFileSync Donation Edition. +Možnost namestitve %x je na voljo samo v FreeFileSync Donation Edition. diff --git a/FreeFileSync/Build/Languages/turkish.lng b/FreeFileSync/Build/Languages/turkish.lng index 1e4843c2..5afdd52e 100755 --- a/FreeFileSync/Build/Languages/turkish.lng +++ b/FreeFileSync/Build/Languages/turkish.lng @@ -53,7 +53,7 @@ %x klasörü için Geri Dönüşüm Kutusu kullanılabilir mi diye bakılıyor... The recycle bin is not supported by the following folders. Deleted or overwritten files will not be able to be restored: - +Geri Dönüşüm Kutusu şu klasörler tarafından desteklenmiyor. Silinmiş ya da üzerine yazılmış klasörler geri yüklenemez: An exception occurred Olağan dışı bir durumla karşılaşıldı @@ -71,7 +71,7 @@ Hata File %x does not contain a valid configuration. -%x dosyası geçerli ayar bilgilerini içermiyor. +%x dosyası geçerli yapılandırma bilgilerini içermiyor. Unequal number of left and right directories specified. Sağdan ve soldan seçilen klasör sayısı aynı değil. @@ -80,7 +80,7 @@ Klasörler komut satırından seçildiği zaman, ayar dosyasında klasör çifti düzeyinde ayarlar bulunmamalıdır. Directories cannot be set for more than one configuration file. -Klasörler birden fazla ayar dosyasında kullanılamaz. +Klasörler birden fazla yapılandırma dosyasında kullanılamaz. Command line Komut Satırı @@ -98,13 +98,13 @@ genel ayar dosyası: Any number of FreeFileSync .ffs_gui and/or .ffs_batch configuration files. -FreeFileSync .ffs_gui ya da .ffs_batch ayar dosyalarının sayısı. +FreeFileSync .ffs_gui ya da .ffs_batch yapılandırma dosyalarının sayısı. Any number of alternative directory pairs for at most one config file. En fazla bir ayar dosyası için herhangi bir sayıda alternatif klasör çifti. Open the selected configuration for editing only without executing it. - +Seçilmiş yapılandırmayı yürütmeden yalnız düzenlemek için açar. Path to an alternate GlobalSettings.xml file. Alternatif GlobalSettings.xml dosyasının yolu. @@ -316,7 +316,7 @@ Gerçekleşen: %y bayt %x sembolik bağlantısı çözümlenemedi. Unable to move %x to the recycle bin. -%x çöp kutusuna atılamadı. +%x geri dönüşüm kutusuna atılamadı. Cannot open file %x. %x dosyası açılamadı. @@ -597,7 +597,7 @@ Komut şu durumlarda yürütülür: Otomatik Eşitleme The %x protocol does not support directory monitoring: - +%x iletişim kuralı klasör izlemesini desteklemiyor: Directory monitoring active Klasör izlemesi yapılıyor @@ -714,7 +714,7 @@ Komut şu durumlarda yürütülür: Temel klasör: The versioning folder is contained in a base folder. - +Sürümlendirme klasörü bir temel klasör içinde bulunuyor. Synchronizing folder pair: Eşitlenen klasör çifti: @@ -729,10 +729,10 @@ Komut şu durumlarda yürütülür: iş adı Show summary - +Özet Görüntülensin Sleep - +Bilgisayar Uykuya Dalsın Shut down Bilgisayar Kapatılsın @@ -759,7 +759,7 @@ Komut şu durumlarda yürütülür: Eşitleme tamamlandı Executing command %x - +%x komutu yürütülüyor Cleaning up old log files... Eski günlük dosyaları temizleniyor... @@ -789,7 +789,7 @@ Komut şu durumlarda yürütülür: Ignore &all - +Tümünü Yok S&ay Retrying operation... İşlem yeniden deneniyor... @@ -1067,13 +1067,13 @@ Komut şu durumlarda yürütülür: Adlandırma Kuralı: &Ignore errors - +&Hatalar yok sayılsın Show pop-up on errors or warnings Hata ya da uyarılar açılır pencerede görüntülenir Run a command after synchronization: - +Eşitleme sonrası yürütülecek komut: OK Tamam @@ -1112,7 +1112,7 @@ Komut şu durumlarda yürütülür: Anahtar &Dosyası &SSH agent - +&SSH İstemcisi User name: Kullanıcı Adı: @@ -1133,7 +1133,7 @@ Komut şu durumlarda yürütülür: En iyi başarım nasıl elde edilir? Connections for directory reading: - +Klasör okuma bağlantıları: SFTP channels per connection: Bir Bağlantı için SFTP Kanalı Sayısı: @@ -1181,7 +1181,7 @@ Komut şu durumlarda yürütülür: Kopyalanan bayt: When finished: - +Tamamlandığında: Close Kapat @@ -1196,19 +1196,19 @@ Komut şu durumlarda yürütülür: Eşitleme işleminin hiç bir soru sorulmadan yapılması için bir toplu iş dosyası oluşturun. İşlemi başlatmak için bu dosyaya çift tıklayın ya da bir görev zamanlayıcıya şu şekilde ekleyin: %x Run minimized -Küçülterek Çalıştır +Küçültülmüş Çalıştırılsın &Show error dialog - +Hata &penceresi görüntülensin &Cancel - +İ&ptal edilsin Stop synchronization at first error Oluşacak ilk hatada eşitleme durdurulsun Save log: -İşlem Günlüğünü Kaydet: +İşlem Günlüğü Kaydedilsin: Limit: Sınır: @@ -1244,7 +1244,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Paylaşılan ya da kilitlenmiş dosyalar Birim Gölge Hizmetini kullanılarak kopyalanır. requires administrator rights - +yönetici izinleri gerekir Transfer file and folder permissions. Dosya ve klasör izinleri de aktarılır. @@ -1280,13 +1280,13 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya FreeFileSync hoşunuza gittiyse: Support with a donation - +Bağış yaparak destek olun Donation details Bağış Bilgileri The auto updater was disabled by the administrator. - +Otomatik güncelleme yönetici tarafından devre dışı bırakılmış. Feedback and suggestions are welcome Öneri ve geri bildirimlerinizi bekleriz @@ -1325,7 +1325,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Çevrimdışı Etkinleştir Save as a Batch Job - +Toplu İş Olarak Kaydet Delete Items Ögeleri Sil @@ -1370,7 +1370,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya &Ayrıntılara Bakın FreeFileSync %x is available! - +FreeFileSync %x sürümü yayınlanmış! Installation files are corrupted. Please reinstall FreeFileSync. Kurulum dosyaları bozulmuş. Lütfen FreeFileSync uygulamasını yeniden kurun. @@ -1472,7 +1472,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Klasör Karşılaştırma ve Eşitleme Configuration saved -Ayarlar kaydedildi +Yapılandırma kaydedildi FreeFileSync batch FreeFileSync toplu işi @@ -1598,7 +1598,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Sevgili %x, bağışın ve desteğin için teşekkürler! Recommended range: - +Önerilen Aralık: Password: Parola: @@ -1607,7 +1607,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Anahtar Parolası: Please enter a file path. - +Lütfen bir dosya yolu yazın. Copy the following item to another folder? @@ -1691,7 +1691,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Eşitleme kuralları kullanıcının isteğine göre belirlenir. Synchronization Settings -Eşitleme ayarları +Eşitleme Ayarları Comparison Karşılaştırma @@ -1724,10 +1724,10 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya MB Retain deleted and overwritten files in the recycle bin - +Silinmiş ve üzerine yazılmış dosyalar geri dönüşüm kutusunda korunsun Delete and overwrite files permanently - +Dosyalar silinsin ve kalıcı olarak üzerine yazılsın Move files to a user-defined folder Dosyalar kullanıcı tarafından belirtilen bir klasöre taşınır @@ -1745,16 +1745,16 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Dosya adlarına zaman damgası eklensin On completion: -İşlem Tamamlandığında: +Tamamlandığında: On errors: - +Sorun Çıktığında: On success: - +Başarılı Olduğunda: Main config -Temel ayarlar +Temel Yapılandırma empty boş @@ -1904,7 +1904,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Giriş/Çıkış işlemi öncelikleri değiştirilemedi. Unable to shut down the system. - +Bilgisayar kapatılamıyor. Checking recycle bin failed for folder %x. %x klasörü için Geri Dönüşüm Kutusu denetlenemedi. @@ -1913,7 +1913,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya Şu XML bileşenleri okunamadı: Configuration file %x is incomplete. The missing elements will be set to their default values. -%x ayar dosyası tam değil. Eksik bileşenler için varsayılan değerler kullanılacak. +%x yapılandırma dosyası tam değil. Eksik bileşenler için varsayılan değerler kullanılacak. Prepare installation Kuruluma hazırlanıyor @@ -1967,7 +1967,7 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya FreeFileSync dosya uzantıları kayıt defterinden siliniyor FreeFileSync Configuration -FreeFileSync Ayarları +FreeFileSync Yapılandırması FreeFileSync Batch File FreeFileSync Toplu İşlem Dosyası @@ -1976,13 +1976,13 @@ Bu yöntem, ciddi bir hata oluşması durumunda bile işlemin tutarlı olarak ya FreeFileSync Eşitleme Veritabanı RealTimeSync Configuration -RealTimeSync Ayarları +RealTimeSync Yapılandırması Edit with FreeFileSync FreeFileSync ile Düzenlensin The FreeFileSync portable version cannot install into a subfolder of %x. - +FreeFileSync taşınabilir sürümü bir %x alt klasörüne yüklenemez. Please choose the local installation type or select a different folder for installation. Kurulum için farklı bir klasör ya da yerel kurulum türünü seçin. diff --git a/FreeFileSync/Build/Resources.zip b/FreeFileSync/Build/Resources.zip index 5c7cc44b..c0c2c2ed 100755 Binary files a/FreeFileSync/Build/Resources.zip and b/FreeFileSync/Build/Resources.zip differ diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile index 46e18617..33197267 100755 --- a/FreeFileSync/Source/Makefile +++ b/FreeFileSync/Source/Makefile @@ -37,15 +37,16 @@ CPP_LIST+=fs/abstract.cpp CPP_LIST+=fs/concrete.cpp CPP_LIST+=fs/native.cpp CPP_LIST+=file_hierarchy.cpp -CPP_LIST+=ui/custom_grid.cpp +CPP_LIST+=ui/cfg_grid.cpp +CPP_LIST+=ui/file_grid.cpp CPP_LIST+=ui/folder_history_box.cpp CPP_LIST+=ui/command_box.cpp CPP_LIST+=ui/folder_selector.cpp CPP_LIST+=ui/batch_config.cpp CPP_LIST+=ui/batch_status_handler.cpp CPP_LIST+=ui/version_check.cpp -CPP_LIST+=ui/grid_view.cpp -CPP_LIST+=ui/tree_view.cpp +CPP_LIST+=ui/file_view.cpp +CPP_LIST+=ui/tree_grid.cpp CPP_LIST+=ui/gui_generated.cpp CPP_LIST+=ui/gui_status_handler.cpp CPP_LIST+=ui/main_dlg.cpp diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index 4acd8c25..f665c783 100755 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -214,7 +214,7 @@ void MainDialog::OnConfigSave(wxCommandEvent& event) { Zstring defaultFileName = currentConfigFileName_.empty() ? Zstr("Realtime.ffs_real") : currentConfigFileName_; //attention: currentConfigFileName may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - if (endsWith(defaultFileName, Zstr(".ffs_batch"))) + if (endsWith(defaultFileName, Zstr(".ffs_batch"), CmpFilePath())) defaultFileName = beforeLast(defaultFileName, Zstr("."), IF_MISSING_RETURN_NONE) + Zstr(".ffs_real"); wxFileDialog filePicker(this, diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index 005f5a64..d38573ef 100755 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -133,8 +133,8 @@ void Application::onQueryEndSession(wxEvent& event) void runGuiMode (const Zstring& globalConfigFile); -void runGuiMode (const Zstring& globalConfigFile, const XmlGuiConfig& guiCfg, const std::vector& referenceFiles, bool startComparison); -void runBatchMode(const Zstring& globalConfigFile, const XmlBatchConfig& batchCfg, const Zstring& referenceFile, FfsReturnCode& returnCode); +void runGuiMode (const Zstring& globalConfigFile, const XmlGuiConfig& guiCfg, const std::vector& cfgFilePaths, bool startComparison); +void runBatchMode(const Zstring& globalConfigFile, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsReturnCode& returnCode); void showSyntaxHelp(); @@ -156,15 +156,19 @@ void Application::launch(const std::vector& commandArgs) }; //parse command line arguments - std::vector dirPathPhrasesLeft; - std::vector dirPathPhrasesRight; + std::vector> dirPathPhrasePairs; std::vector> configFiles; //XmlType: batch or GUI files only Zstring globalConfigFile; bool openForEdit = false; { + std::vector dirPathPhrasesLeft; //TODO: remove migration code at some time! 2017-12-14 + std::vector dirPathPhrasesRight; // + const Zchar optionEdit [] = Zstr("-edit"); - const Zchar optionLeftDir [] = Zstr("-leftdir"); - const Zchar optionRightDir[] = Zstr("-rightdir"); + const Zchar optionLeftDir [] = Zstr("-leftdir"); //TODO: remove migration code at some time! 2017-12-14 + const Zchar optionRightDir[] = Zstr("-rightdir"); // + const Zchar optionDirPair [] = Zstr("-dirpair"); + const Zchar optionSendTo [] = Zstr("-sendto"); //remaining arguments are unspecified number of folder paths; wonky syntax; let's keep it undocumented auto syntaxHelpRequested = [&](const Zstring& arg) { @@ -177,6 +181,16 @@ void Application::launch(const std::vector& commandArgs) argTmp == Zstr("?"); }; + auto isCommandLineOption = [&](const Zstring& arg) + { + return strEqual(arg, optionEdit, CmpAsciiNoCase()) || + strEqual(arg, optionLeftDir, CmpAsciiNoCase()) || + strEqual(arg, optionRightDir, CmpAsciiNoCase()) || + strEqual(arg, optionDirPair, CmpAsciiNoCase()) || + strEqual(arg, optionSendTo, CmpAsciiNoCase()) || + syntaxHelpRequested(arg); + }; + for (auto it = commandArgs.begin(); it != commandArgs.end(); ++it) if (syntaxHelpRequested(*it)) return showSyntaxHelp(); @@ -184,7 +198,7 @@ void Application::launch(const std::vector& commandArgs) openForEdit = true; else if (strEqual(*it, optionLeftDir, CmpAsciiNoCase())) { - if (++it == commandArgs.end()) + if (++it == commandArgs.end() || isCommandLineOption(*it)) { notifyFatalError(replaceCpy(_("A directory path is expected after %x."), L"%x", utfTo(optionLeftDir)), _("Syntax error")); return; @@ -193,13 +207,66 @@ void Application::launch(const std::vector& commandArgs) } else if (strEqual(*it, optionRightDir, CmpAsciiNoCase())) { - if (++it == commandArgs.end()) + if (++it == commandArgs.end() || isCommandLineOption(*it)) { notifyFatalError(replaceCpy(_("A directory path is expected after %x."), L"%x", utfTo(optionRightDir)), _("Syntax error")); return; } dirPathPhrasesRight.push_back(*it); } + else if (strEqual(*it, optionDirPair, CmpAsciiNoCase())) + { + if (++it == commandArgs.end() || isCommandLineOption(*it)) + { + notifyFatalError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo(optionDirPair)), _("Syntax error")); + return; + } + dirPathPhrasePairs.emplace_back(*it, Zstring()); + + if (++it == commandArgs.end() || isCommandLineOption(*it)) + { + notifyFatalError(replaceCpy(_("A left and a right directory path are expected after %x."), L"%x", utfTo(optionDirPair)), _("Syntax error")); + return; + } + dirPathPhrasePairs.back().second = *it; + } + else if (strEqual(*it, optionSendTo, CmpAsciiNoCase())) + { + for (size_t i = 0; ; ++i) + { + if (++it == commandArgs.end() || isCommandLineOption(*it)) + { + --it; + break; + } + + if (i < 2) //-SendTo with more than 2 paths? Doesn't make any sense, does it!? + { + //for -SendTo we expect a list of full native paths, not "phrases" that need to be resolved! + auto getFolderPath = [](Zstring itemPath) + { + try + { + if (getItemType(itemPath) == ItemType::FILE) //throw FileError + if (Opt parentPath = getParentFolderPath(itemPath)) + return *parentPath; + } + catch (FileError&) {} + + return itemPath; + }; + + if (i % 2 == 0) + dirPathPhrasePairs.emplace_back(getFolderPath(*it), Zstring()); + else + { + const Zstring folderPath = getFolderPath(*it); + if (!equalFilePath(dirPathPhrasePairs.back().first, folderPath)) //user accidentally sending to two files, which each time yield the same parent folder + dirPathPhrasePairs.back().second = folderPath; + } + } + } + } else { Zstring filePath = getResolvedFilePath(*it); @@ -243,13 +310,17 @@ void Application::launch(const std::vector& commandArgs) return; } } - } - if (dirPathPhrasesLeft.size() != dirPathPhrasesRight.size()) - { - notifyFatalError(_("Unequal number of left and right directories specified."), _("Syntax error")); - return; + if (dirPathPhrasesLeft.size() != dirPathPhrasesRight.size()) + { + notifyFatalError(_("Unequal number of left and right directories specified."), _("Syntax error")); + return; + } + + for (size_t i = 0; i < dirPathPhrasesLeft.size(); ++i) + dirPathPhrasePairs.emplace_back(dirPathPhrasesLeft[i], dirPathPhrasesRight[i]); } + //---------------------------------------------------------------------------------------------------- auto hasNonDefaultConfig = [](const FolderPairEnh& fp) { @@ -260,7 +331,7 @@ void Application::launch(const std::vector& commandArgs) auto replaceDirectories = [&](MainConfiguration& mainCfg) { - if (!dirPathPhrasesLeft.empty()) + if (!dirPathPhrasePairs.empty()) { //check if config at folder-pair level is present: this probably doesn't make sense when replacing/adding the user-specified directories if (hasNonDefaultConfig(mainCfg.firstPair) || std::any_of(mainCfg.additionalPairs.begin(), mainCfg.additionalPairs.end(), hasNonDefaultConfig)) @@ -270,14 +341,14 @@ void Application::launch(const std::vector& commandArgs) } mainCfg.additionalPairs.clear(); - for (size_t i = 0; i < dirPathPhrasesLeft.size(); ++i) + for (size_t i = 0; i < dirPathPhrasePairs.size(); ++i) if (i == 0) { - mainCfg.firstPair.folderPathPhraseLeft_ = dirPathPhrasesLeft [0]; - mainCfg.firstPair.folderPathPhraseRight_ = dirPathPhrasesRight[0]; + mainCfg.firstPair.folderPathPhraseLeft_ = dirPathPhrasePairs[0].first; + mainCfg.firstPair.folderPathPhraseRight_ = dirPathPhrasePairs[0].second; } else - mainCfg.additionalPairs.emplace_back(dirPathPhrasesLeft[i], dirPathPhrasesRight[i], + mainCfg.additionalPairs.emplace_back(dirPathPhrasePairs[i].first, dirPathPhrasePairs[i].second, nullptr, nullptr, FilterConfig()); } return true; @@ -290,7 +361,7 @@ void Application::launch(const std::vector& commandArgs) if (configFiles.empty()) { //gui mode: default startup - if (dirPathPhrasesLeft.empty()) + if (dirPathPhrasePairs.empty()) runGuiMode(globalConfigFilePath); //gui mode: default config with given directories else @@ -357,7 +428,7 @@ void Application::launch(const std::vector& commandArgs) //gui mode: merged configs else { - if (!dirPathPhrasesLeft.empty()) + if (!dirPathPhrasePairs.empty()) { notifyFatalError(_("Directories cannot be set for more than one configuration file."), _("Syntax error")); return; @@ -387,15 +458,15 @@ void Application::launch(const std::vector& commandArgs) } -void runGuiMode(const Zstring& globalConfigFile) { MainDialog::create(globalConfigFile); } +void runGuiMode(const Zstring& globalConfigFilePath) { MainDialog::create(globalConfigFilePath); } -void runGuiMode(const Zstring& globalConfigFile, +void runGuiMode(const Zstring& globalConfigFilePath, const xmlAccess::XmlGuiConfig& guiCfg, - const std::vector& referenceFiles, + const std::vector& cfgFilePaths, bool startComparison) { - MainDialog::create(globalConfigFile, nullptr, guiCfg, referenceFiles, startComparison); + MainDialog::create(globalConfigFilePath, nullptr, guiCfg, cfgFilePaths, startComparison); } @@ -406,7 +477,7 @@ void showSyntaxHelp() setDetailInstructions(_("Syntax:") + L"\n\n" + L"./FreeFileSync " + L"\n" + L" [" + _("config files:") + L" *.ffs_gui/*.ffs_batch]" + L"\n" + - L" [-LeftDir " + _("directory") + L"] [-RightDir " + _("directory") + L"]" + L"\n" + + L" [-DirPair " + _("directory") + L" " + _("directory") + L"]" + L"\n" + L" [-Edit]" + L"\n" + L" [" + _("global config file:") + L" GlobalSettings.xml]" + L"\n" + L"\n" + @@ -414,7 +485,7 @@ void showSyntaxHelp() _("config files:") + L"\n" + _("Any number of FreeFileSync .ffs_gui and/or .ffs_batch configuration files.") + L"\n\n" + - L"-LeftDir " + _("directory") + L" -RightDir " + _("directory") + L"\n" + + L"-DirPair " + _("directory") + L" " + _("directory") + L"\n" + _("Any number of alternative directory pairs for at most one config file.") + L"\n\n" + L"-Edit" + L"\n" + @@ -425,7 +496,7 @@ void showSyntaxHelp() } -void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& batchCfg, const Zstring& referenceFile, FfsReturnCode& returnCode) +void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsReturnCode& returnCode) { const bool showPopupAllowed = !batchCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorDialog == BatchErrorDialog::SHOW; @@ -476,7 +547,7 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat //class handling status updates and error messages BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized, //throw AbortProcess, BatchRequestSwitchToMainDialog - extractJobName(referenceFile), + extractJobName(cfgFilePath), globalCfg.soundFileSyncFinished, batchStartTime, batchCfg.batchExCfg.logFolderPathPhrase, @@ -525,12 +596,20 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat cmpResult, globalCfg.optDialogs, statusHandler); //throw ? + + //not cancelled? => update last sync date for the selected cfg file + for (xmlAccess::ConfigFileItem& cfi : globalCfg.gui.mainDlg.cfgFileHistory) + if (equalFilePath(cfi.filePath, cfgFilePath)) + { + cfi.lastSyncTime = std::time(nullptr); + break; + } } catch (AbortProcess&) {} //exit used by statusHandler catch (BatchRequestSwitchToMainDialog&) { //open new toplevel window *after* progress dialog is gone => run on main event loop - return MainDialog::create(globalConfigFilePath, &globalCfg, xmlAccess::convertBatchToGui(batchCfg), { referenceFile }, true /*startComparison*/); + return MainDialog::create(globalConfigFilePath, &globalCfg, xmlAccess::convertBatchToGui(batchCfg), { cfgFilePath }, true /*startComparison*/); } try //save global settings to XML: e.g. ignored warnings diff --git a/FreeFileSync/Source/comparison.cpp b/FreeFileSync/Source/comparison.cpp index b75dc62a..ac89cb6c 100755 --- a/FreeFileSync/Source/comparison.cpp +++ b/FreeFileSync/Source/comparison.cpp @@ -86,25 +86,25 @@ ResolvedBaseFolders initializeBaseFolders(const std::vector& cfgL if (!status.notExisting.empty() || !status.failedChecks.empty()) { - std::wstring errorMsg = _("Cannot find the following folders:") + L"\n"; + std::wstring msg = _("Cannot find the following folders:") + L"\n"; for (const AbstractPath& folderPath : status.notExisting) - errorMsg += L"\n" + AFS::getDisplayPath(folderPath); + msg += L"\n" + AFS::getDisplayPath(folderPath); for (const auto& fc : status.failedChecks) - errorMsg += L"\n" + AFS::getDisplayPath(fc.first); + msg += L"\n" + AFS::getDisplayPath(fc.first); - errorMsg += L"\n\n"; - errorMsg += _("If this error is ignored the folders will be considered empty. Missing folders are created automatically when needed."); + msg += L"\n\n"; + msg += _("If this error is ignored the folders will be considered empty. Missing folders are created automatically when needed."); if (!status.failedChecks.empty()) { - errorMsg += L"\n___________________________________________"; + msg += L"\n___________________________________________"; for (const auto& fc : status.failedChecks) - errorMsg += L"\n\n" + replaceCpy(fc.second.toString(), L"\n\n", L"\n"); + msg += L"\n\n" + replaceCpy(fc.second.toString(), L"\n\n", L"\n"); } - throw FileError(errorMsg); + throw FileError(msg); } }, callback); //throw X? diff --git a/FreeFileSync/Source/file_hierarchy.h b/FreeFileSync/Source/file_hierarchy.h index ca9e6248..5b0ae6c7 100755 --- a/FreeFileSync/Source/file_hierarchy.h +++ b/FreeFileSync/Source/file_hierarchy.h @@ -347,7 +347,7 @@ public: V& operator* () const { return **it_; } V* operator->() const { return &** it_; } private: - IterImpl it_; + IterImpl it_{}; }; /* @@ -505,7 +505,7 @@ private: std::unique_ptr syncDirectionConflict_; //non-empty if we have a conflict setting sync-direction //get rid of std::wstring small string optimization (consumes 32/48 byte on VS2010 x86/x64!) - Zstring itemNameL_; //slightly redundant under linux, but on windows the "same" file paths can differ in case + 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! ContainerObject& parent_; @@ -794,7 +794,7 @@ bool FileSystemObject::isPairEmpty() const template inline Zstring FileSystemObject::getItemName() const { - assert(!itemNameL_.empty() || !itemNameR_.empty()); + //assert(!itemNameL_.empty() || !itemNameR_.empty()); -> file pair might be empty (until removed after sync) const Zstring& itemName = SelectParam::ref(itemNameL_, itemNameR_); //empty if not existing if (!itemName.empty()) //avoid ternary-WTF! (implicit copy-constructor call!!!!!!) diff --git a/FreeFileSync/Source/lib/lock_holder.h b/FreeFileSync/Source/lib/lock_holder.h index dc0dccfc..fc3f7a5c 100755 --- a/FreeFileSync/Source/lib/lock_holder.h +++ b/FreeFileSync/Source/lib/lock_holder.h @@ -11,7 +11,7 @@ namespace zen { //intermediate locks created by DirLock use this extension, too: -const Zchar LOCK_FILE_ENDING[] = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! +const Zchar LOCK_FILE_ENDING[] = Zstr(".ffs_lock"); //don't use Zstring as global constant: avoid static initialization order problem in global namespace! //hold locks for a number of directories without blocking during lock creation //call after having checked directory existence! @@ -20,35 +20,44 @@ class LockHolder public: LockHolder(const std::set& dirpathsExisting, //resolved paths bool& warnDirectoryLockFailed, - ProcessCallback& procCallback) + ProcessCallback& pcb) { - for (const Zstring& dirpath : dirpathsExisting) + class WaitOnLockHandler : public DirLockCallback { - class WaitOnLockHandler : public DirLockCallback - { - public: - WaitOnLockHandler(ProcessCallback& pc) : pc_(pc) {} - void requestUiRefresh() override { pc_.requestUiRefresh(); } //allowed to throw exceptions - void reportStatus(const std::wstring& text) override { pc_.reportStatus(text); } - private: - ProcessCallback& pc_; - } callback(procCallback); + public: + WaitOnLockHandler(ProcessCallback& pc) : pc_(pc) {} + void requestUiRefresh() override { pc_.requestUiRefresh(); } //allowed to throw exceptions + void reportStatus(const std::wstring& text) override { pc_.reportStatus(text); } + private: + ProcessCallback& pc_; + } lcb(pcb); + std::map failedLocks; + + for (const Zstring& dirpath : dirpathsExisting) try { //lock file creation is synchronous and may block noticeably for very slow devices (usb sticks, mapped cloud storages) - lockHolder.emplace_back(appendSeparator(dirpath) + Zstr("sync") + LOCK_FILE_ENDING, &callback); //throw FileError + lockHolder_.emplace_back(appendSeparator(dirpath) + Zstr("sync") + LOCK_FILE_ENDING, &lcb); //throw FileError } - catch (const FileError& e) + catch (const FileError& e) { failedLocks.emplace(dirpath, e); } + + if (!failedLocks.empty()) + { + std::wstring msg = _("Cannot set directory locks for the following folders:"); + + for (const auto& fl : failedLocks) { - const std::wstring msg = replaceCpy(_("Cannot set directory lock for %x."), L"%x", fmtPath(dirpath)) + L"\n\n" + e.toString(); - procCallback.reportWarning(msg, warnDirectoryLockFailed); //may throw! + msg += L"\n\n" + fmtPath(fl.first); + msg += L"\n" + replaceCpy(fl.second.toString(), L"\n\n", L"\n"); } + + pcb.reportWarning(msg, warnDirectoryLockFailed); //may throw! } } private: - std::vector lockHolder; + std::vector lockHolder_; }; } diff --git a/FreeFileSync/Source/lib/process_xml.cpp b/FreeFileSync/Source/lib/process_xml.cpp index f60bf16d..8c9eb0b2 100755 --- a/FreeFileSync/Source/lib/process_xml.cpp +++ b/FreeFileSync/Source/lib/process_xml.cpp @@ -23,7 +23,7 @@ using namespace std::rel_ops; namespace { //------------------------------------------------------------------------------------------------------------------------------- -const int XML_FORMAT_VER_GLOBAL = 5; // +const int XML_FORMAT_VER_GLOBAL = 6; //2018-01-08 const int XML_FORMAT_VER_FFS_GUI = 8; //2017-10-24 const int XML_FORMAT_VER_FFS_BATCH = 8; // //------------------------------------------------------------------------------------------------------------------------------- @@ -486,34 +486,61 @@ bool readText(const std::string& input, ItemPathFormat& value) return true; } +template <> inline +void writeText(const ColumnTypeCfg& value, std::string& output) +{ + switch (value) + { + case ColumnTypeCfg::NAME: + output = "Name"; + break; + case ColumnTypeCfg::LAST_SYNC: + output = "Last"; + break; + } +} + +template <> inline +bool readText(const std::string& input, ColumnTypeCfg& value) +{ + const std::string tmp = trimCpy(input); + if (tmp == "Name") + value = ColumnTypeCfg::NAME; + else if (tmp == "Last") + value = ColumnTypeCfg::LAST_SYNC; + else + return false; + return true; +} + template <> inline -void writeText(const ColumnTypeNavi& value, std::string& output) +void writeText(const ColumnTypeTree& value, std::string& output) { switch (value) { - case ColumnTypeNavi::FOLDER_NAME: + case ColumnTypeTree::FOLDER_NAME: output = "Tree"; break; - case ColumnTypeNavi::ITEM_COUNT: + case ColumnTypeTree::ITEM_COUNT: output = "Count"; break; - case ColumnTypeNavi::BYTES: + case ColumnTypeTree::BYTES: output = "Bytes"; break; } } template <> inline -bool readText(const std::string& input, ColumnTypeNavi& value) +bool readText(const std::string& input, ColumnTypeTree& value) { const std::string tmp = trimCpy(input); if (tmp == "Tree") - value = ColumnTypeNavi::FOLDER_NAME; + value = ColumnTypeTree::FOLDER_NAME; else if (tmp == "Count") - value = ColumnTypeNavi::ITEM_COUNT; + value = ColumnTypeTree::ITEM_COUNT; else if (tmp == "Bytes") - value = ColumnTypeNavi::BYTES; + value = ColumnTypeTree::BYTES; else return false; return true; @@ -666,46 +693,68 @@ bool readText(const std::string& input, DirectionConfig::Variant& value) template <> inline -bool readStruc(const XmlElement& input, ColumnAttributeRim& value) +bool readStruc(const XmlElement& input, ColAttributesRim& value) { XmlIn in(input); - bool rv1 = in.attribute("Type", value.type_); - bool rv2 = in.attribute("Visible", value.visible_); - bool rv3 = in.attribute("Width", value.offset_); //offset == width if stretch is 0 - bool rv4 = in.attribute("Stretch", value.stretch_); + bool rv1 = in.attribute("Type", value.type); + bool rv2 = in.attribute("Visible", value.visible); + bool rv3 = in.attribute("Width", value.offset); //offset == width if stretch is 0 + bool rv4 = in.attribute("Stretch", value.stretch); return rv1 && rv2 && rv3 && rv4; } template <> inline -void writeStruc(const ColumnAttributeRim& value, XmlElement& output) +void writeStruc(const ColAttributesRim& value, XmlElement& output) { XmlOut out(output); - out.attribute("Type", value.type_); - out.attribute("Visible", value.visible_); - out.attribute("Width", value.offset_); - out.attribute("Stretch", value.stretch_); + out.attribute("Type", value.type); + out.attribute("Visible", value.visible); + out.attribute("Width", value.offset); + out.attribute("Stretch", value.stretch); } template <> inline -bool readStruc(const XmlElement& input, ColumnAttributeNavi& value) +bool readStruc(const XmlElement& input, ColAttributesCfg& value) { XmlIn in(input); - bool rv1 = in.attribute("Type", value.type_); - bool rv2 = in.attribute("Visible", value.visible_); - bool rv3 = in.attribute("Width", value.offset_); //offset == width if stretch is 0 - bool rv4 = in.attribute("Stretch", value.stretch_); + bool rv1 = in.attribute("Type", value.type); + bool rv2 = in.attribute("Visible", value.visible); + bool rv3 = in.attribute("Width", value.offset); //offset == width if stretch is 0 + bool rv4 = in.attribute("Stretch", value.stretch); return rv1 && rv2 && rv3 && rv4; } template <> inline -void writeStruc(const ColumnAttributeNavi& value, XmlElement& output) +void writeStruc(const ColAttributesCfg& value, XmlElement& output) { XmlOut out(output); - out.attribute("Type", value.type_); - out.attribute("Visible", value.visible_); - out.attribute("Width", value.offset_); - out.attribute("Stretch", value.stretch_); + out.attribute("Type", value.type); + out.attribute("Visible", value.visible); + out.attribute("Width", value.offset); + out.attribute("Stretch", value.stretch); +} + + +template <> inline +bool readStruc(const XmlElement& input, ColAttributesTree& value) +{ + XmlIn in(input); + bool rv1 = in.attribute("Type", value.type); + bool rv2 = in.attribute("Visible", value.visible); + bool rv3 = in.attribute("Width", value.offset); //offset == width if stretch is 0 + bool rv4 = in.attribute("Stretch", value.stretch); + return rv1 && rv2 && rv3 && rv4; +} + +template <> inline +void writeStruc(const ColAttributesTree& value, XmlElement& output) +{ + XmlOut out(output); + out.attribute("Type", value.type); + out.attribute("Visible", value.visible); + out.attribute("Width", value.offset); + out.attribute("Stretch", value.stretch); } @@ -795,17 +844,26 @@ namespace zen { //FFS portable: use special syntax for config file paths: e.g. "ffs_drive:\SyncJob.ffs_gui" template <> inline -bool readText(const std::string& input, ConfigFileItem& value) +bool readStruc(const XmlElement& input, ConfigFileItem& value) { - value.filePath_ = resolveFreeFileSyncDriveMacro(utfTo(input)); - return true; -} + XmlIn in(input); + + Zstring rawPath; + const bool rv1 = in(rawPath); + if (rv1) + value.filePath = resolveFreeFileSyncDriveMacro(rawPath); + const bool rv2 = in.attribute("LastSync", value.lastSyncTime); + + return rv1 && rv2; +} template <> inline -void writeText(const ConfigFileItem& value, std::string& output) +void writeStruc(const ConfigFileItem& value, XmlElement& output) { - output = utfTo(substituteFreeFileSyncDriveLetter(value.filePath_)); + XmlOut out(output); + out(substituteFreeFileSyncDriveLetter(value.filePath)); + out.attribute("LastSync", value.lastSyncTime); } } @@ -1152,27 +1210,86 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& config, int formatVer) //########################################################### + XmlIn inConfig = inWnd["ConfigPanel"]; + inConfig.attribute("ScrollPos", config.gui.mainDlg.cfgGridTopRowPos); + inConfig.attribute("SyncOverdue", config.gui.mainDlg.cfgGridSyncOverdueDays); + inConfig.attribute("SortByColumn", config.gui.mainDlg.cfgGridLastSortColumn); + inConfig.attribute("SortAscending", config.gui.mainDlg.cfgGridLastSortAscending); + + inConfig["Columns"](config.gui.mainDlg.cfgGridColumnAttribs); + + //TODO: remove parameter migration after some time! 2018-01-08 + if (formatVer < 6) + { + inGui["ConfigHistory"].attribute("MaxSize", config.gui.mainDlg.cfgHistItemsMax); + + std::vector cfgHist; + inGui["ConfigHistory"](cfgHist); + + for (const Zstring& cfgPath : cfgHist) + config.gui.mainDlg.cfgFileHistory.emplace_back(cfgPath, 0); + } + else + { + inConfig["Configurations"].attribute("MaxSize", config.gui.mainDlg.cfgHistItemsMax); + inConfig["Configurations"](config.gui.mainDlg.cfgFileHistory); + } + + //TODO: remove parameter migration after some time! 2018-01-08 + if (formatVer < 6) + { + inGui["LastUsedConfig"](config.gui.mainDlg.lastUsedConfigFiles); + } + else + { + std::vector cfgPaths; + if (inConfig["LastUsed"](cfgPaths)) + { + for (Zstring& filePath : cfgPaths) + filePath = resolveFreeFileSyncDriveMacro(filePath); + + config.gui.mainDlg.lastUsedConfigFiles = cfgPaths; + } + } + + //########################################################### + XmlIn inOverview = inWnd["OverviewPanel"]; - inOverview.attribute("ShowPercentage", config.gui.mainDlg.naviGridShowPercentBar); - inOverview.attribute("SortByColumn", config.gui.mainDlg.naviGridLastSortColumn); - inOverview.attribute("SortAscending", config.gui.mainDlg.naviGridLastSortAscending); + inOverview.attribute("ShowPercentage", config.gui.mainDlg.treeGridShowPercentBar); + inOverview.attribute("SortByColumn", config.gui.mainDlg.treeGridLastSortColumn); + inOverview.attribute("SortAscending", config.gui.mainDlg.treeGridLastSortAscending); //read column attributes - XmlIn inColNavi = inOverview["Columns"]; - inColNavi(config.gui.mainDlg.columnAttribNavi); + XmlIn inColTree = inOverview["Columns"]; + inColTree(config.gui.mainDlg.treeGridColumnAttribs); + + XmlIn inFileGrid = inWnd["FilePanel"]; + //TODO: remove parameter migration after some time! 2018-01-08 + if (formatVer < 6) + inFileGrid = inWnd["CenterPanel"]; + + inFileGrid.attribute("ShowIcons", config.gui.mainDlg.showIcons); + inFileGrid.attribute("IconSize", config.gui.mainDlg.iconSize); + inFileGrid.attribute("SashOffset", config.gui.mainDlg.sashOffset); + inFileGrid.attribute("HistoryMaxSize", config.gui.mainDlg.folderHistItemsMax); - XmlIn inMainGrid = inWnd["CenterPanel"]; - inMainGrid.attribute("ShowIcons", config.gui.mainDlg.showIcons); - inMainGrid.attribute("IconSize", config.gui.mainDlg.iconSize); - inMainGrid.attribute("SashOffset", config.gui.mainDlg.sashOffset); + inFileGrid["ColumnsLeft"].attribute("PathFormat", config.gui.mainDlg.itemPathFormatLeftGrid); + inFileGrid["ColumnsLeft"](config.gui.mainDlg.columnAttribLeft); - XmlIn inColLeft = inMainGrid["ColumnsLeft"]; - inColLeft.attribute("PathFormat", config.gui.mainDlg.itemPathFormatLeftGrid); - inColLeft(config.gui.mainDlg.columnAttribLeft); + inFileGrid["FolderHistoryLeft" ](config.gui.mainDlg.folderHistoryLeft); - XmlIn inColRight = inMainGrid["ColumnsRight"]; - inColRight.attribute("PathFormat", config.gui.mainDlg.itemPathFormatRightGrid); - inColRight(config.gui.mainDlg.columnAttribRight); + inFileGrid["ColumnsRight"].attribute("PathFormat", config.gui.mainDlg.itemPathFormatRightGrid); + inFileGrid["ColumnsRight"](config.gui.mainDlg.columnAttribRight); + + inFileGrid["FolderHistoryRight"](config.gui.mainDlg.folderHistoryRight); + + //TODO: remove parameter migration after some time! 2018-01-08 + if (formatVer < 6) + { + inGui["FolderHistoryLeft" ](config.gui.mainDlg.folderHistoryLeft); + inGui["FolderHistoryRight"](config.gui.mainDlg.folderHistoryRight); + inGui["FolderHistoryLeft"].attribute("MaxSize", config.gui.mainDlg.folderHistItemsMax); + } //########################################################### @@ -1183,31 +1300,20 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& config, int formatVer) inGui["DefaultExclusionFilter"](tmp); config.gui.defaultExclusionFilter = mergeFilterLines(tmp); - //load config file history - inGui["LastUsedConfig"](config.gui.lastUsedConfigFiles); - - inGui["ConfigHistory"](config.gui.cfgFileHistory); - inGui["ConfigHistory"].attribute("MaxSize", config.gui.cfgFileHistMax); - inGui["ConfigHistory"].attribute("ScrollPos", config.gui.cfgFileHistFirstItemPos); - //TODO: remove parameter migration after some time! 2016-09-23 if (formatVer < 4) - config.gui.cfgFileHistMax = std::max(config.gui.cfgFileHistMax, 100); - - inGui["FolderHistoryLeft" ](config.gui.folderHistoryLeft); - inGui["FolderHistoryRight"](config.gui.folderHistoryRight); - inGui["FolderHistoryLeft"].attribute("MaxSize", config.gui.folderHistMax); + config.gui.mainDlg.cfgHistItemsMax = std::max(config.gui.mainDlg.cfgHistItemsMax, 100); //TODO: remove if clause after migration! 2017-10-24 if (formatVer < 5) { inGui["OnCompletionHistory"](config.gui.commandHistory); - inGui["OnCompletionHistory"].attribute("MaxSize", config.gui.commandHistoryMax); + inGui["OnCompletionHistory"].attribute("MaxSize", config.gui.commandHistItemsMax); } else { inGui["CommandHistory"](config.gui.commandHistory); - inGui["CommandHistory"].attribute("MaxSize", config.gui.commandHistoryMax); + inGui["CommandHistory"].attribute("MaxSize", config.gui.commandHistItemsMax); } //external applications @@ -1345,16 +1451,16 @@ XmlCfg parseConfig(const XmlDoc& doc, const Zstring& filepath, int currentXmlFor } -void xmlAccess::readAnyConfig(const std::vector& filepaths, XmlGuiConfig& config, std::wstring& warningMsg) //throw FileError +void xmlAccess::readAnyConfig(const std::vector& filePaths, XmlGuiConfig& config, std::wstring& warningMsg) //throw FileError { - assert(!filepaths.empty()); + assert(!filePaths.empty()); std::vector mainCfgs; - for (auto it = filepaths.begin(); it != filepaths.end(); ++it) + for (auto it = filePaths.begin(); it != filePaths.end(); ++it) { const Zstring& filepath = *it; - const bool firstItem = it == filepaths.begin(); //init all non-"mainCfg" settings with first config file + const bool firstItem = it == filePaths.begin(); //init all non-"mainCfg" settings with first config file XmlDoc doc = loadXmlDocument(filepath); //throw FileError @@ -1604,27 +1710,49 @@ void writeConfig(const XmlGlobalSettings& config, XmlOut& out) //########################################################### + XmlOut outConfig = outWnd["ConfigPanel"]; + outConfig.attribute("ScrollPos", config.gui.mainDlg.cfgGridTopRowPos); + outConfig.attribute("SyncOverdue", config.gui.mainDlg.cfgGridSyncOverdueDays); + outConfig.attribute("SortByColumn", config.gui.mainDlg.cfgGridLastSortColumn); + outConfig.attribute("SortAscending", config.gui.mainDlg.cfgGridLastSortAscending); + + outConfig["Columns"](config.gui.mainDlg.cfgGridColumnAttribs); + outConfig["Configurations"].attribute("MaxSize", config.gui.mainDlg.cfgHistItemsMax); + outConfig["Configurations"](config.gui.mainDlg.cfgFileHistory); + { + std::vector cfgPaths = config.gui.mainDlg.lastUsedConfigFiles; + for (Zstring& filePath : cfgPaths) + filePath = substituteFreeFileSyncDriveLetter(filePath); + + outConfig["LastUsed"](cfgPaths); + } + + //########################################################### + XmlOut outOverview = outWnd["OverviewPanel"]; - outOverview.attribute("ShowPercentage", config.gui.mainDlg.naviGridShowPercentBar); - outOverview.attribute("SortByColumn", config.gui.mainDlg.naviGridLastSortColumn); - outOverview.attribute("SortAscending", config.gui.mainDlg.naviGridLastSortAscending); + outOverview.attribute("ShowPercentage", config.gui.mainDlg.treeGridShowPercentBar); + outOverview.attribute("SortByColumn", config.gui.mainDlg.treeGridLastSortColumn); + outOverview.attribute("SortAscending", config.gui.mainDlg.treeGridLastSortAscending); //write column attributes - XmlOut outColNavi = outOverview["Columns"]; - outColNavi(config.gui.mainDlg.columnAttribNavi); + XmlOut outColTree = outOverview["Columns"]; + outColTree(config.gui.mainDlg.treeGridColumnAttribs); + + XmlOut outFileGrid = outWnd["FilePanel"]; + outFileGrid.attribute("ShowIcons", config.gui.mainDlg.showIcons); + outFileGrid.attribute("IconSize", config.gui.mainDlg.iconSize); + outFileGrid.attribute("SashOffset", config.gui.mainDlg.sashOffset); + outFileGrid.attribute("HistoryMaxSize", config.gui.mainDlg.folderHistItemsMax); - XmlOut outMainGrid = outWnd["CenterPanel"]; - outMainGrid.attribute("ShowIcons", config.gui.mainDlg.showIcons); - outMainGrid.attribute("IconSize", config.gui.mainDlg.iconSize); - outMainGrid.attribute("SashOffset", config.gui.mainDlg.sashOffset); + outFileGrid["ColumnsLeft"].attribute("PathFormat", config.gui.mainDlg.itemPathFormatLeftGrid); + outFileGrid["ColumnsLeft"](config.gui.mainDlg.columnAttribLeft); - XmlOut outColLeft = outMainGrid["ColumnsLeft"]; - outColLeft.attribute("PathFormat", config.gui.mainDlg.itemPathFormatLeftGrid); - outColLeft(config.gui.mainDlg.columnAttribLeft); + outFileGrid["FolderHistoryLeft" ](config.gui.mainDlg.folderHistoryLeft); - XmlOut outColRight = outMainGrid["ColumnsRight"]; - outColRight.attribute("PathFormat", config.gui.mainDlg.itemPathFormatRightGrid); - outColRight(config.gui.mainDlg.columnAttribRight); + outFileGrid["ColumnsRight"].attribute("PathFormat", config.gui.mainDlg.itemPathFormatRightGrid); + outFileGrid["ColumnsRight"](config.gui.mainDlg.columnAttribRight); + + outFileGrid["FolderHistoryRight"](config.gui.mainDlg.folderHistoryRight); //########################################################### @@ -1633,19 +1761,8 @@ void writeConfig(const XmlGlobalSettings& config, XmlOut& out) outGui["DefaultExclusionFilter"](splitFilterByLines(config.gui.defaultExclusionFilter)); - //load config file history - outGui["LastUsedConfig"](config.gui.lastUsedConfigFiles); - - outGui["ConfigHistory" ](config.gui.cfgFileHistory); - outGui["ConfigHistory"].attribute("MaxSize", config.gui.cfgFileHistMax); - outGui["ConfigHistory"].attribute("ScrollPos", config.gui.cfgFileHistFirstItemPos); - - outGui["FolderHistoryLeft" ](config.gui.folderHistoryLeft); - outGui["FolderHistoryRight"](config.gui.folderHistoryRight); - outGui["FolderHistoryLeft" ].attribute("MaxSize", config.gui.folderHistMax); - outGui["CommandHistory"](config.gui.commandHistory); - outGui["CommandHistory"].attribute("MaxSize", config.gui.commandHistoryMax); + outGui["CommandHistory"].attribute("MaxSize", config.gui.commandHistItemsMax); //external applications outGui["ExternalApps"](config.gui.externelApplications); diff --git a/FreeFileSync/Source/lib/process_xml.h b/FreeFileSync/Source/lib/process_xml.h index 1ff7584e..1328ceb0 100755 --- a/FreeFileSync/Source/lib/process_xml.h +++ b/FreeFileSync/Source/lib/process_xml.h @@ -11,7 +11,9 @@ #include #include "localization.h" #include "../structures.h" -#include "../ui/column_attr.h" +#include "../ui/file_grid_attr.h" +#include "../ui/tree_grid_attr.h" //RTS: avoid tree grid's "file_hierarchy.h" dependency! +#include "../ui/cfg_grid.h" namespace xmlAccess @@ -132,9 +134,11 @@ struct ViewFilterDefault struct ConfigFileItem { ConfigFileItem() {} - explicit ConfigFileItem(const Zstring& filePath) : filePath_(filePath) {} - Zstring filePath_; - //add support? -> time_t lastSyncTime + ConfigFileItem(const Zstring& fp, time_t lst) : filePath(fp), lastSyncTime(lst) {} + + Zstring filePath; + time_t lastSyncTime = 0; + //Zstring logFilePath; }; @@ -154,13 +158,13 @@ struct XmlGlobalSettings size_t automaticRetryDelay = 5; //unit: [sec] int fileTimeTolerance = 2; //max. allowed file time deviation; < 0 means unlimited tolerance; default 2s: FAT vs NTFS - int folderAccessTimeout = 20; //unit: [s]; consider CD-ROM insert or hard disk spin up time from sleep + int folderAccessTimeout = 20; //unit: [s]; consider CD-ROM insert or hard disk spin up time from sleep bool runWithBackgroundPriority = false; bool createLockFile = true; bool verifyFileCopy = false; size_t lastSyncsLogFileSizeMax = 100000; //maximum size for LastSyncs.log: use a human-readable number Zstring soundFileCompareFinished; - Zstring soundFileSyncFinished= Zstr("gong.wav"); + Zstring soundFileSyncFinished = Zstr("gong.wav"); OptionalDialogs optDialogs; @@ -186,12 +190,23 @@ struct XmlGlobalSettings bool textSearchRespectCase = false; //good default for Linux, too! int maxFolderPairsVisible = 6; - bool naviGridShowPercentBar = zen::naviGridShowPercentageDefault; //in navigation panel - zen::ColumnTypeNavi naviGridLastSortColumn = zen::naviGridLastSortColumnDefault; //remember sort on navigation panel - bool naviGridLastSortAscending = zen::naviGridLastSortAscendingDefault; // - - std::vector columnAttribNavi = zen::getDefaultColumnAttributesNavi(); //compressed view/navigation - + size_t cfgGridTopRowPos = 0; + int cfgGridSyncOverdueDays = 7; + zen::ColumnTypeCfg cfgGridLastSortColumn = zen::cfgGridLastSortColumnDefault; + bool cfgGridLastSortAscending = zen::getDefaultSortDirection(zen::cfgGridLastSortColumnDefault); + std::vector cfgGridColumnAttribs = zen::getCfgGridDefaultColAttribs(); + size_t cfgHistItemsMax = 100; + std::vector cfgFileHistory; + std::vector lastUsedConfigFiles; + + bool treeGridShowPercentBar = zen::treeGridShowPercentageDefault; + zen::ColumnTypeTree treeGridLastSortColumn = zen::treeGridLastSortColumnDefault; //remember sort on overview panel + bool treeGridLastSortAscending = zen::getDefaultSortDirection(zen::treeGridLastSortColumnDefault); // + std::vector treeGridColumnAttribs = zen::getTreeGridDefaultColAttribs(); + + std::vector folderHistoryLeft; + std::vector folderHistoryRight; + size_t folderHistItemsMax = 15; bool showIcons = true; FileIconSize iconSize = ICON_SIZE_SMALL; int sashOffset = 0; @@ -199,8 +214,8 @@ struct XmlGlobalSettings zen::ItemPathFormat itemPathFormatLeftGrid = zen::defaultItemPathFormatLeftGrid; zen::ItemPathFormat itemPathFormatRightGrid = zen::defaultItemPathFormatRightGrid; - std::vector columnAttribLeft = zen::getDefaultColumnAttributesLeft(); - std::vector columnAttribRight = zen::getDefaultColumnAttributesRight(); + std::vector columnAttribLeft = zen::getFileGridDefaultColAttribsLeft(); + std::vector columnAttribRight = zen::getFileGridDefaultColAttribsRight(); ViewFilterDefault viewFilterDefault; wxString guiPerspectiveLast; //used by wxAuiManager @@ -209,18 +224,8 @@ struct XmlGlobalSettings Zstring defaultExclusionFilter = Zstr("/.Trash-*/") Zstr("\n") Zstr("/.recycle/"); - std::vector lastUsedConfigFiles; - - std::vector cfgFileHistory; - size_t cfgFileHistMax = 100; - int cfgFileHistFirstItemPos = 0; - - std::vector folderHistoryLeft; - std::vector folderHistoryRight; - size_t folderHistMax = 15; - std::vector commandHistory; - size_t commandHistoryMax = 8; + size_t commandHistItemsMax = 8; ExternalApps externelApplications { diff --git a/FreeFileSync/Source/lib/resolve_path.cpp b/FreeFileSync/Source/lib/resolve_path.cpp index 1c67bd78..62d56577 100755 --- a/FreeFileSync/Source/lib/resolve_path.cpp +++ b/FreeFileSync/Source/lib/resolve_path.cpp @@ -150,36 +150,36 @@ namespace //expand volume name if possible, return original input otherwise -Zstring expandVolumeName(const Zstring& text) // [volname]:\folder [volname]\folder [volname]folder -> C:\folder +Zstring expandVolumeName(Zstring pathPhrase) // [volname]:\folder [volname]\folder [volname]folder -> C:\folder { //this would be a nice job for a C++11 regex... //we only expect the [.*] pattern at the beginning => do not touch dir names like "C:\somedir\[stuff]" - const Zstring textTmp = trimCpy(text, true, false); - if (startsWith(textTmp, Zstr("["))) + trim(pathPhrase, true, false); + if (startsWith(pathPhrase, Zstr("["))) { - size_t posEnd = textTmp.find(Zstr("]")); + size_t posEnd = pathPhrase.find(Zstr("]")); if (posEnd != Zstring::npos) { - Zstring volname = Zstring(textTmp.c_str() + 1, posEnd - 1); - Zstring rest = Zstring(textTmp.c_str() + posEnd + 1); - - if (startsWith(rest, Zstr(':'))) - rest = afterFirst(rest, Zstr(':'), IF_MISSING_RETURN_NONE); - if (startsWith(rest, FILE_NAME_SEPARATOR)) - rest = afterFirst(rest, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE); - return "/.../[" + volname + "]/" + rest; + Zstring volName = Zstring(pathPhrase.c_str() + 1, posEnd - 1); + Zstring relPath = Zstring(pathPhrase.c_str() + posEnd + 1); + + if (startsWith(relPath, Zstr(':'))) + relPath = afterFirst(relPath, Zstr(':'), IF_MISSING_RETURN_NONE); + if (startsWith(relPath, FILE_NAME_SEPARATOR)) + relPath = afterFirst(relPath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE); + return "/.../[" + volName + "]/" + relPath; } } - return text; + return pathPhrase; } } -void getDirectoryAliasesRecursive(const Zstring& dirPath, std::set& output) +void getDirectoryAliasesRecursive(const Zstring& pathPhrase, std::set& output) { - //3. environment variables: C:\Users\ -> %UserProfile%, C:\Users\%UserName% + //3. environment variables: C:\Users\ -> %UserProfile% { std::vector> macroList; @@ -197,16 +197,16 @@ void getDirectoryAliasesRecursive(const Zstring& dirPath, std::set C:\Users\ { - const Zstring pathExp = expandMacros(dirPath); - if (pathExp != dirPath) + const Zstring pathExp = expandMacros(pathPhrase); + if (pathExp != pathPhrase) if (output.insert(pathExp).second) getDirectoryAliasesRecursive(pathExp, output); //recurse! } @@ -217,7 +217,7 @@ std::vector zen::getDirectoryAliases(const Zstring& folderPathPhrase) { const Zstring dirPath = trimCpy(folderPathPhrase, true, false); if (dirPath.empty()) - return std::vector(); + return {}; std::set tmp; getDirectoryAliasesRecursive(dirPath, tmp); @@ -225,7 +225,7 @@ std::vector zen::getDirectoryAliases(const Zstring& folderPathPhrase) tmp.erase(dirPath); tmp.erase(Zstring()); - return std::vector(tmp.begin(), tmp.end()); + return { tmp.begin(), tmp.end() }; } @@ -261,12 +261,11 @@ Zstring zen::getResolvedFilePath(const Zstring& pathPhrase) //noexcept //remove trailing slash, unless volume root: if (Opt pc = parsePathComponents(path)) { - //keep this brace for GCC: -Wparentheses if (pc->relPath.empty()) path = pc->rootPath; else path = appendSeparator(pc->rootPath) + pc->relPath; - } + } //keep this brace for GCC: -Wparentheses return path; } diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp index 80a03bfb..814d23c8 100755 --- a/FreeFileSync/Source/ui/batch_config.cpp +++ b/FreeFileSync/Source/ui/batch_config.cpp @@ -47,6 +47,8 @@ private: void OnToggleGenerateLogfile(wxCommandEvent& event) override { updateGui(); } void OnToggleLogfilesLimit (wxCommandEvent& event) override { updateGui(); } + void onLocalKeyEvent(wxKeyEvent& event); + void updateGui(); //re-evaluate gui after config changes void setConfig(const BatchDialogConfig& batchCfg); @@ -82,6 +84,9 @@ BatchDialog::BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg) : setConfig(dlgCfg); + //enable dialog-specific key local events + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(BatchDialog::onLocalKeyEvent), nullptr, this); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! Center(); //needs to be re-applied after a dialog size change! @@ -161,6 +166,12 @@ BatchDialogConfig BatchDialog::getConfig() const } +void BatchDialog::onLocalKeyEvent(wxKeyEvent& event) +{ + event.Skip(); +} + + void BatchDialog::OnSaveBatchJob(wxCommandEvent& event) { dlgCfgOut_ = getConfig(); diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp index 50945143..e3e59690 100755 --- a/FreeFileSync/Source/ui/batch_status_handler.cpp +++ b/FreeFileSync/Source/ui/batch_status_handler.cpp @@ -308,6 +308,7 @@ BatchStatusHandler::~BatchStatusHandler() { //post sync action bool showSummary = true; + bool triggerSleep = false; if (!getAbortStatus() || *getAbortStatus() != AbortTrigger::USER) //user cancelled => don't run post sync action! switch (progressDlg_->getOptionPostSyncAction()) { @@ -317,11 +318,7 @@ BatchStatusHandler::~BatchStatusHandler() showSummary = false; break; case PostSyncAction::SLEEP: - try - { - tryReportingError([&] { suspendSystem(); /*throw FileError*/ }, *this); //throw X - } - catch (...) {} + triggerSleep = true; break; case PostSyncAction::SHUTDOWN: showSummary = false; @@ -343,6 +340,13 @@ BatchStatusHandler::~BatchStatusHandler() else progressDlg_->closeDirectly(true /*restoreParentFrame: n/a here*/); //progressDlg_ is main window => program will quit shortly after + if (triggerSleep) //sleep *after* showing results dialog (consider total time!) + try + { + tryReportingError([&] { suspendSystem(); /*throw FileError*/ }, *this); //throw X + } + catch (...) {} + //wait until progress dialog notified shutdown via onProgressDialogTerminate() //-> required since it has our "this" pointer captured in lambda "notifyWindowTerminate"! //-> nicely manages dialog lifetime diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp new file mode 100755 index 00000000..3d6cc451 --- /dev/null +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -0,0 +1,387 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "cfg_grid.h" +#include +#include +#include +#include +#include +#include +#include "../lib/icon_buffer.h" +#include "../lib/ffs_paths.h" + +using namespace zen; + + +Zstring zen::getLastRunConfigPath() +{ + return zen::getConfigDirPathPf() + Zstr("LastRun.ffs_gui"); +} + + +void ConfigView::addCfgFiles(const std::vector& filePaths) +{ + //determine highest "last use" index number of m_listBoxHistory + int lastUseIndexMax = 0; + for (const auto& item : cfgList_) + lastUseIndexMax = std::max(lastUseIndexMax, item.second.lastUseIndex); + + for (const Zstring& filePath : filePaths) + { + auto it = cfgList_.find(filePath); + if (it == cfgList_.end()) + { + Details detail = {}; + detail.filePath = filePath; + detail.lastUseIndex = ++lastUseIndexMax; + + std::tie(detail.name, detail.cfgType, detail.isLastRunCfg) = [&] + { + if (equalFilePath(filePath, lastRunConfigPath_)) + return std::make_tuple(utfTo(L"<" + _("Last session") + L">"), Details::CFG_TYPE_GUI, true); + + const Zstring fileName = afterLast(filePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); + + if (endsWith(fileName, Zstr(".ffs_gui"), CmpFilePath())) + return std::make_tuple(beforeLast(fileName, Zstr('.'), IF_MISSING_RETURN_NONE), Details::CFG_TYPE_GUI, false); + else if (endsWith(fileName, Zstr(".ffs_batch"), CmpFilePath())) + return std::make_tuple(beforeLast(fileName, Zstr('.'), IF_MISSING_RETURN_NONE), Details::CFG_TYPE_BATCH, false); + else + return std::make_tuple(fileName, Details::CFG_TYPE_NONE, false); + }(); + + auto itNew = cfgList_.emplace_hint(cfgList_.end(), filePath, std::move(detail)); + cfgListView_.push_back(itNew); + } + else + it->second.lastUseIndex = ++lastUseIndexMax; + } + + sortListView(); +} + + +void ConfigView::removeItems(const std::vector& filePaths) +{ + const std::set pathsSorted(filePaths.begin(), filePaths.end()); + + erase_if(cfgListView_, [&](auto it) { return pathsSorted.find(it->first) != pathsSorted.end(); }); + + for (const Zstring& filePath : filePaths) + cfgList_.erase(filePath); + + assert(cfgList_.size() == cfgListView_.size()); +} + + +void ConfigView::setLastSyncTime(const std::vector>& syncTimes) +{ + for (const auto& st : syncTimes) + { + auto it = cfgList_.find(st.first); + if (it != cfgList_.end()) + it->second.lastSyncTime = st.second; + } + sortListView(); //needed if sorted by last sync time +} + + +const ConfigView::Details* ConfigView::getItem(size_t row) const +{ + if (row < cfgListView_.size()) + return &cfgListView_[row]->second; + return nullptr; +} + + +void ConfigView::setSortDirection(ColumnTypeCfg colType, bool ascending) +{ + sortColumn_ = colType; + sortAscending_ = ascending; + + sortListView(); +} + + +template +void ConfigView::sortListViewImpl() +{ + const auto lessCfgName = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) + { + if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) + return lhs->second.isLastRunCfg > rhs->second.isLastRunCfg; //"last session" label should be at top position! + + return LessNaturalSort()(lhs->second.name, rhs->second.name); + }; + + const auto lessLastSync = [](CfgFileList::iterator lhs, CfgFileList::iterator rhs) + { + if (lhs->second.isLastRunCfg != rhs->second.isLastRunCfg) + return lhs->second.isLastRunCfg < rhs->second.isLastRunCfg; //"last session" label should be (always) last + + return makeSortDirection(std::greater<>(), Int2Type())(lhs->second.lastSyncTime, rhs->second.lastSyncTime); + //[!] ascending LAST_SYNC shows lowest "days past" first <=> highest lastSyncTime first + }; + + switch (sortColumn_) + { + case ColumnTypeCfg::NAME: + std::sort(cfgListView_.begin(), cfgListView_.end(), makeSortDirection(lessCfgName, Int2Type())); + break; + case ColumnTypeCfg::LAST_SYNC: + std::sort(cfgListView_.begin(), cfgListView_.end(), lessLastSync); + break; + } +} + + +void ConfigView::sortListView() +{ + if (sortAscending_) + sortListViewImpl(); + else + sortListViewImpl(); +} + +//------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------- + +namespace +{ +class GridDataCfg : public GridData +{ +public: + GridDataCfg(int fileIconSize) : fileIconSize_(fileIconSize) {} + + ConfigView& getDataView() { return cfgView_; } + + static int getRowDefaultHeight(const Grid& grid) + { + return grid.getMainWin().GetCharHeight(); + } + + int getSyncOverdueDays() const { return syncOverdueDays_; } + void setSyncOverdueDays(int syncOverdueDays) { syncOverdueDays_ = syncOverdueDays; } + +private: + size_t getRowCount() const override { return cfgView_.getRowCount(); } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const ConfigView::Details* item = cfgView_.getItem(row)) + switch (static_cast(colType)) + { + case ColumnTypeCfg::NAME: + return utfTo(item->name); + + case ColumnTypeCfg::LAST_SYNC: + { + if (item->isLastRunCfg) + return std::wstring(); + + if (item->lastSyncTime == 0) + return std::wstring(1, EN_DASH); + + const int daysPast = numeric::round((std::time(nullptr) - item->lastSyncTime) / (24.0 * 3600)); + if (daysPast == 0) + return _("Today"); + + return _P("1 day", "%x days", daysPast); + } + //return formatTime(FORMAT_DATE_TIME, getLocalTime(item->lastSyncTime)); + } + return std::wstring(); + } + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + wxRect rectTmp = rect; + + wxDCTextColourChanger dummy(dc); //accessibility: always set both foreground AND background colors! + if (selected) + dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT)); + else + { + if (enabled) + dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + else + dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + } + + if (const ConfigView::Details* item = cfgView_.getItem(row)) + switch (static_cast(colType)) + { + case ColumnTypeCfg::NAME: + rectTmp.x += COLUMN_GAP_LEFT; + rectTmp.width -= COLUMN_GAP_LEFT; + + switch (item->cfgType) + { + case ConfigView::Details::CFG_TYPE_NONE: + break; + case ConfigView::Details::CFG_TYPE_GUI: + drawBitmapRtlNoMirror(dc, enabled ? syncIconSmall_ : syncIconSmall_.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + break; + case ConfigView::Details::CFG_TYPE_BATCH: + drawBitmapRtlNoMirror(dc, enabled ? batchIconSmall_ : batchIconSmall_.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + break; + } + rectTmp.x += fileIconSize_ + COLUMN_GAP_LEFT; + rectTmp.width -= fileIconSize_ + COLUMN_GAP_LEFT; + + drawCellText(dc, rectTmp, getValue(row, colType)); + break; + + case ColumnTypeCfg::LAST_SYNC: + { + wxDCTextColourChanger dummy2(dc); + if (syncOverdueDays_ > 0) + { + const int daysPast = numeric::round((std::time(nullptr) - item->lastSyncTime) / (24.0 * 3600)); + if (daysPast >= syncOverdueDays_) + dummy2.Set(*wxRED); + } + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER); + } + break; + } + } + + int getBestSize(wxDC& dc, size_t row, ColumnType colType) override + { + // -> synchronize renderCell() <-> getBestSize() + + switch (static_cast(colType)) + { + case ColumnTypeCfg::NAME: + return COLUMN_GAP_LEFT + fileIconSize_ + COLUMN_GAP_LEFT + dc.GetTextExtent(getValue(row, colType)).GetWidth() + COLUMN_GAP_LEFT; + + case ColumnTypeCfg::LAST_SYNC: + return COLUMN_GAP_LEFT + dc.GetTextExtent(getValue(row, colType)).GetWidth() + COLUMN_GAP_LEFT; + } + return 0; + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override + { + if (selected) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + } + + void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); + + rectInside.x += COLUMN_GAP_LEFT; + rectInside.width -= COLUMN_GAP_LEFT; + drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); + + auto sortInfo = cfgView_.getSortDirection(); + if (colType == static_cast(sortInfo.first)) + { + const wxBitmap& marker = getResourceImage(sortInfo.second ? L"sortAscending" : L"sortDescending"); + drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL); + } + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeCfg::NAME: + return _("Name"); + case ColumnTypeCfg::LAST_SYNC: + return _("Last sync"); + } + return std::wstring(); + } + +private: + ConfigView cfgView_; + int syncOverdueDays_ = 0; + const int fileIconSize_; + const wxBitmap syncIconSmall_ = getResourceImage(L"sync" ).ConvertToImage().Scale(fileIconSize_, fileIconSize_, wxIMAGE_QUALITY_BILINEAR); //looks sharper than wxIMAGE_QUALITY_HIGH! + const wxBitmap batchIconSmall_ = getResourceImage(L"batch").ConvertToImage().Scale(fileIconSize_, fileIconSize_, wxIMAGE_QUALITY_BILINEAR); +}; +} + + +void cfggrid::init(Grid& grid) +{ + const int rowHeight = GridDataCfg::getRowDefaultHeight(grid); + + auto prov = std::make_shared(rowHeight /*fileIconSize*/); + + grid.setDataProvider(prov); + grid.showRowLabel(false); + grid.setRowHeight(rowHeight); + + grid.setColumnLabelHeight(rowHeight + 2); +} + + +ConfigView& cfggrid::getDataView(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getDataView(); + throw std::runtime_error("cfggrid was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); +} + + +void cfggrid::addAndSelect(Grid& grid, const std::vector& filePaths, bool scrollToSelection) +{ + auto* prov = dynamic_cast(grid.getDataProvider()); + if (!prov) + throw std::runtime_error("cfggrid was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); + + prov->getDataView().addCfgFiles(filePaths); + grid.Refresh(); //[!] let Grid know about changed row count *before* fiddling with selection!!! + + grid.clearSelection(GridEventPolicy::DENY_GRID_EVENT); + + const std::set pathsSorted(filePaths.begin(), filePaths.end()); + ptrdiff_t selectionTopRow = -1; + + for (size_t i = 0; i < grid.getRowCount(); ++i) + if (const ConfigView::Details* cfg = prov->getDataView().getItem(i)) + { + if (pathsSorted.find(cfg->filePath) != pathsSorted.end()) + { + if (selectionTopRow < 0) + selectionTopRow = i; + + grid.selectRow(i, GridEventPolicy::DENY_GRID_EVENT); + } + } + else + assert(false); + + if (scrollToSelection && selectionTopRow >= 0) + grid.makeRowVisible(selectionTopRow); +} + + +int cfggrid::getSyncOverdueDays(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getSyncOverdueDays(); + throw std::runtime_error("cfggrid was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); +} + + +void cfggrid::setSyncOverdueDays(Grid& grid, int syncOverdueDays) +{ + auto* prov = dynamic_cast(grid.getDataProvider()); + if (!prov) + throw std::runtime_error("cfggrid was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); + + prov->setSyncOverdueDays(syncOverdueDays); + grid.Refresh(); +} diff --git a/FreeFileSync/Source/ui/cfg_grid.h b/FreeFileSync/Source/ui/cfg_grid.h new file mode 100755 index 00000000..8a898aa7 --- /dev/null +++ b/FreeFileSync/Source/ui/cfg_grid.h @@ -0,0 +1,122 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef CONFIG_HISTORY_3248789479826359832 +#define CONFIG_HISTORY_3248789479826359832 + +#include +#include + +namespace zen +{ +enum class ColumnTypeCfg +{ + NAME, + LAST_SYNC, +}; + + +struct ColAttributesCfg +{ + ColumnTypeCfg type = ColumnTypeCfg::NAME; + int offset = 0; + int stretch = 0; + bool visible = false; +}; + +inline +std::vector getCfgGridDefaultColAttribs() +{ + return + { + { ColumnTypeCfg::NAME, -75, 1, true }, + { ColumnTypeCfg::LAST_SYNC, 75, 0, true }, + }; +} + +const ColumnTypeCfg cfgGridLastSortColumnDefault = ColumnTypeCfg::NAME; + +inline +bool getDefaultSortDirection(ColumnTypeCfg colType) +{ + switch (colType) + { + case ColumnTypeCfg::NAME: + return true; + case ColumnTypeCfg::LAST_SYNC: //actual sort order is "time since last sync" + return false; + } + assert(false); + return true; +} +//--------------------------------------------------------------------------------------------------------------------- +Zstring getLastRunConfigPath(); + + +class ConfigView +{ +public: + ConfigView() {} + + void addCfgFiles(const std::vector& filePaths); + void removeItems(const std::vector& filePaths); + + void setLastSyncTime(const std::vector>& syncTimes); + + struct Details + { + Zstring filePath; + Zstring name; + time_t lastSyncTime = 0; + int lastUseIndex = 0; //support truncating the config list size via last usage, the higher the index the more recent the usage + bool isLastRunCfg = false; //LastRun.ffs_gui + + enum ConfigType + { + CFG_TYPE_NONE, + CFG_TYPE_GUI, + CFG_TYPE_BATCH, + } cfgType = CFG_TYPE_NONE; + }; + + const Details* getItem(size_t row) const; + size_t getRowCount() const { assert(cfgList_.size() == cfgListView_.size()); return cfgListView_.size(); } + + void setSortDirection(ColumnTypeCfg colType, bool ascending); + std::pair getSortDirection() { return std::make_pair(sortColumn_, sortAscending_); } + +private: + ConfigView (const ConfigView&) = delete; + ConfigView& operator=(const ConfigView&) = delete; + + void sortListView(); + template void sortListViewImpl(); + + const Zstring lastRunConfigPath_ = getLastRunConfigPath(); //let's not use another static... + + using CfgFileList = std::map; + + CfgFileList cfgList_; + std::vector cfgListView_; //sorted view on cfgList_ + + ColumnTypeCfg sortColumn_ = cfgGridLastSortColumnDefault; + bool sortAscending_ = getDefaultSortDirection(cfgGridLastSortColumnDefault); +}; + + +namespace cfggrid +{ +void init(Grid& grid); +ConfigView& getDataView(Grid& grid); //grid.Refresh() after making changes! + +void addAndSelect(Grid& grid, const std::vector& filePaths, bool scrollToSelection); + +int getSyncOverdueDays(Grid& grid); +void setSyncOverdueDays(Grid& grid, int syncOverdueDays); +} +} + +#endif //CONFIG_HISTORY_3248789479826359832 diff --git a/FreeFileSync/Source/ui/column_attr.h b/FreeFileSync/Source/ui/column_attr.h deleted file mode 100755 index 25a96736..00000000 --- a/FreeFileSync/Source/ui/column_attr.h +++ /dev/null @@ -1,108 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#ifndef COLUMN_ATTR_H_189467891346732143213 -#define COLUMN_ATTR_H_189467891346732143213 - -#include - - -namespace zen -{ -enum class ColumnTypeRim -{ - ITEM_PATH, - SIZE, - DATE, - EXTENSION, -}; - -struct ColumnAttributeRim -{ - ColumnAttributeRim() {} - ColumnAttributeRim(ColumnTypeRim type, int offset, int stretch, bool visible) : type_(type), offset_(offset), stretch_(stretch), visible_(visible) {} - - ColumnTypeRim type_ = ColumnTypeRim::ITEM_PATH; - int offset_ = 0; - int stretch_ = 0;; - bool visible_ = false; -}; - -inline -std::vector getDefaultColumnAttributesLeft() -{ - return //harmonize with main_dlg.cpp::onGridLabelContextRim() => expects stretched ITEM_PATH and non-stretched other columns! - { - { ColumnTypeRim::ITEM_PATH, -100, 1, true }, - { ColumnTypeRim::EXTENSION, 60, 0, false }, - { ColumnTypeRim::DATE, 140, 0, false }, - { ColumnTypeRim::SIZE, 100, 0, true }, - }; -} - -inline -std::vector getDefaultColumnAttributesRight() -{ - return getDefaultColumnAttributesLeft(); //*currently* same default -} - -enum class ItemPathFormat -{ - FULL_PATH, - RELATIVE_PATH, - ITEM_NAME, -}; - -const ItemPathFormat defaultItemPathFormatLeftGrid = ItemPathFormat::RELATIVE_PATH; -const ItemPathFormat defaultItemPathFormatRightGrid = ItemPathFormat::RELATIVE_PATH; - -//------------------------------------------------------------------ - -enum class ColumnTypeCenter -{ - CHECKBOX, - CMP_CATEGORY, - SYNC_ACTION, -}; - -//------------------------------------------------------------------ - -enum class ColumnTypeNavi -{ - FOLDER_NAME, - ITEM_COUNT, - BYTES, -}; - -struct ColumnAttributeNavi -{ - ColumnAttributeNavi() {} - ColumnAttributeNavi(ColumnTypeNavi type, int offset, int stretch, bool visible) : type_(type), offset_(offset), stretch_(stretch), visible_(visible) {} - - ColumnTypeNavi type_ = ColumnTypeNavi::FOLDER_NAME; - int offset_ = 0; - int stretch_ = 0;; - bool visible_ = false; -}; - - -inline -std::vector getDefaultColumnAttributesNavi() -{ - return //harmonize with tree_view.cpp::onGridLabelContext() => expects stretched FOLDER_NAME and non-stretched other columns! - { - { ColumnTypeNavi::FOLDER_NAME, -120, 1, true }, //stretch to full width and substract sum of fixed size widths - { ColumnTypeNavi::ITEM_COUNT, 60, 0, true }, - { ColumnTypeNavi::BYTES, 60, 0, true }, //GTK needs a few pixels more width - }; -} - -const bool naviGridShowPercentageDefault = true; -const ColumnTypeNavi naviGridLastSortColumnDefault = ColumnTypeNavi::BYTES; //remember sort on navigation panel -const bool naviGridLastSortAscendingDefault = false; // -} - -#endif //COLUMN_ATTR_H_189467891346732143213 diff --git a/FreeFileSync/Source/ui/custom_grid.cpp b/FreeFileSync/Source/ui/custom_grid.cpp deleted file mode 100755 index bcc97859..00000000 --- a/FreeFileSync/Source/ui/custom_grid.cpp +++ /dev/null @@ -1,1842 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#include "custom_grid.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../file_hierarchy.h" - -using namespace zen; -using namespace gridview; - - -const wxEventType zen::EVENT_GRID_CHECK_ROWS = wxNewEventType(); -const wxEventType zen::EVENT_GRID_SYNC_DIRECTION = wxNewEventType(); - -namespace -{ -//let's NOT create wxWidgets objects statically: -inline wxColor getColorOrange () { return { 238, 201, 0 }; } -inline wxColor getColorGrey () { return { 212, 208, 200 }; } -inline wxColor getColorYellow () { return { 247, 252, 62 }; } -//inline wxColor getColorYellowLight() { return { 253, 252, 169 }; } -inline wxColor getColorCmpRed () { return { 255, 185, 187 }; } -inline wxColor getColorSyncBlue () { return { 185, 188, 255 }; } -inline wxColor getColorSyncGreen() { return { 196, 255, 185 }; } -inline wxColor getColorNotActive() { return { 228, 228, 228 }; } //light grey -inline wxColor getColorGridLine () { return { 192, 192, 192 }; } //light grey - -const size_t ROW_COUNT_IF_NO_DATA = 0; - -/* -class hierarchy: - GridDataBase - /|\ - ________________|________________ - | | - GridDataRim | - /|\ | - __________|__________ | - | | | - GridDataLeft GridDataRight GridDataCenter -*/ - -std::pair getVisibleRows(const Grid& grid) //returns range [from, to) -{ - const wxSize clientSize = grid.getMainWin().GetClientSize(); - if (clientSize.GetHeight() > 0) - { - const wxPoint topLeft = grid.CalcUnscrolledPosition(wxPoint(0, 0)); - const wxPoint bottom = grid.CalcUnscrolledPosition(wxPoint(0, clientSize.GetHeight() - 1)); - - const ptrdiff_t rowCount = grid.getRowCount(); - const ptrdiff_t rowFrom = grid.getRowAtPos(topLeft.y); //return -1 for invalid position, rowCount if out of range - const ptrdiff_t rowTo = grid.getRowAtPos(bottom.y); - if (rowFrom >= 0 && rowTo >= 0) - return std::make_pair(rowFrom, std::min(rowTo + 1, rowCount)); - } - assert(false); - return std::make_pair(0, 0); -} - - -void fillBackgroundDefaultColorAlternating(wxDC& dc, const wxRect& rect, bool evenRowNumber) -{ - //alternate background color to improve readability (while lacking cell borders) - if (!evenRowNumber) - { - //accessibility, support high-contrast schemes => work with user-defined background color! - const auto backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - - auto incChannel = [](unsigned char c, int diff) { return static_cast(std::max(0, std::min(255, c + diff))); }; - - auto getAdjustedColor = [&](int diff) - { - return wxColor(incChannel(backCol.Red (), diff), - incChannel(backCol.Green(), diff), - incChannel(backCol.Blue (), diff)); - }; - - auto colorDist = [](const wxColor& lhs, const wxColor& rhs) //just some metric - { - return numeric::power<2>(static_cast(lhs.Red ()) - static_cast(rhs.Red ())) + - numeric::power<2>(static_cast(lhs.Green()) - static_cast(rhs.Green())) + - numeric::power<2>(static_cast(lhs.Blue ()) - static_cast(rhs.Blue ())); - }; - - const int signLevel = colorDist(backCol, *wxBLACK) < colorDist(backCol, *wxWHITE) ? 1 : -1; //brighten or darken - - const wxColor colOutter = getAdjustedColor(signLevel * 14); //just some very faint gradient to avoid visual distraction - const wxColor colInner = getAdjustedColor(signLevel * 11); // - - //clearArea(dc, rect, backColAlt); - - //add some nice background gradient - wxRect rectUpper = rect; - rectUpper.height /= 2; - wxRect rectLower = rect; - rectLower.y += rectUpper.height; - rectLower.height -= rectUpper.height; - dc.GradientFillLinear(rectUpper, colOutter, colInner, wxSOUTH); - dc.GradientFillLinear(rectLower, colOutter, colInner, wxNORTH); - } - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -} - - -class IconUpdater; -class GridEventManager; -class GridDataLeft; -class GridDataRight; - -struct IconManager -{ - IconManager(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer::IconSize sz) : - iconBuffer(sz), - dirIcon (IconBuffer::genericDirIcon (sz)), - linkOverlayIcon(IconBuffer::linkOverlayIcon(sz)), - iconUpdater(std::make_unique(provLeft, provRight, iconBuffer)) {} - - void startIconUpdater(); - IconBuffer& refIconBuffer() { return iconBuffer; } - - const wxBitmap& getGenericDirIcon () const { return dirIcon; } - const wxBitmap& getLinkOverlayIcon() const { return linkOverlayIcon; } - -private: - IconBuffer iconBuffer; - const wxBitmap dirIcon; - const wxBitmap linkOverlayIcon; - - std::unique_ptr iconUpdater; //bind ownership to GridDataRim<>! -}; - -//######################################################################################################## - -class GridDataBase : public GridData -{ -public: - GridDataBase(Grid& grid, const std::shared_ptr& gridDataView) : grid_(grid), gridDataView_(gridDataView) {} - - void holdOwnership(const std::shared_ptr& evtMgr) { evtMgr_ = evtMgr; } - GridEventManager* getEventManager() { return evtMgr_.get(); } - -protected: - Grid& refGrid() { return grid_; } - const Grid& refGrid() const { return grid_; } - - const GridView* getGridDataView() const { return gridDataView_.get(); } - - const FileSystemObject* getRawData(size_t row) const - { - if (auto view = getGridDataView()) - return view->getObject(row); - return nullptr; - } - -private: - size_t getRowCount() const override - { - if (!gridDataView_ || gridDataView_->rowsTotal() == 0) - return ROW_COUNT_IF_NO_DATA; - - return gridDataView_->rowsOnView(); - //return std::max(MIN_ROW_COUNT, gridDataView_ ? gridDataView_->rowsOnView() : 0); - } - - std::shared_ptr evtMgr_; - Grid& grid_; - std::shared_ptr gridDataView_; -}; - -//######################################################################################################## - -template -class GridDataRim : public GridDataBase -{ -public: - GridDataRim(const std::shared_ptr& gridDataView, Grid& grid) : GridDataBase(grid, gridDataView) {} - - void setIconManager(const std::shared_ptr& iconMgr) { iconMgr_ = iconMgr; } - - void setItemPathForm(ItemPathFormat fmt) { itemPathFormat = fmt; } - - void getUnbufferedIconsForPreload(std::vector>& newLoad) //return (priority, filepath) list - { - if (iconMgr_) - { - const auto& rowsOnScreen = getVisibleRows(refGrid()); - const ptrdiff_t visibleRowCount = rowsOnScreen.second - rowsOnScreen.first; - - //preload icons not yet on screen: - const int preloadSize = 2 * std::max(20, visibleRowCount); //:= sum of lines above and below of visible range to preload - //=> use full visible height to handle "next page" command and a minimum of 20 for excessive mouse wheel scrolls - - for (ptrdiff_t i = 0; i < preloadSize; ++i) - { - const ptrdiff_t currentRow = rowsOnScreen.first - (preloadSize + 1) / 2 + getAlternatingPos(i, visibleRowCount + preloadSize); //for odd preloadSize start one row earlier - - const IconInfo ii = getIconInfo(currentRow); - if (ii.type == IconInfo::ICON_PATH) - if (!iconMgr_->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath())) - newLoad.emplace_back(i, ii.fsObj->template getAbstractPath()); //insert least-important items on outer rim first - } - } - } - - void updateNewAndGetUnbufferedIcons(std::vector& newLoad) //loads all not yet drawn icons - { - if (iconMgr_) - { - const auto& rowsOnScreen = getVisibleRows(refGrid()); - const ptrdiff_t visibleRowCount = rowsOnScreen.second - rowsOnScreen.first; - - //loop over all visible rows - for (ptrdiff_t i = 0; i < visibleRowCount; ++i) - { - //alternate when adding rows: first, last, first + 1, last - 1 ... - const ptrdiff_t currentRow = rowsOnScreen.first + getAlternatingPos(i, visibleRowCount); - - if (isFailedLoad(currentRow)) //find failed attempts to load icon - { - const IconInfo ii = getIconInfo(currentRow); - if (ii.type == IconInfo::ICON_PATH) - { - //test if they are already loaded in buffer: - if (iconMgr_->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath())) - { - //do a *full* refresh for *every* failed load to update partial DC updates while scrolling - refGrid().refreshCell(currentRow, static_cast(ColumnTypeRim::ITEM_PATH)); - setFailedLoad(currentRow, false); - } - else //not yet in buffer: mark for async. loading - newLoad.push_back(ii.fsObj->template getAbstractPath()); - } - } - } - } - } - -private: - bool isFailedLoad(size_t row) const { return row < failedLoads.size() ? failedLoads[row] != 0 : false; } - - void setFailedLoad(size_t row, bool failed = true) - { - if (failedLoads.size() != refGrid().getRowCount()) - failedLoads.resize(refGrid().getRowCount()); - - if (row < failedLoads.size()) - failedLoads[row] = failed; - } - - //icon buffer will load reversely, i.e. if we want to go from inside out, we need to start from outside in - static size_t getAlternatingPos(size_t pos, size_t total) - { - assert(pos < total); - return pos % 2 == 0 ? pos / 2 : total - 1 - pos / 2; - } - -protected: - void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override - { - if (enabled) - { - if (selected) - dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST); - //ignore focus - else - { - //alternate background color to improve readability (while lacking cell borders) - if (getRowDisplayType(row) == DisplayType::NORMAL) - fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); - else - clearArea(dc, rect, getBackGroundColor(row)); - - //draw horizontal border if required - DisplayType dispTp = getRowDisplayType(row); - if (dispTp != DisplayType::NORMAL && - dispTp == getRowDisplayType(row + 1)) - { - wxDCPenChanger dummy2(dc, getColorGridLine()); - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); - } - } - } - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - } - - wxColor getBackGroundColor(size_t row) const - { - //accessibility: always set both foreground AND background colors! - // => harmonize with renderCell()! - - switch (getRowDisplayType(row)) - { - case DisplayType::NORMAL: - break; - case DisplayType::FOLDER: - return getColorGrey(); - case DisplayType::SYMLINK: - return getColorOrange(); - case DisplayType::INACTIVE: - return getColorNotActive(); - } - return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - } - -private: - enum class DisplayType - { - NORMAL, - FOLDER, - SYMLINK, - INACTIVE, - }; - - DisplayType getRowDisplayType(size_t row) const - { - const FileSystemObject* fsObj = getRawData(row); - if (!fsObj ) - return DisplayType::NORMAL; - - //mark filtered rows - if (!fsObj->isActive()) - return DisplayType::INACTIVE; - - if (fsObj->isEmpty()) //always show not existing files/dirs/symlinks as empty - return DisplayType::NORMAL; - - DisplayType output = DisplayType::NORMAL; - //mark directories and symlinks - visitFSObject(*fsObj, [&](const FolderPair& folder) { output = DisplayType::FOLDER; }, - [](const FilePair& file) {}, - [&](const SymlinkPair& symlink) { output = DisplayType::SYMLINK; }); - - return output; - } - - std::wstring getValue(size_t row, ColumnType colType) const override - { - if (const FileSystemObject* fsObj = getRawData(row)) - { - const ColumnTypeRim colTypeRim = static_cast(colType); - - std::wstring value; - visitFSObject(*fsObj, [&](const FolderPair& folder) - { - value = [&] - { - if (folder.isEmpty()) - return std::wstring(); - - switch (colTypeRim) - { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat) - { - case ItemPathFormat::FULL_PATH: - return AFS::getDisplayPath(folder.getAbstractPath()); - case ItemPathFormat::RELATIVE_PATH: - return utfTo(folder.getRelativePath()); - case ItemPathFormat::ITEM_NAME: - return utfTo(folder.getItemName()); - } - break; - case ColumnTypeRim::SIZE: - return L"<" + _("Folder") + L">"; - case ColumnTypeRim::DATE: - return std::wstring(); - case ColumnTypeRim::EXTENSION: - return std::wstring(); - } - assert(false); - return std::wstring(); - }(); - }, - - [&](const FilePair& file) - { - value = [&] - { - if (file.isEmpty()) - return std::wstring(); - - switch (colTypeRim) - { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat) - { - case ItemPathFormat::FULL_PATH: - return AFS::getDisplayPath(file.getAbstractPath()); - case ItemPathFormat::RELATIVE_PATH: - return utfTo(file.getRelativePath()); - case ItemPathFormat::ITEM_NAME: - return utfTo(file.getItemName()); - } - break; - case ColumnTypeRim::SIZE: - //return utfTo(file.getFileId()); // -> test file id - return formatNumber(file.getFileSize()); - case ColumnTypeRim::DATE: - return formatUtcToLocalTime(file.getLastWriteTime()); - case ColumnTypeRim::EXTENSION: - return utfTo(getFileExtension(file.getItemName())); - } - assert(false); - return std::wstring(); - }(); - }, - - [&](const SymlinkPair& symlink) - { - value = [&] - { - if (symlink.isEmpty()) - return std::wstring(); - - switch (colTypeRim) - { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat) - { - case ItemPathFormat::FULL_PATH: - return AFS::getDisplayPath(symlink.getAbstractPath()); - case ItemPathFormat::RELATIVE_PATH: - return utfTo(symlink.getRelativePath()); - case ItemPathFormat::ITEM_NAME: - return utfTo(symlink.getItemName()); - } - break; - case ColumnTypeRim::SIZE: - return L"<" + _("Symlink") + L">"; - case ColumnTypeRim::DATE: - return formatUtcToLocalTime(symlink.getLastWriteTime()); - case ColumnTypeRim::EXTENSION: - return utfTo(getFileExtension(symlink.getItemName())); - } - assert(false); - return std::wstring(); - }(); - }); - return value; - } - //if data is not found: - return std::wstring(); - } - - static const int GAP_SIZE = 2; - - void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override - { - //don't forget to harmonize with getBestSize()!!! - - const bool isActive = [&] - { - if (const FileSystemObject* fsObj = this->getRawData(row)) - return fsObj->isActive(); - return true; - }(); - - wxDCTextColourChanger dummy(dc); - if (!isActive) - dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - else if (getRowDisplayType(row) != DisplayType::NORMAL) - dummy.Set(*wxBLACK); //accessibility: always set both foreground AND background colors! - - wxRect rectTmp = rect; - - auto drawTextBlock = [&](const std::wstring& text) - { - rectTmp.x += GAP_SIZE; - rectTmp.width -= GAP_SIZE; - const wxSize extent = drawCellText(dc, rectTmp, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - rectTmp.x += extent.GetWidth(); - rectTmp.width -= extent.GetWidth(); - }; - - const std::wstring cellValue = getValue(row, colType); - - switch (static_cast(colType)) - { - case ColumnTypeRim::ITEM_PATH: - { - if (!iconMgr_) - drawTextBlock(cellValue); - else - { - auto it = cellValue.end(); - while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate - { - --it; - if (*it == '\\' || *it == '/') - { - ++it; - break; - } - } - const std::wstring pathPrefix(cellValue.begin(), it); - const std::wstring itemName(it, cellValue.end()); - - // Partitioning: - // __________________________________________________ - // | gap | path prefix | gap | icon | gap | item name | - // -------------------------------------------------- - if (!pathPrefix.empty()) - drawTextBlock(pathPrefix); - - //draw file icon - rectTmp.x += GAP_SIZE; - rectTmp.width -= GAP_SIZE; - - const int iconSize = iconMgr_->refIconBuffer().getSize(); - if (rectTmp.GetWidth() >= iconSize) - { - //whenever there's something new to render on screen, start up watching for failed icon drawing: - //=> ideally it would suffice to start watching only when scrolling grid or showing new grid content, but this solution is more robust - //and the icon updater will stop automatically when finished anyway - //Note: it's not sufficient to start up on failed icon loads only, since we support prefetching of not yet visible rows!!! - iconMgr_->startIconUpdater(); - - const IconInfo ii = getIconInfo(row); - - wxBitmap fileIcon; - switch (ii.type) - { - case IconInfo::FOLDER: - fileIcon = iconMgr_->getGenericDirIcon(); - break; - - case IconInfo::ICON_PATH: - if (Opt tmpIco = iconMgr_->refIconBuffer().retrieveFileIcon(ii.fsObj->template getAbstractPath())) - fileIcon = *tmpIco; - else - { - setFailedLoad(row); //save status of failed icon load -> used for async. icon loading - //falsify only! we want to avoid writing incorrect success values when only partially updating the DC, e.g. when scrolling, - //see repaint behavior of ::ScrollWindow() function! - fileIcon = iconMgr_->refIconBuffer().getIconByExtension(ii.fsObj->template getItemName()); //better than nothing - } - break; - - case IconInfo::EMPTY: - break; - } - - if (fileIcon.IsOk()) - { - wxRect rectIcon = rectTmp; - rectIcon.width = iconSize; //support small thumbnail centering - - auto drawIcon = [&](const wxBitmap& icon) - { - if (isActive) - drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER); - else - drawBitmapRtlNoMirror(dc, wxBitmap(icon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)), //treat all channels equally! - rectIcon, wxALIGN_CENTER); - }; - - drawIcon(fileIcon); - - if (ii.drawAsLink) - drawIcon(iconMgr_->getLinkOverlayIcon()); - } - } - rectTmp.x += iconSize; - rectTmp.width -= iconSize; - - drawTextBlock(itemName); - } - } - break; - - case ColumnTypeRim::SIZE: - if (refGrid().GetLayoutDirection() != wxLayout_RightToLeft) - { - rectTmp.width -= GAP_SIZE; //have file size right-justified (but don't change for RTL languages) - drawCellText(dc, rectTmp, cellValue, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); - } - else - drawTextBlock(cellValue); - break; - - case ColumnTypeRim::DATE: - case ColumnTypeRim::EXTENSION: - drawTextBlock(cellValue); - break; - } - } - - int getBestSize(wxDC& dc, size_t row, ColumnType colType) override - { - // Partitioning: - // ________________________________________________________ - // | gap | path prefix | gap | icon | gap | item name | gap | - // -------------------------------------------------------- - - const std::wstring cellValue = getValue(row, colType); - - if (static_cast(colType) == ColumnTypeRim::ITEM_PATH && iconMgr_) - { - auto it = cellValue.end(); - while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate - { - --it; - if (*it == '\\' || *it == '/') - { - ++it; - break; - } - } - const std::wstring pathPrefix(cellValue.begin(), it); - const std::wstring itemName(it, cellValue.end()); - - int bestSize = 0; - if (!pathPrefix.empty()) - bestSize += GAP_SIZE + dc.GetTextExtent(pathPrefix).GetWidth(); - - bestSize += GAP_SIZE + iconMgr_->refIconBuffer().getSize(); - bestSize += GAP_SIZE + dc.GetTextExtent(itemName).GetWidth() + GAP_SIZE; - return bestSize; - } - else - return GAP_SIZE + dc.GetTextExtent(cellValue).GetWidth() + GAP_SIZE; - // + 1 pix for cell border line ? -> not used anymore! - } - - std::wstring getColumnLabel(ColumnType colType) const override - { - switch (static_cast(colType)) - { - case ColumnTypeRim::ITEM_PATH: - switch (itemPathFormat) - { - case ItemPathFormat::FULL_PATH: - return _("Full path"); - case ItemPathFormat::RELATIVE_PATH: - return _("Relative path"); - case ItemPathFormat::ITEM_NAME: - return _("Item name"); - } - assert(false); - break; - case ColumnTypeRim::SIZE: - return _("Size"); - case ColumnTypeRim::DATE: - return _("Date"); - case ColumnTypeRim::EXTENSION: - return _("Extension"); - } - //assert(false); may be ColumnType::NONE - return std::wstring(); - } - - void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override - { - wxRect rectInside = drawColumnLabelBorder(dc, rect); - drawColumnLabelBackground(dc, rectInside, highlighted); - - rectInside.x += COLUMN_GAP_LEFT; - rectInside.width -= COLUMN_GAP_LEFT; - drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); - - //draw sort marker - if (getGridDataView()) - { - auto sortInfo = getGridDataView()->getSortInfo(); - if (sortInfo) - { - if (colType == static_cast(sortInfo->type_) && (side == LEFT_SIDE) == sortInfo->onLeft_) - { - const wxBitmap& marker = getResourceImage(sortInfo->ascending_ ? L"sortAscending" : L"sortDescending"); - drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL); - } - } - } - } - - struct IconInfo - { - enum IconType - { - EMPTY, - FOLDER, - ICON_PATH, - }; - IconType type = EMPTY; - const FileSystemObject* fsObj = nullptr; //only set if type != EMPTY - bool drawAsLink = false; - }; - - IconInfo getIconInfo(size_t row) const //return ICON_FILE_FOLDER if row points to a folder - { - IconInfo out; - - const FileSystemObject* fsObj = getRawData(row); - if (fsObj && !fsObj->isEmpty()) - { - out.fsObj = fsObj; - - visitFSObject(*fsObj, [&](const FolderPair& folder) - { - out.type = IconInfo::FOLDER; - out.drawAsLink = folder.isFollowedSymlink(); - }, - - [&](const FilePair& file) - { - out.type = IconInfo::ICON_PATH; - out.drawAsLink = file.isFollowedSymlink() || hasLinkExtension(file.getItemName()); - }, - - [&](const SymlinkPair& symlink) - { - out.type = IconInfo::ICON_PATH; - out.drawAsLink = true; - }); - } - return out; - } - - std::wstring getToolTip(size_t row, ColumnType colType) const override - { - std::wstring toolTip; - - if (const FileSystemObject* fsObj = getRawData(row)) - if (!fsObj->isEmpty()) - { - toolTip = getGridDataView() && getGridDataView()->getFolderPairCount() > 1 ? - AFS::getDisplayPath(fsObj->getAbstractPath()) : - utfTo(fsObj->getRelativePath()); - - visitFSObject(*fsObj, [](const FolderPair& folder) {}, - [&](const FilePair& file) - { - toolTip += L"\n" + - _("Size:") + L" " + zen::formatFilesizeShort(file.getFileSize()) + L"\n" + - _("Date:") + L" " + zen::formatUtcToLocalTime(file.getLastWriteTime()); - }, - - [&](const SymlinkPair& symlink) - { - toolTip += L"\n" + - _("Date:") + L" " + zen::formatUtcToLocalTime(symlink.getLastWriteTime()); - }); - } - return toolTip; - } - - std::shared_ptr iconMgr_; //optional - ItemPathFormat itemPathFormat = ItemPathFormat::FULL_PATH; - - std::vector failedLoads; //effectively a vector of size "number of rows" - Opt renderBuf; //avoid costs of recreating this temporal variable -}; - - -class GridDataLeft : public GridDataRim -{ -public: - GridDataLeft(const std::shared_ptr& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} - - void setNavigationMarker(std::unordered_set&& markedFilesAndLinks, - std::unordered_set&& markedContainer) - { - markedFilesAndLinks_.swap(markedFilesAndLinks); - markedContainer_ .swap(markedContainer); - } - -private: - void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override - { - GridDataRim::renderRowBackgound(dc, rect, row, enabled, selected); - - //mark rows selected on navigation grid: - if (enabled && !selected) - { - const bool markRow = [&] - { - if (const FileSystemObject* fsObj = getRawData(row)) - { - if (markedFilesAndLinks_.find(fsObj) != markedFilesAndLinks_.end()) //mark files/links directly - return true; - - if (auto folder = dynamic_cast(fsObj)) - { - if (markedContainer_.find(folder) != markedContainer_.end()) //mark directories which *are* the given ContainerObject* - return true; - } - - //mark all objects which have the ContainerObject as *any* matching ancestor - const ContainerObject* parent = &(fsObj->parent()); - for (;;) - { - if (markedContainer_.find(parent) != markedContainer_.end()) - return true; - - if (auto folder = dynamic_cast(parent)) - parent = &(folder->parent()); - else - break; - } - } - return false; - }(); - - if (markRow) - { - wxRect rectTmp = rect; - rectTmp.width /= 20; - dc.GradientFillLinear(rectTmp, Grid::getColorSelectionGradientFrom(), GridDataRim::getBackGroundColor(row), wxEAST); - } - } - } - - std::unordered_set markedFilesAndLinks_; //mark files/symlinks directly within a container - std::unordered_set markedContainer_; //mark full container including all child-objects - //DO NOT DEREFERENCE!!!! NOT GUARANTEED TO BE VALID!!! -}; - - -class GridDataRight : public GridDataRim -{ -public: - GridDataRight(const std::shared_ptr& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} -}; - -//######################################################################################################## - -class GridDataCenter : public GridDataBase -{ -public: - GridDataCenter(const std::shared_ptr& gridDataView, Grid& grid) : - GridDataBase(grid, gridDataView), - toolTip(grid) {} //tool tip must not live longer than grid! - - void onSelectBegin() - { - selectionInProgress = true; - refGrid().clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion! - toolTip.hide(); //handle custom tooltip - } - - void onSelectEnd(size_t rowFirst, size_t rowLast, HoverArea rowHover, ptrdiff_t clickInitRow) - { - refGrid().clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion! - - //issue custom event - if (selectionInProgress) //don't process selections initiated by right-click - if (rowFirst < rowLast && rowLast <= refGrid().getRowCount()) //empty? probably not in this context - if (wxEvtHandler* evtHandler = refGrid().GetEventHandler()) - switch (static_cast(rowHover)) - { - case HoverAreaCenter::CHECK_BOX: - if (const FileSystemObject* fsObj = getRawData(clickInitRow)) - { - const bool setIncluded = !fsObj->isActive(); - CheckRowsEvent evt(rowFirst, rowLast, setIncluded); - evtHandler->ProcessEvent(evt); - } - break; - case HoverAreaCenter::DIR_LEFT: - { - SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::LEFT); - evtHandler->ProcessEvent(evt); - } - break; - case HoverAreaCenter::DIR_NONE: - { - SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::NONE); - evtHandler->ProcessEvent(evt); - } - break; - case HoverAreaCenter::DIR_RIGHT: - { - SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::RIGHT); - evtHandler->ProcessEvent(evt); - } - break; - } - selectionInProgress = false; - - //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows) - wxPoint clientPos = refGrid().getMainWin().ScreenToClient(wxGetMousePosition()); - onMouseMovement(clientPos); - } - - void onMouseMovement(const wxPoint& clientPos) - { - //manage block highlighting and custom tooltip - if (!selectionInProgress) - { - const wxPoint& topLeftAbs = refGrid().CalcUnscrolledPosition(clientPos); - const size_t row = refGrid().getRowAtPos(topLeftAbs.y); //return -1 for invalid position, rowCount if one past the end - const Grid::ColumnPosInfo cpi = refGrid().getColumnAtPos(topLeftAbs.x); //returns ColumnType::NONE if no column at x position! - - if (row < refGrid().getRowCount() && cpi.colType != ColumnType::NONE && - refGrid().getMainWin().GetClientRect().Contains(clientPos)) //cursor might have moved outside visible client area - showToolTip(row, static_cast(cpi.colType), refGrid().getMainWin().ClientToScreen(clientPos)); - else - toolTip.hide(); - } - } - - void onMouseLeave() //wxEVT_LEAVE_WINDOW does not respect mouse capture! - { - toolTip.hide(); //handle custom tooltip - } - - void highlightSyncAction(bool value) { highlightSyncAction_ = value; } - -private: - enum class HoverAreaCenter //each cell can be divided into four blocks concerning mouse selections - { - CHECK_BOX, - DIR_LEFT, - DIR_NONE, - DIR_RIGHT - }; - - std::wstring getValue(size_t row, ColumnType colType) const override - { - if (const FileSystemObject* fsObj = getRawData(row)) - switch (static_cast(colType)) - { - case ColumnTypeCenter::CHECKBOX: - break; - case ColumnTypeCenter::CMP_CATEGORY: - return getSymbol(fsObj->getCategory()); - case ColumnTypeCenter::SYNC_ACTION: - return getSymbol(fsObj->getSyncOperation()); - } - return std::wstring(); - } - - void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override - { - if (enabled) - { - if (selected) - dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST); - else - { - if (const FileSystemObject* fsObj = getRawData(row)) - { - if (fsObj->isActive()) - fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); - else - clearArea(dc, rect, getColorNotActive()); - } - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - } - } - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - } - - void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override - { - auto drawHighlightBackground = [&](const FileSystemObject& fsObj, const wxColor& col) - { - if (enabled && !selected && fsObj.isActive()) //coordinate with renderRowBackgound()! - clearArea(dc, rect, col); - }; - - switch (static_cast(colType)) - { - case ColumnTypeCenter::CHECKBOX: - if (const FileSystemObject* fsObj = getRawData(row)) - { - const bool drawMouseHover = static_cast(rowHover) == HoverAreaCenter::CHECK_BOX; - - if (fsObj->isActive()) - drawBitmapRtlMirror(dc, getResourceImage(drawMouseHover ? L"checkbox_true_hover" : L"checkbox_true"), rect, wxALIGN_CENTER, renderBuf); - else //default - drawBitmapRtlMirror(dc, getResourceImage(drawMouseHover ? L"checkbox_false_hover" : L"checkbox_false"), rect, wxALIGN_CENTER, renderBuf); - } - break; - - case ColumnTypeCenter::CMP_CATEGORY: - if (const FileSystemObject* fsObj = getRawData(row)) - { - if (!highlightSyncAction_) - drawHighlightBackground(*fsObj, getBackGroundColorCmpCategory(fsObj)); - - wxRect rectTmp = rect; - { - //draw notch on left side - if (notch.GetHeight() != rectTmp.GetHeight()) - notch.Rescale(notch.GetWidth(), rectTmp.GetHeight()); - - //wxWidgets screws up again and has wxALIGN_RIGHT off by one pixel! -> use wxALIGN_LEFT instead - const wxRect rectNotch(rectTmp.x + rectTmp.width - notch.GetWidth(), rectTmp.y, notch.GetWidth(), rectTmp.height); - drawBitmapRtlMirror(dc, notch, rectNotch, wxALIGN_LEFT, renderBuf); - rectTmp.width -= notch.GetWidth(); - } - - if (!highlightSyncAction_) - drawBitmapRtlMirror(dc, getCmpResultImage(fsObj->getCategory()), rectTmp, wxALIGN_CENTER, renderBuf); - else if (fsObj->getCategory() != FILE_EQUAL) //don't show = in both middle columns - drawBitmapRtlMirror(dc, greyScale(getCmpResultImage(fsObj->getCategory())), rectTmp, wxALIGN_CENTER, renderBuf); - } - break; - - case ColumnTypeCenter::SYNC_ACTION: - if (const FileSystemObject* fsObj = getRawData(row)) - { - if (highlightSyncAction_) - drawHighlightBackground(*fsObj, getBackGroundColorSyncAction(fsObj)); - - //synchronization preview - const auto rowHoverCenter = rowHover == HoverArea::NONE ? HoverAreaCenter::CHECK_BOX : static_cast(rowHover); - switch (rowHoverCenter) - { - case HoverAreaCenter::DIR_LEFT: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::LEFT)), rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, renderBuf); - break; - case HoverAreaCenter::DIR_NONE: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::NONE)), rect, wxALIGN_CENTER, renderBuf); - break; - case HoverAreaCenter::DIR_RIGHT: - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::RIGHT)), rect, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, renderBuf); - break; - case HoverAreaCenter::CHECK_BOX: - if (highlightSyncAction_) - drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->getSyncOperation()), rect, wxALIGN_CENTER, renderBuf); - else if (fsObj->getSyncOperation() != SO_EQUAL) //don't show = in both middle columns - drawBitmapRtlMirror(dc, greyScale(getSyncOpImage(fsObj->getSyncOperation())), rect, wxALIGN_CENTER, renderBuf); - break; - } - } - break; - } - } - - HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override - { - if (const FileSystemObject* const fsObj = getRawData(row)) - switch (static_cast(colType)) - { - case ColumnTypeCenter::CHECKBOX: - case ColumnTypeCenter::CMP_CATEGORY: - return static_cast(HoverAreaCenter::CHECK_BOX); - - case ColumnTypeCenter::SYNC_ACTION: - if (fsObj->getSyncOperation() == SO_EQUAL) //in sync-preview equal files shall be treated like a checkbox - return static_cast(HoverAreaCenter::CHECK_BOX); - // cell: - // ----------------------- - // | left | middle | right| - // ----------------------- - if (0 <= cellRelativePosX) - { - if (cellRelativePosX < cellWidth / 3) - return static_cast(HoverAreaCenter::DIR_LEFT); - else if (cellRelativePosX < 2 * cellWidth / 3) - return static_cast(HoverAreaCenter::DIR_NONE); - else if (cellRelativePosX < cellWidth) - return static_cast(HoverAreaCenter::DIR_RIGHT); - } - break; - } - return HoverArea::NONE; - } - - std::wstring getColumnLabel(ColumnType colType) const override - { - switch (static_cast(colType)) - { - case ColumnTypeCenter::CHECKBOX: - break; - case ColumnTypeCenter::CMP_CATEGORY: - return _("Category") + L" (F10)"; - case ColumnTypeCenter::SYNC_ACTION: - return _("Action") + L" (F10)"; - } - return std::wstring(); - } - - std::wstring getToolTip(ColumnType colType) const override { return getColumnLabel(colType); } - - void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override - { - switch (static_cast(colType)) - { - case ColumnTypeCenter::CHECKBOX: - drawColumnLabelBackground(dc, rect, false); - break; - - case ColumnTypeCenter::CMP_CATEGORY: - { - wxRect rectInside = drawColumnLabelBorder(dc, rect); - drawColumnLabelBackground(dc, rectInside, highlighted); - - const wxBitmap& cmpIcon = getResourceImage(L"compare_small"); - drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? greyScale(cmpIcon) : cmpIcon, rectInside, wxALIGN_CENTER); - } - break; - - case ColumnTypeCenter::SYNC_ACTION: - { - wxRect rectInside = drawColumnLabelBorder(dc, rect); - drawColumnLabelBackground(dc, rectInside, highlighted); - - const wxBitmap& syncIcon = getResourceImage(L"sync_small"); - drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? syncIcon : greyScale(syncIcon), rectInside, wxALIGN_CENTER); - } - break; - } - } - - static wxColor getBackGroundColorSyncAction(const FileSystemObject* fsObj) - { - if (fsObj) - { - if (!fsObj->isActive()) - return getColorNotActive(); - - switch (fsObj->getSyncOperation()) //evaluate comparison result and sync direction - { - case SO_DO_NOTHING: - return getColorNotActive(); - case SO_EQUAL: - break; //usually white - - case SO_CREATE_NEW_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: - return getColorSyncBlue(); - - case SO_CREATE_NEW_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: - return getColorSyncGreen(); - - case SO_UNRESOLVED_CONFLICT: - return getColorYellow(); - } - } - return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - } - - static wxColor getBackGroundColorCmpCategory(const FileSystemObject* fsObj) - { - if (fsObj) - { - if (!fsObj->isActive()) - return getColorNotActive(); - - switch (fsObj->getCategory()) - { - case FILE_LEFT_SIDE_ONLY: - case FILE_LEFT_NEWER: - return getColorSyncBlue(); //COLOR_CMP_BLUE; - - case FILE_RIGHT_SIDE_ONLY: - case FILE_RIGHT_NEWER: - return getColorSyncGreen(); //COLOR_CMP_GREEN; - - case FILE_DIFFERENT_CONTENT: - return getColorCmpRed(); - case FILE_EQUAL: - break; //usually white - case FILE_CONFLICT: - case FILE_DIFFERENT_METADATA: //= sub-category of equal, but hint via background that sync direction follows conflict-setting - return getColorYellow(); - //return getColorYellowLight(); - } - } - return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - } - - void showToolTip(size_t row, ColumnTypeCenter colType, wxPoint posScreen) - { - if (const FileSystemObject* fsObj = getRawData(row)) - { - switch (colType) - { - case ColumnTypeCenter::CHECKBOX: - case ColumnTypeCenter::CMP_CATEGORY: - { - const wchar_t* imageName = [&] - { - const CompareFilesResult cmpRes = fsObj->getCategory(); - switch (cmpRes) - { - case FILE_LEFT_SIDE_ONLY: - return L"cat_left_only"; - case FILE_RIGHT_SIDE_ONLY: - return L"cat_right_only"; - case FILE_LEFT_NEWER: - return L"cat_left_newer"; - case FILE_RIGHT_NEWER: - return L"cat_right_newer"; - case FILE_DIFFERENT_CONTENT: - return L"cat_different"; - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal - return L"cat_equal"; - case FILE_CONFLICT: - return L"cat_conflict"; - } - assert(false); - return L""; - }(); - const auto& img = mirrorIfRtl(getResourceImage(imageName)); - toolTip.show(getCategoryDescription(*fsObj), posScreen, &img); - } - break; - - case ColumnTypeCenter::SYNC_ACTION: - { - const wchar_t* imageName = [&] - { - const SyncOperation syncOp = fsObj->getSyncOperation(); - switch (syncOp) - { - case SO_CREATE_NEW_LEFT: - return L"so_create_left"; - case SO_CREATE_NEW_RIGHT: - return L"so_create_right"; - case SO_DELETE_LEFT: - return L"so_delete_left"; - case SO_DELETE_RIGHT: - return L"so_delete_right"; - case SO_MOVE_LEFT_FROM: - return L"so_move_left_source"; - case SO_MOVE_LEFT_TO: - return L"so_move_left_target"; - case SO_MOVE_RIGHT_FROM: - return L"so_move_right_source"; - case SO_MOVE_RIGHT_TO: - return L"so_move_right_target"; - case SO_OVERWRITE_LEFT: - return L"so_update_left"; - case SO_OVERWRITE_RIGHT: - return L"so_update_right"; - case SO_COPY_METADATA_TO_LEFT: - return L"so_move_left"; - case SO_COPY_METADATA_TO_RIGHT: - return L"so_move_right"; - case SO_DO_NOTHING: - return L"so_none"; - case SO_EQUAL: - return L"cat_equal"; - case SO_UNRESOLVED_CONFLICT: - return L"cat_conflict"; - }; - assert(false); - return L""; - }(); - const auto& img = mirrorIfRtl(getResourceImage(imageName)); - toolTip.show(getSyncOpDescription(*fsObj), posScreen, &img); - } - break; - } - } - else - toolTip.hide(); //if invalid row... - } - - bool highlightSyncAction_ = false; - bool selectionInProgress = false; - - Opt renderBuf; //avoid costs of recreating this temporal variable - Tooltip toolTip; - wxImage notch { getResourceImage(L"notch").ConvertToImage() }; -}; - -//######################################################################################################## - -const wxEventType EVENT_ALIGN_SCROLLBARS = wxNewEventType(); - -class GridEventManager : private wxEvtHandler -{ -public: - GridEventManager(Grid& gridL, - Grid& gridC, - Grid& gridR, - GridDataCenter& provCenter) : - gridL_(gridL), gridC_(gridC), gridR_(gridR), - provCenter_(provCenter) - { - gridL_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnL), nullptr, this); - gridR_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnR), nullptr, this); - - gridL_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler (GridEventManager::onKeyDownL), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler (GridEventManager::onKeyDownC), nullptr, this); - gridR_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler (GridEventManager::onKeyDownR), nullptr, this); - - gridC_.getMainWin().Connect(wxEVT_MOTION, wxMouseEventHandler(GridEventManager::onCenterMouseMovement), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(GridEventManager::onCenterMouseLeave ), nullptr, this); - - gridC_.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler (GridEventManager::onCenterSelectBegin), nullptr, this); - gridC_.Connect(EVENT_GRID_SELECT_RANGE, GridRangeSelectEventHandler(GridEventManager::onCenterSelectEnd ), nullptr, this); - - //clear selection of other grid when selecting on - gridL_.Connect(EVENT_GRID_SELECT_RANGE, GridRangeSelectEventHandler(GridEventManager::onGridSelectionL), nullptr, this); - gridR_.Connect(EVENT_GRID_SELECT_RANGE, GridRangeSelectEventHandler(GridEventManager::onGridSelectionR), nullptr, this); - - //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: - gridL_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridL), nullptr, this); - gridC_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridC), nullptr, this); - gridR_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridR), nullptr, this); - - auto connectGridAccess = [&](Grid& grid, wxObjectEventFunction func) - { - grid.Connect(wxEVT_SCROLLWIN_TOP, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_BOTTOM, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_LINEUP, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_LINEDOWN, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_PAGEUP, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_PAGEDOWN, func, nullptr, this); - grid.Connect(wxEVT_SCROLLWIN_THUMBTRACK, func, nullptr, this); - //wxEVT_KILL_FOCUS -> there's no need to reset "scrollMaster" - //wxEVT_SET_FOCUS -> not good enough: - //e.g.: left grid has input, right grid is "scrollMaster" due to dragging scroll thumb via mouse. - //=> Next keyboard input on left does *not* emit focus change event, but still "scrollMaster" needs to change - //=> hook keyboard input instead of focus event: - grid.getMainWin().Connect(wxEVT_CHAR, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_KEY_UP, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_KEY_DOWN, func, nullptr, this); - - grid.getMainWin().Connect(wxEVT_LEFT_DOWN, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_LEFT_DCLICK, func, nullptr, this); - grid.getMainWin().Connect(wxEVT_RIGHT_DOWN, func, nullptr, this); - //grid.getMainWin().Connect(wxEVT_MOUSEWHEEL, func, nullptr, this); -> should be covered by wxEVT_SCROLLWIN_* - }; - connectGridAccess(gridL_, wxEventHandler(GridEventManager::onGridAccessL)); // - connectGridAccess(gridC_, wxEventHandler(GridEventManager::onGridAccessC)); //connect *after* onKeyDown() in order to receive callback *before*!!! - connectGridAccess(gridR_, wxEventHandler(GridEventManager::onGridAccessR)); // - - Connect(EVENT_ALIGN_SCROLLBARS, wxEventHandler(GridEventManager::onAlignScrollBars), NULL, this); - } - - ~GridEventManager() { assert(!scrollbarUpdatePending); } - - void setScrollMaster(const Grid& grid) { scrollMaster = &grid; } - -private: - void onCenterSelectBegin(GridClickEvent& event) - { - provCenter_.onSelectBegin(); - event.Skip(); - } - - void onCenterSelectEnd(GridRangeSelectEvent& event) - { - if (event.positive_) - { - if (event.mouseInitiated_) - provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseInitiated_->hoverArea_, event.mouseInitiated_->row_); - else - provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::NONE, -1); - } - event.Skip(); - } - - void onCenterMouseMovement(wxMouseEvent& event) - { - provCenter_.onMouseMovement(event.GetPosition()); - event.Skip(); - } - - void onCenterMouseLeave(wxMouseEvent& event) - { - provCenter_.onMouseLeave(); - event.Skip(); - } - - void onGridSelectionL(GridRangeSelectEvent& event) { onGridSelection(gridL_, gridR_); event.Skip(); } - void onGridSelectionR(GridRangeSelectEvent& event) { onGridSelection(gridR_, gridL_); event.Skip(); } - - void onGridSelection(const Grid& grid, Grid& other) - { - if (!wxGetKeyState(WXK_CONTROL)) //clear other grid unless user is holding CTRL - other.clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion! - } - - void onKeyDownL(wxKeyEvent& event) { onKeyDown(event, gridL_); } - void onKeyDownC(wxKeyEvent& event) { onKeyDown(event, gridC_); } - void onKeyDownR(wxKeyEvent& event) { onKeyDown(event, gridR_); } - - void onKeyDown(wxKeyEvent& event, const Grid& grid) - { - int keyCode = event.GetKeyCode(); - if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) - { - if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) - keyCode = WXK_RIGHT; - else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) - keyCode = WXK_LEFT; - } - - //skip middle component when navigating via keyboard - const size_t row = grid.getGridCursor(); - - if (event.ShiftDown()) - ; - else if (event.ControlDown()) - ; - else - switch (keyCode) - { - case WXK_LEFT: - case WXK_NUMPAD_LEFT: - gridL_.setGridCursor(row); - gridL_.SetFocus(); - //since key event is likely originating from right grid, we need to set scrollMaster manually! - scrollMaster = &gridL_; //onKeyDown is called *after* onGridAccessL()! - return; //swallow event - - case WXK_RIGHT: - case WXK_NUMPAD_RIGHT: - gridR_.setGridCursor(row); - gridR_.SetFocus(); - scrollMaster = &gridR_; - return; //swallow event - } - - event.Skip(); - } - - void onResizeColumnL(GridColumnResizeEvent& event) { resizeOtherSide(gridL_, gridR_, event.colType_, event.offset_); } - void onResizeColumnR(GridColumnResizeEvent& event) { resizeOtherSide(gridR_, gridL_, event.colType_, event.offset_); } - - void resizeOtherSide(const Grid& src, Grid& trg, ColumnType type, int offset) - { - //find stretch factor of resized column: type is unique due to makeConsistent()! - std::vector cfgSrc = src.getColumnConfig(); - auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColumnAttribute& ca) { return ca.type_ == type; }); - if (it == cfgSrc.end()) - return; - const int stretchSrc = it->stretch_; - - //we do not propagate resizings on stretched columns to the other side: awkward user experience - if (stretchSrc > 0) - return; - - //apply resized offset to other side, but only if stretch factors match! - std::vector cfgTrg = trg.getColumnConfig(); - for (Grid::ColumnAttribute& ca : cfgTrg) - if (ca.type_ == type && ca.stretch_ == stretchSrc) - ca.offset_ = offset; - trg.setColumnConfig(cfgTrg); - } - - void onGridAccessL(wxEvent& event) { scrollMaster = &gridL_; event.Skip(); } - void onGridAccessC(wxEvent& event) { scrollMaster = &gridC_; event.Skip(); } - void onGridAccessR(wxEvent& event) { scrollMaster = &gridR_; event.Skip(); } - - void onPaintGridL(wxEvent& event) { onPaintGrid(gridL_); event.Skip(); } - void onPaintGridC(wxEvent& event) { onPaintGrid(gridC_); event.Skip(); } - void onPaintGridR(wxEvent& event) { onPaintGrid(gridR_); event.Skip(); } - - void onPaintGrid(const Grid& grid) - { - //align scroll positions of all three grids *synchronously* during paint event! (wxGTK has visible delay when this is done asynchronously, no delay on Windows) - - //determine lead grid - const Grid* lead = nullptr; - Grid* follow1 = nullptr; - Grid* follow2 = nullptr; - auto setGrids = [&](const Grid& l, Grid& f1, Grid& f2) { lead = &l; follow1 = &f1; follow2 = &f2; }; - - if (&gridC_ == scrollMaster) - setGrids(gridC_, gridL_, gridR_); - else if (&gridR_ == scrollMaster) - setGrids(gridR_, gridL_, gridC_); - else //default: left panel - setGrids(gridL_, gridC_, gridR_); - - //align other grids only while repainting the lead grid to avoid scrolling and updating a grid at the same time! - if (lead != &grid) return; - - auto scroll = [](Grid& target, int y) //support polling - { - //scroll vertically only - scrolling horizontally becomes annoying if left and right sides have different widths; - //e.g. h-scroll on left would be undone when scrolling vertically on right which doesn't have a h-scrollbar - int yOld = 0; - target.GetViewStart(nullptr, &yOld); - if (yOld != y) - target.Scroll(-1, y); //empirical test Windows/Ubuntu: this call does NOT trigger a wxEVT_SCROLLWIN event, which would incorrectly set "scrollMaster" to "&target"! - }; - int y = 0; - lead->GetViewStart(nullptr, &y); - scroll(*follow1, y); - scroll(*follow2, y); - - //harmonize placement of horizontal scrollbar to avoid grids getting out of sync! - //since this affects the grid that is currently repainted as well, we do work asynchronously! - //avoids at least this problem: remaining graphics artifact when changing from Grid::SB_SHOW_ALWAYS to Grid::SB_SHOW_NEVER at location of old scrollbar (Windows only) - - //perf note: send one async event at most, else they may accumulate and create perf issues, see grid.cpp - if (!scrollbarUpdatePending) - { - scrollbarUpdatePending = true; - wxCommandEvent alignEvent(EVENT_ALIGN_SCROLLBARS); - AddPendingEvent(alignEvent); //waits until next idle event - may take up to a second if the app is busy on wxGTK! - } - } - - void onAlignScrollBars(wxEvent& event) - { - ZEN_ON_SCOPE_EXIT(scrollbarUpdatePending = false); - assert(scrollbarUpdatePending); - - auto needsHorizontalScrollbars = [](const Grid& grid) -> bool - { - const wxWindow& mainWin = grid.getMainWin(); - return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth(); - //assuming Grid::updateWindowSizes() does its job well, this should suffice! - //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other - //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, ect...) - //while in fact both are NOT needed, this special case results in a bogus need for scrollbars! - //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083 - // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant - }; - - Grid::ScrollBarStatus sbStatusX = needsHorizontalScrollbars(gridL_) || - needsHorizontalScrollbars(gridR_) ? - Grid::SB_SHOW_ALWAYS : Grid::SB_SHOW_NEVER; - gridL_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); - gridC_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); - gridR_.showScrollBars(sbStatusX, Grid::SB_SHOW_AUTOMATIC); - } - - Grid& gridL_; - Grid& gridC_; - Grid& gridR_; - - const Grid* scrollMaster = nullptr; //for address check only; this needn't be the grid having focus! - //e.g. mouse wheel events should set window under cursor as scrollMaster, but *not* change focus - - GridDataCenter& provCenter_; - bool scrollbarUpdatePending = false; -}; -} - -//######################################################################################################## - -void gridview::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, const std::shared_ptr& gridDataView) -{ - auto provLeft_ = std::make_shared(gridDataView, gridLeft); - auto provCenter_ = std::make_shared(gridDataView, gridCenter); - auto provRight_ = std::make_shared(gridDataView, gridRight); - - gridLeft .setDataProvider(provLeft_); //data providers reference grid => - gridCenter.setDataProvider(provCenter_); //ownership must belong *exclusively* to grid! - gridRight .setDataProvider(provRight_); - - auto evtMgr = std::make_shared(gridLeft, gridCenter, gridRight, *provCenter_); - provLeft_ ->holdOwnership(evtMgr); - provCenter_->holdOwnership(evtMgr); - provRight_ ->holdOwnership(evtMgr); - - gridCenter.enableColumnMove (false); - gridCenter.enableColumnResize(false); - - gridCenter.showRowLabel(false); - gridRight .showRowLabel(false); - - //gridLeft .showScrollBars(Grid::SB_SHOW_AUTOMATIC, Grid::SB_SHOW_NEVER); -> redundant: configuration happens in GridEventManager::onAlignScrollBars() - //gridCenter.showScrollBars(Grid::SB_SHOW_NEVER, Grid::SB_SHOW_NEVER); - - const int widthCheckbox = getResourceImage(L"checkbox_true").GetWidth() + 4 + getResourceImage(L"notch").GetWidth(); - const int widthCategory = 30; - const int widthAction = 45; - gridCenter.SetSize(widthCategory + widthCheckbox + widthAction, -1); - - gridCenter.setColumnConfig( - { - { static_cast(ColumnTypeCenter::CHECKBOX ), widthCheckbox, 0, true }, - { static_cast(ColumnTypeCenter::CMP_CATEGORY), widthCategory, 0, true }, - { static_cast(ColumnTypeCenter::SYNC_ACTION ), widthAction, 0, true }, - }); -} - - -namespace -{ -std::vector makeConsistent(const std::vector& attribs) -{ - std::set usedTypes; - - std::vector output; - //remove duplicates: required by GridEventManager::resizeOtherSide() to find corresponding column on other side - std::copy_if(attribs.begin(), attribs.end(), std::back_inserter(output), - [&](const ColumnAttributeRim& a) { return usedTypes.insert(a.type_).second; }); - - //make sure each type is existing! -> should *only* be a problem if user manually messes with GlobalSettings.xml - const auto& defAttr = getDefaultColumnAttributesLeft(); - std::copy_if(defAttr.begin(), defAttr.end(), std::back_inserter(output), - [&](const ColumnAttributeRim& a) { return usedTypes.insert(a.type_).second; }); - - return output; -} -} - -std::vector gridview::convertConfig(const std::vector& attribs) -{ - std::vector output; - for (const ColumnAttributeRim& ca : makeConsistent(attribs)) - output.emplace_back(static_cast(ca.type_), ca.offset_, ca.stretch_, ca.visible_); - return output; -} - - -std::vector gridview::convertConfig(const std::vector& attribs) -{ - std::vector output; - for (const Grid::ColumnAttribute& ca : attribs) - output.emplace_back(static_cast(ca.type_), ca.offset_, ca.stretch_, ca.visible_); - return makeConsistent(output); -} - - -namespace -{ -class IconUpdater : private wxEvtHandler //update file icons periodically: use SINGLE instance to coordinate left and right grids in parallel -{ -public: - IconUpdater(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer& iconBuffer) : provLeft_(provLeft), provRight_(provRight), iconBuffer_(iconBuffer) - { - timer_.Connect(wxEVT_TIMER, wxEventHandler(IconUpdater::loadIconsAsynchronously), nullptr, this); - } - - void start() { if (!timer_.IsRunning()) timer_.Start(100); } //timer interval in [ms] - //don't check too often! give worker thread some time to fetch data - -private: - void stop() { if (timer_.IsRunning()) timer_.Stop(); } - - void loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons - { - std::vector> prefetchLoad; - provLeft_ .getUnbufferedIconsForPreload(prefetchLoad); - provRight_.getUnbufferedIconsForPreload(prefetchLoad); - - //make sure least-important prefetch rows are inserted first into workload (=> processed last) - //priority index nicely considers both grids at the same time! - std::sort(prefetchLoad.begin(), prefetchLoad.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); - - //last inserted items are processed first in icon buffer: - std::vector newLoad; - for (const auto& item : prefetchLoad) - newLoad.push_back(item.second); - - provRight_.updateNewAndGetUnbufferedIcons(newLoad); - provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); - - iconBuffer_.setWorkload(newLoad); - - if (newLoad.empty()) //let's only pay for IconUpdater when needed - stop(); - } - - GridDataLeft& provLeft_; - GridDataRight& provRight_; - IconBuffer& iconBuffer_; - wxTimer timer_; -}; - - -//resolve circular linker dependencies -inline -void IconManager::startIconUpdater() { if (iconUpdater) iconUpdater->start(); } -} - - -void gridview::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool show, IconBuffer::IconSize sz) -{ - auto* provLeft = dynamic_cast(gridLeft .getDataProvider()); - auto* provRight = dynamic_cast(gridRight.getDataProvider()); - - if (provLeft && provRight) - { - int iconHeight = 0; - if (show) - { - auto iconMgr = std::make_shared(*provLeft, *provRight, sz); - provLeft ->setIconManager(iconMgr); - provRight->setIconManager(iconMgr); - iconHeight = iconMgr->refIconBuffer().getSize(); - } - else - { - provLeft ->setIconManager(nullptr); - provRight->setIconManager(nullptr); - iconHeight = IconBuffer::getSize(IconBuffer::SIZE_SMALL); - } - - const int newRowHeight = std::max(iconHeight, gridLeft.getMainWin().GetCharHeight()) + 1; //add some space - - gridLeft .setRowHeight(newRowHeight); - gridCenter.setRowHeight(newRowHeight); - gridRight .setRowHeight(newRowHeight); - } - else - assert(false); -} - - -void gridview::setItemPathForm(Grid& grid, ItemPathFormat fmt) -{ - if (auto* provLeft = dynamic_cast(grid.getDataProvider())) - provLeft->setItemPathForm(fmt); - else if (auto* provRight = dynamic_cast(grid.getDataProvider())) - provRight->setItemPathForm(fmt); - else - assert(false); - grid.Refresh(); -} - - -void gridview::refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) -{ - gridLeft .Refresh(); - gridCenter.Refresh(); - gridRight .Refresh(); -} - - -void gridview::setScrollMaster(Grid& grid) -{ - if (auto prov = dynamic_cast(grid.getDataProvider())) - if (auto evtMgr = prov->getEventManager()) - { - evtMgr->setScrollMaster(grid); - return; - } - assert(false); -} - - -void gridview::setNavigationMarker(Grid& gridLeft, - std::unordered_set&& markedFilesAndLinks, - std::unordered_set&& markedContainer) -{ - if (auto provLeft = dynamic_cast(gridLeft.getDataProvider())) - provLeft->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); - else - assert(false); - gridLeft.Refresh(); -} - - -void gridview::highlightSyncAction(Grid& gridCenter, bool value) -{ - if (auto provCenter = dynamic_cast(gridCenter.getDataProvider())) - provCenter->highlightSyncAction(value); - else - assert(false); - gridCenter.Refresh(); -} - - -wxBitmap zen::getSyncOpImage(SyncOperation syncOp) -{ - switch (syncOp) //evaluate comparison result and sync direction - { - case SO_CREATE_NEW_LEFT: - return getResourceImage(L"so_create_left_small"); - case SO_CREATE_NEW_RIGHT: - return getResourceImage(L"so_create_right_small"); - case SO_DELETE_LEFT: - return getResourceImage(L"so_delete_left_small"); - case SO_DELETE_RIGHT: - return getResourceImage(L"so_delete_right_small"); - case SO_MOVE_LEFT_FROM: - return getResourceImage(L"so_move_left_source_small"); - case SO_MOVE_LEFT_TO: - return getResourceImage(L"so_move_left_target_small"); - case SO_MOVE_RIGHT_FROM: - return getResourceImage(L"so_move_right_source_small"); - case SO_MOVE_RIGHT_TO: - return getResourceImage(L"so_move_right_target_small"); - case SO_OVERWRITE_LEFT: - return getResourceImage(L"so_update_left_small"); - case SO_OVERWRITE_RIGHT: - return getResourceImage(L"so_update_right_small"); - case SO_COPY_METADATA_TO_LEFT: - return getResourceImage(L"so_move_left_small"); - case SO_COPY_METADATA_TO_RIGHT: - return getResourceImage(L"so_move_right_small"); - case SO_DO_NOTHING: - return getResourceImage(L"so_none_small"); - case SO_EQUAL: - return getResourceImage(L"cat_equal_small"); - case SO_UNRESOLVED_CONFLICT: - return getResourceImage(L"cat_conflict_small"); - } - assert(false); - return wxNullBitmap; -} - - -wxBitmap zen::getCmpResultImage(CompareFilesResult cmpResult) -{ - switch (cmpResult) - { - case FILE_LEFT_SIDE_ONLY: - return getResourceImage(L"cat_left_only_small"); - case FILE_RIGHT_SIDE_ONLY: - return getResourceImage(L"cat_right_only_small"); - case FILE_LEFT_NEWER: - return getResourceImage(L"cat_left_newer_small"); - case FILE_RIGHT_NEWER: - return getResourceImage(L"cat_right_newer_small"); - case FILE_DIFFERENT_CONTENT: - return getResourceImage(L"cat_different_small"); - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal - return getResourceImage(L"cat_equal_small"); - case FILE_CONFLICT: - return getResourceImage(L"cat_conflict_small"); - } - assert(false); - return wxNullBitmap; -} diff --git a/FreeFileSync/Source/ui/custom_grid.h b/FreeFileSync/Source/ui/custom_grid.h deleted file mode 100755 index c5353ba9..00000000 --- a/FreeFileSync/Source/ui/custom_grid.h +++ /dev/null @@ -1,84 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#ifndef CUSTOM_GRID_H_8405817408327894 -#define CUSTOM_GRID_H_8405817408327894 - -#include -#include "grid_view.h" -#include "column_attr.h" -#include "../lib/icon_buffer.h" - - -namespace zen -{ -//setup grid to show grid view within three components: -namespace gridview -{ -void init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, const std::shared_ptr& gridDataView); - -std::vector convertConfig(const std::vector< ColumnAttributeRim>& attribs); //+ make consistent -std::vector convertConfig(const std::vector& attribs); // - -void highlightSyncAction(Grid& gridCenter, bool value); - -void setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool show, IconBuffer::IconSize sz); - -void setItemPathForm(Grid& grid, ItemPathFormat fmt); //only for left/right grid - -void refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight); - -void setScrollMaster(Grid& grid); - -//mark rows selected in navigation/compressed tree and navigate to leading object -void setNavigationMarker(Grid& gridLeft, - std::unordered_set&& markedFilesAndLinks,//mark files/symlinks directly within a container - std::unordered_set&& markedContainer); //mark full container including child-objects -} - -wxBitmap getSyncOpImage(SyncOperation syncOp); -wxBitmap getCmpResultImage(CompareFilesResult cmpResult); - - -//---------- custom events for middle grid ---------- - -//(UN-)CHECKING ROWS FROM SYNCHRONIZATION -extern const wxEventType EVENT_GRID_CHECK_ROWS; -//SELECTING SYNC DIRECTION -extern const wxEventType EVENT_GRID_SYNC_DIRECTION; - -struct CheckRowsEvent : public wxCommandEvent -{ - CheckRowsEvent(size_t rowFirst, size_t rowLast, bool setIncluded) : wxCommandEvent(EVENT_GRID_CHECK_ROWS), rowFirst_(rowFirst), rowLast_(rowLast), setIncluded_(setIncluded) { assert(rowFirst <= rowLast); } - wxEvent* Clone() const override { return new CheckRowsEvent(*this); } - - const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) - const size_t rowLast_; //range is empty when clearing selection - const bool setIncluded_; -}; - - -struct SyncDirectionEvent : public wxCommandEvent -{ - SyncDirectionEvent(size_t rowFirst, size_t rowLast, SyncDirection direction) : wxCommandEvent(EVENT_GRID_SYNC_DIRECTION), rowFirst_(rowFirst), rowLast_(rowLast), direction_(direction) { assert(rowFirst <= rowLast); } - wxEvent* Clone() const override { return new SyncDirectionEvent(*this); } - - const size_t rowFirst_; //see CheckRowsEvent - const size_t rowLast_; // - const SyncDirection direction_; -}; - -using CheckRowsEventFunction = void (wxEvtHandler::*)(CheckRowsEvent&); -using SyncDirectionEventFunction = void (wxEvtHandler::*)(SyncDirectionEvent&); - -#define CheckRowsEventHandler(func) \ - (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(CheckRowsEventFunction, &func) - -#define SyncDirectionEventHandler(func) \ - (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(SyncDirectionEventFunction, &func) -} - -#endif //CUSTOM_GRID_H_8405817408327894 diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp new file mode 100755 index 00000000..2a440adc --- /dev/null +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -0,0 +1,1817 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "file_grid.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../file_hierarchy.h" + +using namespace zen; +using namespace filegrid; + + +const wxEventType zen::EVENT_GRID_CHECK_ROWS = wxNewEventType(); +const wxEventType zen::EVENT_GRID_SYNC_DIRECTION = wxNewEventType(); + +namespace +{ +//let's NOT create wxWidgets objects statically: +inline wxColor getColorOrange () { return { 238, 201, 0 }; } +inline wxColor getColorGrey () { return { 212, 208, 200 }; } +inline wxColor getColorYellow () { return { 247, 252, 62 }; } +//inline wxColor getColorYellowLight() { return { 253, 252, 169 }; } +inline wxColor getColorCmpRed () { return { 255, 185, 187 }; } +inline wxColor getColorSyncBlue () { return { 185, 188, 255 }; } +inline wxColor getColorSyncGreen() { return { 196, 255, 185 }; } +inline wxColor getColorNotActive() { return { 228, 228, 228 }; } //light grey +inline wxColor getColorGridLine () { return { 192, 192, 192 }; } //light grey + +const size_t ROW_COUNT_IF_NO_DATA = 0; + +/* +class hierarchy: + GridDataBase + /|\ + ________________|________________ + | | + GridDataRim | + /|\ | + __________|__________ | + | | | + GridDataLeft GridDataRight GridDataCenter +*/ + +std::pair getVisibleRows(const Grid& grid) //returns range [from, to) +{ + const wxSize clientSize = grid.getMainWin().GetClientSize(); + if (clientSize.GetHeight() > 0) + { + const wxPoint topLeft = grid.CalcUnscrolledPosition(wxPoint(0, 0)); + const wxPoint bottom = grid.CalcUnscrolledPosition(wxPoint(0, clientSize.GetHeight() - 1)); + + const ptrdiff_t rowCount = grid.getRowCount(); + const ptrdiff_t rowFrom = grid.getRowAtPos(topLeft.y); //return -1 for invalid position, rowCount if out of range + const ptrdiff_t rowTo = grid.getRowAtPos(bottom.y); + if (rowFrom >= 0 && rowTo >= 0) + return std::make_pair(rowFrom, std::min(rowTo + 1, rowCount)); + } + assert(false); + return std::make_pair(0, 0); +} + + +void fillBackgroundDefaultColorAlternating(wxDC& dc, const wxRect& rect, bool evenRowNumber) +{ + //alternate background color to improve readability (while lacking cell borders) + if (!evenRowNumber) + { + //accessibility, support high-contrast schemes => work with user-defined background color! + const auto backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + + auto incChannel = [](unsigned char c, int diff) { return static_cast(std::max(0, std::min(255, c + diff))); }; + + auto getAdjustedColor = [&](int diff) + { + return wxColor(incChannel(backCol.Red (), diff), + incChannel(backCol.Green(), diff), + incChannel(backCol.Blue (), diff)); + }; + + auto colorDist = [](const wxColor& lhs, const wxColor& rhs) //just some metric + { + return numeric::power<2>(static_cast(lhs.Red ()) - static_cast(rhs.Red ())) + + numeric::power<2>(static_cast(lhs.Green()) - static_cast(rhs.Green())) + + numeric::power<2>(static_cast(lhs.Blue ()) - static_cast(rhs.Blue ())); + }; + + const int signLevel = colorDist(backCol, *wxBLACK) < colorDist(backCol, *wxWHITE) ? 1 : -1; //brighten or darken + + const wxColor colOutter = getAdjustedColor(signLevel * 14); //just some very faint gradient to avoid visual distraction + const wxColor colInner = getAdjustedColor(signLevel * 11); // + + //clearArea(dc, rect, backColAlt); + + //add some nice background gradient + wxRect rectUpper = rect; + rectUpper.height /= 2; + wxRect rectLower = rect; + rectLower.y += rectUpper.height; + rectLower.height -= rectUpper.height; + dc.GradientFillLinear(rectUpper, colOutter, colInner, wxSOUTH); + dc.GradientFillLinear(rectLower, colOutter, colInner, wxNORTH); + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +} + + +class IconUpdater; +class GridEventManager; +class GridDataLeft; +class GridDataRight; + +struct IconManager +{ + IconManager(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer::IconSize sz) : + iconBuffer(sz), + dirIcon (IconBuffer::genericDirIcon (sz)), + linkOverlayIcon(IconBuffer::linkOverlayIcon(sz)), + iconUpdater(std::make_unique(provLeft, provRight, iconBuffer)) {} + + void startIconUpdater(); + IconBuffer& refIconBuffer() { return iconBuffer; } + + const wxBitmap& getGenericDirIcon () const { return dirIcon; } + const wxBitmap& getLinkOverlayIcon() const { return linkOverlayIcon; } + +private: + IconBuffer iconBuffer; + const wxBitmap dirIcon; + const wxBitmap linkOverlayIcon; + + std::unique_ptr iconUpdater; //bind ownership to GridDataRim<>! +}; + +//######################################################################################################## + +class GridDataBase : public GridData +{ +public: + GridDataBase(Grid& grid, const std::shared_ptr& gridDataView) : grid_(grid), gridDataView_(gridDataView) {} + + void holdOwnership(const std::shared_ptr& evtMgr) { evtMgr_ = evtMgr; } + GridEventManager* getEventManager() { return evtMgr_.get(); } + + FileView& getDataView() { return *gridDataView_; } + +protected: + Grid& refGrid() { return grid_; } + const Grid& refGrid() const { return grid_; } + + const FileView* getGridDataView() const { return gridDataView_.get(); } + + const FileSystemObject* getRawData(size_t row) const + { + if (auto view = getGridDataView()) + return view->getObject(row); + return nullptr; + } + +private: + size_t getRowCount() const override + { + if (!gridDataView_ || gridDataView_->rowsTotal() == 0) + return ROW_COUNT_IF_NO_DATA; + + return gridDataView_->rowsOnView(); + //return std::max(MIN_ROW_COUNT, gridDataView_ ? gridDataView_->rowsOnView() : 0); + } + + std::shared_ptr evtMgr_; + Grid& grid_; + const std::shared_ptr gridDataView_; +}; + +//######################################################################################################## + +template +class GridDataRim : public GridDataBase +{ +public: + GridDataRim(const std::shared_ptr& gridDataView, Grid& grid) : GridDataBase(grid, gridDataView) {} + + void setIconManager(const std::shared_ptr& iconMgr) { iconMgr_ = iconMgr; } + + void setItemPathForm(ItemPathFormat fmt) { itemPathFormat = fmt; } + + void getUnbufferedIconsForPreload(std::vector>& newLoad) //return (priority, filepath) list + { + if (iconMgr_) + { + const auto& rowsOnScreen = getVisibleRows(refGrid()); + const ptrdiff_t visibleRowCount = rowsOnScreen.second - rowsOnScreen.first; + + //preload icons not yet on screen: + const int preloadSize = 2 * std::max(20, visibleRowCount); //:= sum of lines above and below of visible range to preload + //=> use full visible height to handle "next page" command and a minimum of 20 for excessive mouse wheel scrolls + + for (ptrdiff_t i = 0; i < preloadSize; ++i) + { + const ptrdiff_t currentRow = rowsOnScreen.first - (preloadSize + 1) / 2 + getAlternatingPos(i, visibleRowCount + preloadSize); //for odd preloadSize start one row earlier + + const IconInfo ii = getIconInfo(currentRow); + if (ii.type == IconInfo::ICON_PATH) + if (!iconMgr_->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath())) + newLoad.emplace_back(i, ii.fsObj->template getAbstractPath()); //insert least-important items on outer rim first + } + } + } + + void updateNewAndGetUnbufferedIcons(std::vector& newLoad) //loads all not yet drawn icons + { + if (iconMgr_) + { + const auto& rowsOnScreen = getVisibleRows(refGrid()); + const ptrdiff_t visibleRowCount = rowsOnScreen.second - rowsOnScreen.first; + + //loop over all visible rows + for (ptrdiff_t i = 0; i < visibleRowCount; ++i) + { + //alternate when adding rows: first, last, first + 1, last - 1 ... + const ptrdiff_t currentRow = rowsOnScreen.first + getAlternatingPos(i, visibleRowCount); + + if (isFailedLoad(currentRow)) //find failed attempts to load icon + { + const IconInfo ii = getIconInfo(currentRow); + if (ii.type == IconInfo::ICON_PATH) + { + //test if they are already loaded in buffer: + if (iconMgr_->refIconBuffer().readyForRetrieval(ii.fsObj->template getAbstractPath())) + { + //do a *full* refresh for *every* failed load to update partial DC updates while scrolling + refGrid().refreshCell(currentRow, static_cast(ColumnTypeRim::ITEM_PATH)); + setFailedLoad(currentRow, false); + } + else //not yet in buffer: mark for async. loading + newLoad.push_back(ii.fsObj->template getAbstractPath()); + } + } + } + } + } + +private: + bool isFailedLoad(size_t row) const { return row < failedLoads.size() ? failedLoads[row] != 0 : false; } + + void setFailedLoad(size_t row, bool failed = true) + { + if (failedLoads.size() != refGrid().getRowCount()) + failedLoads.resize(refGrid().getRowCount()); + + if (row < failedLoads.size()) + failedLoads[row] = failed; + } + + //icon buffer will load reversely, i.e. if we want to go from inside out, we need to start from outside in + static size_t getAlternatingPos(size_t pos, size_t total) + { + assert(pos < total); + return pos % 2 == 0 ? pos / 2 : total - 1 - pos / 2; + } + +protected: + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override + { + if (enabled) + { + if (selected) + dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST); + //ignore focus + else + { + //alternate background color to improve readability (while lacking cell borders) + if (getRowDisplayType(row) == DisplayType::NORMAL) + fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); + else + clearArea(dc, rect, getBackGroundColor(row)); + + //draw horizontal border if required + DisplayType dispTp = getRowDisplayType(row); + if (dispTp != DisplayType::NORMAL && + dispTp == getRowDisplayType(row + 1)) + { + wxDCPenChanger dummy2(dc, getColorGridLine()); + dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); + } + } + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + } + + wxColor getBackGroundColor(size_t row) const + { + //accessibility: always set both foreground AND background colors! + // => harmonize with renderCell()! + + switch (getRowDisplayType(row)) + { + case DisplayType::NORMAL: + break; + case DisplayType::FOLDER: + return getColorGrey(); + case DisplayType::SYMLINK: + return getColorOrange(); + case DisplayType::INACTIVE: + return getColorNotActive(); + } + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + } + +private: + enum class DisplayType + { + NORMAL, + FOLDER, + SYMLINK, + INACTIVE, + }; + + DisplayType getRowDisplayType(size_t row) const + { + const FileSystemObject* fsObj = getRawData(row); + if (!fsObj ) + return DisplayType::NORMAL; + + //mark filtered rows + if (!fsObj->isActive()) + return DisplayType::INACTIVE; + + if (fsObj->isEmpty()) //always show not existing files/dirs/symlinks as empty + return DisplayType::NORMAL; + + DisplayType output = DisplayType::NORMAL; + //mark directories and symlinks + visitFSObject(*fsObj, [&](const FolderPair& folder) { output = DisplayType::FOLDER; }, + [](const FilePair& file) {}, + [&](const SymlinkPair& symlink) { output = DisplayType::SYMLINK; }); + + return output; + } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const FileSystemObject* fsObj = getRawData(row)) + { + const ColumnTypeRim colTypeRim = static_cast(colType); + + std::wstring value; + visitFSObject(*fsObj, [&](const FolderPair& folder) + { + value = [&] + { + if (folder.isEmpty()) + return std::wstring(); + + switch (colTypeRim) + { + case ColumnTypeRim::ITEM_PATH: + switch (itemPathFormat) + { + case ItemPathFormat::FULL_PATH: + return AFS::getDisplayPath(folder.getAbstractPath()); + case ItemPathFormat::RELATIVE_PATH: + return utfTo(folder.getRelativePath()); + case ItemPathFormat::ITEM_NAME: + return utfTo(folder.getItemName()); + } + break; + case ColumnTypeRim::SIZE: + return L"<" + _("Folder") + L">"; + case ColumnTypeRim::DATE: + return std::wstring(); + case ColumnTypeRim::EXTENSION: + return std::wstring(); + } + assert(false); + return std::wstring(); + }(); + }, + + [&](const FilePair& file) + { + value = [&] + { + if (file.isEmpty()) + return std::wstring(); + + switch (colTypeRim) + { + case ColumnTypeRim::ITEM_PATH: + switch (itemPathFormat) + { + case ItemPathFormat::FULL_PATH: + return AFS::getDisplayPath(file.getAbstractPath()); + case ItemPathFormat::RELATIVE_PATH: + return utfTo(file.getRelativePath()); + case ItemPathFormat::ITEM_NAME: + return utfTo(file.getItemName()); + } + break; + case ColumnTypeRim::SIZE: + //return utfTo(file.getFileId()); // -> test file id + return formatNumber(file.getFileSize()); + case ColumnTypeRim::DATE: + return formatUtcToLocalTime(file.getLastWriteTime()); + case ColumnTypeRim::EXTENSION: + return utfTo(getFileExtension(file.getItemName())); + } + assert(false); + return std::wstring(); + }(); + }, + + [&](const SymlinkPair& symlink) + { + value = [&] + { + if (symlink.isEmpty()) + return std::wstring(); + + switch (colTypeRim) + { + case ColumnTypeRim::ITEM_PATH: + switch (itemPathFormat) + { + case ItemPathFormat::FULL_PATH: + return AFS::getDisplayPath(symlink.getAbstractPath()); + case ItemPathFormat::RELATIVE_PATH: + return utfTo(symlink.getRelativePath()); + case ItemPathFormat::ITEM_NAME: + return utfTo(symlink.getItemName()); + } + break; + case ColumnTypeRim::SIZE: + return L"<" + _("Symlink") + L">"; + case ColumnTypeRim::DATE: + return formatUtcToLocalTime(symlink.getLastWriteTime()); + case ColumnTypeRim::EXTENSION: + return utfTo(getFileExtension(symlink.getItemName())); + } + assert(false); + return std::wstring(); + }(); + }); + return value; + } + //if data is not found: + return std::wstring(); + } + + static const int GAP_SIZE = 2; + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + //don't forget to harmonize with getBestSize()!!! + + const bool isActive = [&] + { + if (const FileSystemObject* fsObj = this->getRawData(row)) + return fsObj->isActive(); + return true; + }(); + + wxDCTextColourChanger dummy(dc); + if (!isActive) + dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + else if (getRowDisplayType(row) != DisplayType::NORMAL) + dummy.Set(*wxBLACK); //accessibility: always set both foreground AND background colors! + + wxRect rectTmp = rect; + + auto drawTextBlock = [&](const std::wstring& text) + { + rectTmp.x += GAP_SIZE; + rectTmp.width -= GAP_SIZE; + const wxSize extent = drawCellText(dc, rectTmp, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + rectTmp.x += extent.GetWidth(); + rectTmp.width -= extent.GetWidth(); + }; + + const std::wstring cellValue = getValue(row, colType); + + switch (static_cast(colType)) + { + case ColumnTypeRim::ITEM_PATH: + { + if (!iconMgr_) + drawTextBlock(cellValue); + else + { + auto it = cellValue.end(); + while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate + { + --it; + if (*it == '\\' || *it == '/') + { + ++it; + break; + } + } + const std::wstring pathPrefix(cellValue.begin(), it); + const std::wstring itemName(it, cellValue.end()); + + // Partitioning: + // __________________________________________________ + // | gap | path prefix | gap | icon | gap | item name | + // -------------------------------------------------- + if (!pathPrefix.empty()) + drawTextBlock(pathPrefix); + + //draw file icon + rectTmp.x += GAP_SIZE; + rectTmp.width -= GAP_SIZE; + + const int iconSize = iconMgr_->refIconBuffer().getSize(); + if (rectTmp.GetWidth() >= iconSize) + { + //whenever there's something new to render on screen, start up watching for failed icon drawing: + //=> ideally it would suffice to start watching only when scrolling grid or showing new grid content, but this solution is more robust + //and the icon updater will stop automatically when finished anyway + //Note: it's not sufficient to start up on failed icon loads only, since we support prefetching of not yet visible rows!!! + iconMgr_->startIconUpdater(); + + const IconInfo ii = getIconInfo(row); + + wxBitmap fileIcon; + switch (ii.type) + { + case IconInfo::FOLDER: + fileIcon = iconMgr_->getGenericDirIcon(); + break; + + case IconInfo::ICON_PATH: + if (Opt tmpIco = iconMgr_->refIconBuffer().retrieveFileIcon(ii.fsObj->template getAbstractPath())) + fileIcon = *tmpIco; + else + { + setFailedLoad(row); //save status of failed icon load -> used for async. icon loading + //falsify only! we want to avoid writing incorrect success values when only partially updating the DC, e.g. when scrolling, + //see repaint behavior of ::ScrollWindow() function! + fileIcon = iconMgr_->refIconBuffer().getIconByExtension(ii.fsObj->template getItemName()); //better than nothing + } + break; + + case IconInfo::EMPTY: + break; + } + + if (fileIcon.IsOk()) + { + wxRect rectIcon = rectTmp; + rectIcon.width = iconSize; //support small thumbnail centering + + auto drawIcon = [&](const wxBitmap& icon) + { + if (isActive) + drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_CENTER); + else + drawBitmapRtlNoMirror(dc, wxBitmap(icon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)), //treat all channels equally! + rectIcon, wxALIGN_CENTER); + }; + + drawIcon(fileIcon); + + if (ii.drawAsLink) + drawIcon(iconMgr_->getLinkOverlayIcon()); + } + } + rectTmp.x += iconSize; + rectTmp.width -= iconSize; + + drawTextBlock(itemName); + } + } + break; + + case ColumnTypeRim::SIZE: + if (refGrid().GetLayoutDirection() != wxLayout_RightToLeft) + { + rectTmp.width -= GAP_SIZE; //have file size right-justified (but don't change for RTL languages) + drawCellText(dc, rectTmp, cellValue, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + } + else + drawTextBlock(cellValue); + break; + + case ColumnTypeRim::DATE: + case ColumnTypeRim::EXTENSION: + drawTextBlock(cellValue); + break; + } + } + + int getBestSize(wxDC& dc, size_t row, ColumnType colType) override + { + // Partitioning: + // ________________________________________________________ + // | gap | path prefix | gap | icon | gap | item name | gap | + // -------------------------------------------------------- + + const std::wstring cellValue = getValue(row, colType); + + if (static_cast(colType) == ColumnTypeRim::ITEM_PATH && iconMgr_) + { + auto it = cellValue.end(); + while (it != cellValue.begin()) //reverse iteration: 1. check 2. decrement 3. evaluate + { + --it; + if (*it == '\\' || *it == '/') + { + ++it; + break; + } + } + const std::wstring pathPrefix(cellValue.begin(), it); + const std::wstring itemName(it, cellValue.end()); + + int bestSize = 0; + if (!pathPrefix.empty()) + bestSize += GAP_SIZE + dc.GetTextExtent(pathPrefix).GetWidth(); + + bestSize += GAP_SIZE + iconMgr_->refIconBuffer().getSize(); + bestSize += GAP_SIZE + dc.GetTextExtent(itemName).GetWidth() + GAP_SIZE; + return bestSize; + } + else + return GAP_SIZE + dc.GetTextExtent(cellValue).GetWidth() + GAP_SIZE; + // + 1 pix for cell border line ? -> not used anymore! + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeRim::ITEM_PATH: + switch (itemPathFormat) + { + case ItemPathFormat::FULL_PATH: + return _("Full path"); + case ItemPathFormat::RELATIVE_PATH: + return _("Relative path"); + case ItemPathFormat::ITEM_NAME: + return _("Item name"); + } + assert(false); + break; + case ColumnTypeRim::SIZE: + return _("Size"); + case ColumnTypeRim::DATE: + return _("Date"); + case ColumnTypeRim::EXTENSION: + return _("Extension"); + } + //assert(false); may be ColumnType::NONE + return std::wstring(); + } + + void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); + + rectInside.x += COLUMN_GAP_LEFT; + rectInside.width -= COLUMN_GAP_LEFT; + drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); + + //draw sort marker + if (getGridDataView()) + { + auto sortInfo = getGridDataView()->getSortInfo(); + if (sortInfo) + { + if (colType == static_cast(sortInfo->type) && (side == LEFT_SIDE) == sortInfo->onLeft) + { + const wxBitmap& marker = getResourceImage(sortInfo->ascending ? L"sortAscending" : L"sortDescending"); + drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL); + } + } + } + } + + struct IconInfo + { + enum IconType + { + EMPTY, + FOLDER, + ICON_PATH, + }; + IconType type = EMPTY; + const FileSystemObject* fsObj = nullptr; //only set if type != EMPTY + bool drawAsLink = false; + }; + + IconInfo getIconInfo(size_t row) const //return ICON_FILE_FOLDER if row points to a folder + { + IconInfo out; + + const FileSystemObject* fsObj = getRawData(row); + if (fsObj && !fsObj->isEmpty()) + { + out.fsObj = fsObj; + + visitFSObject(*fsObj, [&](const FolderPair& folder) + { + out.type = IconInfo::FOLDER; + out.drawAsLink = folder.isFollowedSymlink(); + }, + + [&](const FilePair& file) + { + out.type = IconInfo::ICON_PATH; + out.drawAsLink = file.isFollowedSymlink() || hasLinkExtension(file.getItemName()); + }, + + [&](const SymlinkPair& symlink) + { + out.type = IconInfo::ICON_PATH; + out.drawAsLink = true; + }); + } + return out; + } + + std::wstring getToolTip(size_t row, ColumnType colType) const override + { + std::wstring toolTip; + + if (const FileSystemObject* fsObj = getRawData(row)) + if (!fsObj->isEmpty()) + { + toolTip = getGridDataView() && getGridDataView()->getFolderPairCount() > 1 ? + AFS::getDisplayPath(fsObj->getAbstractPath()) : + utfTo(fsObj->getRelativePath()); + + visitFSObject(*fsObj, [](const FolderPair& folder) {}, + [&](const FilePair& file) + { + toolTip += L"\n" + + _("Size:") + L" " + zen::formatFilesizeShort(file.getFileSize()) + L"\n" + + _("Date:") + L" " + zen::formatUtcToLocalTime(file.getLastWriteTime()); + }, + + [&](const SymlinkPair& symlink) + { + toolTip += L"\n" + + _("Date:") + L" " + zen::formatUtcToLocalTime(symlink.getLastWriteTime()); + }); + } + return toolTip; + } + + std::shared_ptr iconMgr_; //optional + ItemPathFormat itemPathFormat = ItemPathFormat::FULL_PATH; + + std::vector failedLoads; //effectively a vector of size "number of rows" + Opt renderBuf; //avoid costs of recreating this temporary variable +}; + + +class GridDataLeft : public GridDataRim +{ +public: + GridDataLeft(const std::shared_ptr& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} + + void setNavigationMarker(std::unordered_set&& markedFilesAndLinks, + std::unordered_set&& markedContainer) + { + markedFilesAndLinks_.swap(markedFilesAndLinks); + markedContainer_ .swap(markedContainer); + } + +private: + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override + { + GridDataRim::renderRowBackgound(dc, rect, row, enabled, selected); + + //mark rows selected on overview panel: + if (enabled && !selected) + { + const bool markRow = [&] + { + if (const FileSystemObject* fsObj = getRawData(row)) + { + if (markedFilesAndLinks_.find(fsObj) != markedFilesAndLinks_.end()) //mark files/links directly + return true; + + if (auto folder = dynamic_cast(fsObj)) + { + if (markedContainer_.find(folder) != markedContainer_.end()) //mark directories which *are* the given ContainerObject* + return true; + } + + //mark all objects which have the ContainerObject as *any* matching ancestor + const ContainerObject* parent = &(fsObj->parent()); + for (;;) + { + if (markedContainer_.find(parent) != markedContainer_.end()) + return true; + + if (auto folder = dynamic_cast(parent)) + parent = &(folder->parent()); + else + break; + } + } + return false; + }(); + + if (markRow) + { + wxRect rectTmp = rect; + rectTmp.width /= 20; + dc.GradientFillLinear(rectTmp, Grid::getColorSelectionGradientFrom(), GridDataRim::getBackGroundColor(row), wxEAST); + } + } + } + + std::unordered_set markedFilesAndLinks_; //mark files/symlinks directly within a container + std::unordered_set markedContainer_; //mark full container including all child-objects + //DO NOT DEREFERENCE!!!! NOT GUARANTEED TO BE VALID!!! +}; + + +class GridDataRight : public GridDataRim +{ +public: + GridDataRight(const std::shared_ptr& gridDataView, Grid& grid) : GridDataRim(gridDataView, grid) {} +}; + +//######################################################################################################## + +class GridDataCenter : public GridDataBase +{ +public: + GridDataCenter(const std::shared_ptr& gridDataView, Grid& grid) : + GridDataBase(grid, gridDataView), + toolTip_(grid) {} //tool tip must not live longer than grid! + + void onSelectBegin() + { + selectionInProgress_ = true; + refGrid().clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion! + toolTip_.hide(); //handle custom tooltip + } + + void onSelectEnd(size_t rowFirst, size_t rowLast, HoverArea rowHover, ptrdiff_t clickInitRow) + { + refGrid().clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion! + + //issue custom event + if (selectionInProgress_) //don't process selections initiated by right-click + if (rowFirst < rowLast && rowLast <= refGrid().getRowCount()) //empty? probably not in this context + if (wxEvtHandler* evtHandler = refGrid().GetEventHandler()) + switch (static_cast(rowHover)) + { + case HoverAreaCenter::CHECK_BOX: + if (const FileSystemObject* fsObj = getRawData(clickInitRow)) + { + const bool setIncluded = !fsObj->isActive(); + CheckRowsEvent evt(rowFirst, rowLast, setIncluded); + evtHandler->ProcessEvent(evt); + } + break; + case HoverAreaCenter::DIR_LEFT: + { + SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::LEFT); + evtHandler->ProcessEvent(evt); + } + break; + case HoverAreaCenter::DIR_NONE: + { + SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::NONE); + evtHandler->ProcessEvent(evt); + } + break; + case HoverAreaCenter::DIR_RIGHT: + { + SyncDirectionEvent evt(rowFirst, rowLast, SyncDirection::RIGHT); + evtHandler->ProcessEvent(evt); + } + break; + } + selectionInProgress_ = false; + + //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows) + wxPoint clientPos = refGrid().getMainWin().ScreenToClient(wxGetMousePosition()); + onMouseMovement(clientPos); + } + + void onMouseMovement(const wxPoint& clientPos) + { + //manage block highlighting and custom tooltip + if (!selectionInProgress_) + { + const wxPoint& topLeftAbs = refGrid().CalcUnscrolledPosition(clientPos); + const size_t row = refGrid().getRowAtPos(topLeftAbs.y); //return -1 for invalid position, rowCount if one past the end + const Grid::ColumnPosInfo cpi = refGrid().getColumnAtPos(topLeftAbs.x); //returns ColumnType::NONE if no column at x position! + + if (row < refGrid().getRowCount() && cpi.colType != ColumnType::NONE && + refGrid().getMainWin().GetClientRect().Contains(clientPos)) //cursor might have moved outside visible client area + showToolTip(row, static_cast(cpi.colType), refGrid().getMainWin().ClientToScreen(clientPos)); + else + toolTip_.hide(); + } + } + + void onMouseLeave() //wxEVT_LEAVE_WINDOW does not respect mouse capture! + { + toolTip_.hide(); //handle custom tooltip + } + + void highlightSyncAction(bool value) { highlightSyncAction_ = value; } + +private: + enum class HoverAreaCenter //each cell can be divided into four blocks concerning mouse selections + { + CHECK_BOX, + DIR_LEFT, + DIR_NONE, + DIR_RIGHT + }; + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (const FileSystemObject* fsObj = getRawData(row)) + switch (static_cast(colType)) + { + case ColumnTypeCenter::CHECKBOX: + break; + case ColumnTypeCenter::CMP_CATEGORY: + return getSymbol(fsObj->getCategory()); + case ColumnTypeCenter::SYNC_ACTION: + return getSymbol(fsObj->getSyncOperation()); + } + return std::wstring(); + } + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override + { + if (enabled) + { + if (selected) + dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST); + else + { + if (const FileSystemObject* fsObj = getRawData(row)) + { + if (fsObj->isActive()) + fillBackgroundDefaultColorAlternating(dc, rect, row % 2 == 0); + else + clearArea(dc, rect, getColorNotActive()); + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + } + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + } + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + auto drawHighlightBackground = [&](const FileSystemObject& fsObj, const wxColor& col) + { + if (enabled && !selected && fsObj.isActive()) //coordinate with renderRowBackgound()! + clearArea(dc, rect, col); + }; + + switch (static_cast(colType)) + { + case ColumnTypeCenter::CHECKBOX: + if (const FileSystemObject* fsObj = getRawData(row)) + { + const bool drawMouseHover = static_cast(rowHover) == HoverAreaCenter::CHECK_BOX; + + if (fsObj->isActive()) + drawBitmapRtlMirror(dc, getResourceImage(drawMouseHover ? L"checkbox_true_hover" : L"checkbox_true"), rect, wxALIGN_CENTER, renderBuf_); + else //default + drawBitmapRtlMirror(dc, getResourceImage(drawMouseHover ? L"checkbox_false_hover" : L"checkbox_false"), rect, wxALIGN_CENTER, renderBuf_); + } + break; + + case ColumnTypeCenter::CMP_CATEGORY: + if (const FileSystemObject* fsObj = getRawData(row)) + { + if (!highlightSyncAction_) + drawHighlightBackground(*fsObj, getBackGroundColorCmpCategory(fsObj)); + + wxRect rectTmp = rect; + { + //draw notch on left side + if (notch_.GetHeight() != rectTmp.GetHeight()) + notch_.Rescale(notch_.GetWidth(), rectTmp.GetHeight()); + + //wxWidgets screws up again and has wxALIGN_RIGHT off by one pixel! -> use wxALIGN_LEFT instead + const wxRect rectNotch(rectTmp.x + rectTmp.width - notch_.GetWidth(), rectTmp.y, notch_.GetWidth(), rectTmp.height); + drawBitmapRtlMirror(dc, notch_, rectNotch, wxALIGN_LEFT, renderBuf_); + rectTmp.width -= notch_.GetWidth(); + } + + if (!highlightSyncAction_) + drawBitmapRtlMirror(dc, getCmpResultImage(fsObj->getCategory()), rectTmp, wxALIGN_CENTER, renderBuf_); + else if (fsObj->getCategory() != FILE_EQUAL) //don't show = in both middle columns + drawBitmapRtlMirror(dc, greyScale(getCmpResultImage(fsObj->getCategory())), rectTmp, wxALIGN_CENTER, renderBuf_); + } + break; + + case ColumnTypeCenter::SYNC_ACTION: + if (const FileSystemObject* fsObj = getRawData(row)) + { + if (highlightSyncAction_) + drawHighlightBackground(*fsObj, getBackGroundColorSyncAction(fsObj)); + + //synchronization preview + const auto rowHoverCenter = rowHover == HoverArea::NONE ? HoverAreaCenter::CHECK_BOX : static_cast(rowHover); + switch (rowHoverCenter) + { + case HoverAreaCenter::DIR_LEFT: + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::LEFT)), rect, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, renderBuf_); + break; + case HoverAreaCenter::DIR_NONE: + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::NONE)), rect, wxALIGN_CENTER, renderBuf_); + break; + case HoverAreaCenter::DIR_RIGHT: + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->testSyncOperation(SyncDirection::RIGHT)), rect, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, renderBuf_); + break; + case HoverAreaCenter::CHECK_BOX: + if (highlightSyncAction_) + drawBitmapRtlMirror(dc, getSyncOpImage(fsObj->getSyncOperation()), rect, wxALIGN_CENTER, renderBuf_); + else if (fsObj->getSyncOperation() != SO_EQUAL) //don't show = in both middle columns + drawBitmapRtlMirror(dc, greyScale(getSyncOpImage(fsObj->getSyncOperation())), rect, wxALIGN_CENTER, renderBuf_); + break; + } + } + break; + } + } + + HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override + { + if (const FileSystemObject* const fsObj = getRawData(row)) + switch (static_cast(colType)) + { + case ColumnTypeCenter::CHECKBOX: + case ColumnTypeCenter::CMP_CATEGORY: + return static_cast(HoverAreaCenter::CHECK_BOX); + + case ColumnTypeCenter::SYNC_ACTION: + if (fsObj->getSyncOperation() == SO_EQUAL) //in sync-preview equal files shall be treated like a checkbox + return static_cast(HoverAreaCenter::CHECK_BOX); + // cell: + // ----------------------- + // | left | middle | right| + // ----------------------- + if (0 <= cellRelativePosX) + { + if (cellRelativePosX < cellWidth / 3) + return static_cast(HoverAreaCenter::DIR_LEFT); + else if (cellRelativePosX < 2 * cellWidth / 3) + return static_cast(HoverAreaCenter::DIR_NONE); + else if (cellRelativePosX < cellWidth) + return static_cast(HoverAreaCenter::DIR_RIGHT); + } + break; + } + return HoverArea::NONE; + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeCenter::CHECKBOX: + break; + case ColumnTypeCenter::CMP_CATEGORY: + return _("Category") + L" (F10)"; + case ColumnTypeCenter::SYNC_ACTION: + return _("Action") + L" (F10)"; + } + return std::wstring(); + } + + std::wstring getToolTip(ColumnType colType) const override { return getColumnLabel(colType); } + + void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override + { + switch (static_cast(colType)) + { + case ColumnTypeCenter::CHECKBOX: + drawColumnLabelBackground(dc, rect, false); + break; + + case ColumnTypeCenter::CMP_CATEGORY: + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); + + const wxBitmap& cmpIcon = getResourceImage(L"compare_small"); + drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? greyScale(cmpIcon) : cmpIcon, rectInside, wxALIGN_CENTER); + } + break; + + case ColumnTypeCenter::SYNC_ACTION: + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); + + const wxBitmap& syncIcon = getResourceImage(L"sync_small"); + drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? syncIcon : greyScale(syncIcon), rectInside, wxALIGN_CENTER); + } + break; + } + } + + static wxColor getBackGroundColorSyncAction(const FileSystemObject* fsObj) + { + if (fsObj) + { + if (!fsObj->isActive()) + return getColorNotActive(); + + switch (fsObj->getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_DO_NOTHING: + return getColorNotActive(); + case SO_EQUAL: + break; //usually white + + case SO_CREATE_NEW_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: + return getColorSyncBlue(); + + case SO_CREATE_NEW_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: + return getColorSyncGreen(); + + case SO_UNRESOLVED_CONFLICT: + return getColorYellow(); + } + } + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + } + + static wxColor getBackGroundColorCmpCategory(const FileSystemObject* fsObj) + { + if (fsObj) + { + if (!fsObj->isActive()) + return getColorNotActive(); + + switch (fsObj->getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + case FILE_LEFT_NEWER: + return getColorSyncBlue(); //COLOR_CMP_BLUE; + + case FILE_RIGHT_SIDE_ONLY: + case FILE_RIGHT_NEWER: + return getColorSyncGreen(); //COLOR_CMP_GREEN; + + case FILE_DIFFERENT_CONTENT: + return getColorCmpRed(); + case FILE_EQUAL: + break; //usually white + case FILE_CONFLICT: + case FILE_DIFFERENT_METADATA: //= sub-category of equal, but hint via background that sync direction follows conflict-setting + return getColorYellow(); + //return getColorYellowLight(); + } + } + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + } + + void showToolTip(size_t row, ColumnTypeCenter colType, wxPoint posScreen) + { + if (const FileSystemObject* fsObj = getRawData(row)) + { + switch (colType) + { + case ColumnTypeCenter::CHECKBOX: + case ColumnTypeCenter::CMP_CATEGORY: + { + const wchar_t* imageName = [&] + { + const CompareFilesResult cmpRes = fsObj->getCategory(); + switch (cmpRes) + { + case FILE_LEFT_SIDE_ONLY: + return L"cat_left_only"; + case FILE_RIGHT_SIDE_ONLY: + return L"cat_right_only"; + case FILE_LEFT_NEWER: + return L"cat_left_newer"; + case FILE_RIGHT_NEWER: + return L"cat_right_newer"; + case FILE_DIFFERENT_CONTENT: + return L"cat_different"; + case FILE_EQUAL: + case FILE_DIFFERENT_METADATA: //= sub-category of equal + return L"cat_equal"; + case FILE_CONFLICT: + return L"cat_conflict"; + } + assert(false); + return L""; + }(); + const auto& img = mirrorIfRtl(getResourceImage(imageName)); + toolTip_.show(getCategoryDescription(*fsObj), posScreen, &img); + } + break; + + case ColumnTypeCenter::SYNC_ACTION: + { + const wchar_t* imageName = [&] + { + const SyncOperation syncOp = fsObj->getSyncOperation(); + switch (syncOp) + { + case SO_CREATE_NEW_LEFT: + return L"so_create_left"; + case SO_CREATE_NEW_RIGHT: + return L"so_create_right"; + case SO_DELETE_LEFT: + return L"so_delete_left"; + case SO_DELETE_RIGHT: + return L"so_delete_right"; + case SO_MOVE_LEFT_FROM: + return L"so_move_left_source"; + case SO_MOVE_LEFT_TO: + return L"so_move_left_target"; + case SO_MOVE_RIGHT_FROM: + return L"so_move_right_source"; + case SO_MOVE_RIGHT_TO: + return L"so_move_right_target"; + case SO_OVERWRITE_LEFT: + return L"so_update_left"; + case SO_OVERWRITE_RIGHT: + return L"so_update_right"; + case SO_COPY_METADATA_TO_LEFT: + return L"so_move_left"; + case SO_COPY_METADATA_TO_RIGHT: + return L"so_move_right"; + case SO_DO_NOTHING: + return L"so_none"; + case SO_EQUAL: + return L"cat_equal"; + case SO_UNRESOLVED_CONFLICT: + return L"cat_conflict"; + }; + assert(false); + return L""; + }(); + const auto& img = mirrorIfRtl(getResourceImage(imageName)); + toolTip_.show(getSyncOpDescription(*fsObj), posScreen, &img); + } + break; + } + } + else + toolTip_.hide(); //if invalid row... + } + + bool highlightSyncAction_ = false; + bool selectionInProgress_ = false; + + Opt renderBuf_; //avoid costs of recreating this temporary variable + Tooltip toolTip_; + wxImage notch_ = getResourceImage(L"notch").ConvertToImage(); +}; + +//######################################################################################################## + +const wxEventType EVENT_ALIGN_SCROLLBARS = wxNewEventType(); + +class GridEventManager : private wxEvtHandler +{ +public: + GridEventManager(Grid& gridL, + Grid& gridC, + Grid& gridR, + GridDataCenter& provCenter) : + gridL_(gridL), gridC_(gridC), gridR_(gridR), + provCenter_(provCenter) + { + gridL_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnL), nullptr, this); + gridR_.Connect(EVENT_GRID_COL_RESIZE, GridColumnResizeEventHandler(GridEventManager::onResizeColumnR), nullptr, this); + + gridL_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownL), nullptr, this); + gridC_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownC), nullptr, this); + gridR_.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridEventManager::onKeyDownR), nullptr, this); + + gridC_.getMainWin().Connect(wxEVT_MOTION, wxMouseEventHandler(GridEventManager::onCenterMouseMovement), nullptr, this); + gridC_.getMainWin().Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(GridEventManager::onCenterMouseLeave ), nullptr, this); + + gridC_.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler (GridEventManager::onCenterSelectBegin), nullptr, this); + gridC_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onCenterSelectEnd ), nullptr, this); + + //clear selection of other grid when selecting on + gridL_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onGridSelectionL), nullptr, this); + gridR_.Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(GridEventManager::onGridSelectionR), nullptr, this); + + //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: + gridL_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridL), nullptr, this); + gridC_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridC), nullptr, this); + gridR_.getMainWin().Connect(wxEVT_PAINT, wxEventHandler(GridEventManager::onPaintGridR), nullptr, this); + + auto connectGridAccess = [&](Grid& grid, wxObjectEventFunction func) + { + grid.Connect(wxEVT_SCROLLWIN_TOP, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_BOTTOM, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_LINEUP, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_LINEDOWN, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_PAGEUP, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_PAGEDOWN, func, nullptr, this); + grid.Connect(wxEVT_SCROLLWIN_THUMBTRACK, func, nullptr, this); + //wxEVT_KILL_FOCUS -> there's no need to reset "scrollMaster" + //wxEVT_SET_FOCUS -> not good enough: + //e.g.: left grid has input, right grid is "scrollMaster" due to dragging scroll thumb via mouse. + //=> Next keyboard input on left does *not* emit focus change event, but still "scrollMaster" needs to change + //=> hook keyboard input instead of focus event: + grid.getMainWin().Connect(wxEVT_CHAR, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_KEY_UP, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_KEY_DOWN, func, nullptr, this); + + grid.getMainWin().Connect(wxEVT_LEFT_DOWN, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_LEFT_DCLICK, func, nullptr, this); + grid.getMainWin().Connect(wxEVT_RIGHT_DOWN, func, nullptr, this); + //grid.getMainWin().Connect(wxEVT_MOUSEWHEEL, func, nullptr, this); -> should be covered by wxEVT_SCROLLWIN_* + }; + connectGridAccess(gridL_, wxEventHandler(GridEventManager::onGridAccessL)); // + connectGridAccess(gridC_, wxEventHandler(GridEventManager::onGridAccessC)); //connect *after* onKeyDown() in order to receive callback *before*!!! + connectGridAccess(gridR_, wxEventHandler(GridEventManager::onGridAccessR)); // + + Connect(EVENT_ALIGN_SCROLLBARS, wxEventHandler(GridEventManager::onAlignScrollBars), NULL, this); + } + + ~GridEventManager() { assert(!scrollbarUpdatePending_); } + + void setScrollMaster(const Grid& grid) { scrollMaster_ = &grid; } + +private: + void onCenterSelectBegin(GridClickEvent& event) + { + provCenter_.onSelectBegin(); + event.Skip(); + } + + void onCenterSelectEnd(GridSelectEvent& event) + { + if (event.positive_) + { + if (event.mouseSelect_) + provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseSelect_->click.hoverArea_, event.mouseSelect_->click.row_); + else + provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::NONE, -1); + } + event.Skip(); + } + + void onCenterMouseMovement(wxMouseEvent& event) + { + provCenter_.onMouseMovement(event.GetPosition()); + event.Skip(); + } + + void onCenterMouseLeave(wxMouseEvent& event) + { + provCenter_.onMouseLeave(); + event.Skip(); + } + + void onGridSelectionL(GridSelectEvent& event) { onGridSelection(gridL_, gridR_); event.Skip(); } + void onGridSelectionR(GridSelectEvent& event) { onGridSelection(gridR_, gridL_); event.Skip(); } + + void onGridSelection(const Grid& grid, Grid& other) + { + if (!wxGetKeyState(WXK_CONTROL)) //clear other grid unless user is holding CTRL + other.clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion! + } + + void onKeyDownL(wxKeyEvent& event) { onKeyDown(event, gridL_); } + void onKeyDownC(wxKeyEvent& event) { onKeyDown(event, gridC_); } + void onKeyDownR(wxKeyEvent& event) { onKeyDown(event, gridR_); } + + void onKeyDown(wxKeyEvent& event, const Grid& grid) + { + int keyCode = event.GetKeyCode(); + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + //skip middle component when navigating via keyboard + const size_t row = grid.getGridCursor(); + + if (event.ShiftDown()) + ; + else if (event.ControlDown()) + ; + else + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + gridL_.setGridCursor(row); + gridL_.SetFocus(); + //since key event is likely originating from right grid, we need to set scrollMaster manually! + scrollMaster_ = &gridL_; //onKeyDown is called *after* onGridAccessL()! + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + gridR_.setGridCursor(row); + gridR_.SetFocus(); + scrollMaster_ = &gridR_; + return; //swallow event + } + + event.Skip(); + } + + void onResizeColumnL(GridColumnResizeEvent& event) { resizeOtherSide(gridL_, gridR_, event.colType_, event.offset_); } + void onResizeColumnR(GridColumnResizeEvent& event) { resizeOtherSide(gridR_, gridL_, event.colType_, event.offset_); } + + void resizeOtherSide(const Grid& src, Grid& trg, ColumnType type, int offset) + { + //find stretch factor of resized column: type is unique due to makeConsistent()! + std::vector cfgSrc = src.getColumnConfig(); + auto it = std::find_if(cfgSrc.begin(), cfgSrc.end(), [&](Grid::ColAttributes& ca) { return ca.type == type; }); + if (it == cfgSrc.end()) + return; + const int stretchSrc = it->stretch; + + //we do not propagate resizings on stretched columns to the other side: awkward user experience + if (stretchSrc > 0) + return; + + //apply resized offset to other side, but only if stretch factors match! + std::vector cfgTrg = trg.getColumnConfig(); + for (Grid::ColAttributes& ca : cfgTrg) + if (ca.type == type && ca.stretch == stretchSrc) + ca.offset = offset; + trg.setColumnConfig(cfgTrg); + } + + void onGridAccessL(wxEvent& event) { scrollMaster_ = &gridL_; event.Skip(); } + void onGridAccessC(wxEvent& event) { scrollMaster_ = &gridC_; event.Skip(); } + void onGridAccessR(wxEvent& event) { scrollMaster_ = &gridR_; event.Skip(); } + + void onPaintGridL(wxEvent& event) { onPaintGrid(gridL_); event.Skip(); } + void onPaintGridC(wxEvent& event) { onPaintGrid(gridC_); event.Skip(); } + void onPaintGridR(wxEvent& event) { onPaintGrid(gridR_); event.Skip(); } + + void onPaintGrid(const Grid& grid) + { + //align scroll positions of all three grids *synchronously* during paint event! (wxGTK has visible delay when this is done asynchronously, no delay on Windows) + + //determine lead grid + const Grid* lead = nullptr; + Grid* follow1 = nullptr; + Grid* follow2 = nullptr; + auto setGrids = [&](const Grid& l, Grid& f1, Grid& f2) { lead = &l; follow1 = &f1; follow2 = &f2; }; + + if (&gridC_ == scrollMaster_) + setGrids(gridC_, gridL_, gridR_); + else if (&gridR_ == scrollMaster_) + setGrids(gridR_, gridL_, gridC_); + else //default: left panel + setGrids(gridL_, gridC_, gridR_); + + //align other grids only while repainting the lead grid to avoid scrolling and updating a grid at the same time! + if (lead != &grid) return; + + auto scroll = [](Grid& target, int y) //support polling + { + //scroll vertically only - scrolling horizontally becomes annoying if left and right sides have different widths; + //e.g. h-scroll on left would be undone when scrolling vertically on right which doesn't have a h-scrollbar + int yOld = 0; + target.GetViewStart(nullptr, &yOld); + if (yOld != y) + target.Scroll(-1, y); //empirical test Windows/Ubuntu: this call does NOT trigger a wxEVT_SCROLLWIN event, which would incorrectly set "scrollMaster" to "&target"! + }; + int y = 0; + lead->GetViewStart(nullptr, &y); + scroll(*follow1, y); + scroll(*follow2, y); + + //harmonize placement of horizontal scrollbar to avoid grids getting out of sync! + //since this affects the grid that is currently repainted as well, we do work asynchronously! + //avoids at least this problem: remaining graphics artifact when changing from Grid::SB_SHOW_ALWAYS to Grid::SB_SHOW_NEVER at location of old scrollbar (Windows only) + + //perf note: send one async event at most, else they may accumulate and create perf issues, see grid.cpp + if (!scrollbarUpdatePending_) + { + scrollbarUpdatePending_ = true; + wxCommandEvent alignEvent(EVENT_ALIGN_SCROLLBARS); + AddPendingEvent(alignEvent); //waits until next idle event - may take up to a second if the app is busy on wxGTK! + } + } + + void onAlignScrollBars(wxEvent& event) + { + ZEN_ON_SCOPE_EXIT(scrollbarUpdatePending_ = false); + assert(scrollbarUpdatePending_); + + auto needsHorizontalScrollbars = [](const Grid& grid) -> bool + { + const wxWindow& mainWin = grid.getMainWin(); + return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth(); + //assuming Grid::updateWindowSizes() does its job well, this should suffice! + //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other + //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, ect...) + //while in fact both are NOT needed, this special case results in a bogus need for scrollbars! + //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083 + // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant + }; + + Grid::ScrollBarStatus sbStatusX = needsHorizontalScrollbars(gridL_) || + needsHorizontalScrollbars(gridR_) ? + Grid::SB_SHOW_ALWAYS : Grid::SB_SHOW_NEVER; + gridL_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); + gridC_.showScrollBars(sbStatusX, Grid::SB_SHOW_NEVER); + gridR_.showScrollBars(sbStatusX, Grid::SB_SHOW_AUTOMATIC); + } + + Grid& gridL_; + Grid& gridC_; + Grid& gridR_; + + const Grid* scrollMaster_ = nullptr; //for address check only; this needn't be the grid having focus! + //e.g. mouse wheel events should set window under cursor as scrollMaster, but *not* change focus + + GridDataCenter& provCenter_; + bool scrollbarUpdatePending_ = false; +}; +} + +//######################################################################################################## + +void filegrid::init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) +{ + const auto gridDataView = std::make_shared(); + + auto provLeft_ = std::make_shared(gridDataView, gridLeft); + auto provCenter_ = std::make_shared(gridDataView, gridCenter); + auto provRight_ = std::make_shared(gridDataView, gridRight); + + gridLeft .setDataProvider(provLeft_); //data providers reference grid => + gridCenter.setDataProvider(provCenter_); //ownership must belong *exclusively* to grid! + gridRight .setDataProvider(provRight_); + + auto evtMgr = std::make_shared(gridLeft, gridCenter, gridRight, *provCenter_); + provLeft_ ->holdOwnership(evtMgr); + provCenter_->holdOwnership(evtMgr); + provRight_ ->holdOwnership(evtMgr); + + gridCenter.enableColumnMove (false); + gridCenter.enableColumnResize(false); + + gridCenter.showRowLabel(false); + gridRight .showRowLabel(false); + + //gridLeft .showScrollBars(Grid::SB_SHOW_AUTOMATIC, Grid::SB_SHOW_NEVER); -> redundant: configuration happens in GridEventManager::onAlignScrollBars() + //gridCenter.showScrollBars(Grid::SB_SHOW_NEVER, Grid::SB_SHOW_NEVER); + + const int widthCheckbox = getResourceImage(L"checkbox_true").GetWidth() + 4 + getResourceImage(L"notch").GetWidth(); + const int widthCategory = 30; + const int widthAction = 45; + gridCenter.SetSize(widthCategory + widthCheckbox + widthAction, -1); + + gridCenter.setColumnConfig( + { + { static_cast(ColumnTypeCenter::CHECKBOX ), widthCheckbox, 0, true }, + { static_cast(ColumnTypeCenter::CMP_CATEGORY), widthCategory, 0, true }, + { static_cast(ColumnTypeCenter::SYNC_ACTION ), widthAction, 0, true }, + }); +} + + +FileView& filegrid::getDataView(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getDataView(); + + throw std::runtime_error("filegrid was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); +} + + +namespace +{ +class IconUpdater : private wxEvtHandler //update file icons periodically: use SINGLE instance to coordinate left and right grids in parallel +{ +public: + IconUpdater(GridDataLeft& provLeft, GridDataRight& provRight, IconBuffer& iconBuffer) : provLeft_(provLeft), provRight_(provRight), iconBuffer_(iconBuffer) + { + timer_.Connect(wxEVT_TIMER, wxEventHandler(IconUpdater::loadIconsAsynchronously), nullptr, this); + } + + void start() { if (!timer_.IsRunning()) timer_.Start(100); } //timer interval in [ms] + //don't check too often! give worker thread some time to fetch data + +private: + void stop() { if (timer_.IsRunning()) timer_.Stop(); } + + void loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons + { + std::vector> prefetchLoad; + provLeft_ .getUnbufferedIconsForPreload(prefetchLoad); + provRight_.getUnbufferedIconsForPreload(prefetchLoad); + + //make sure least-important prefetch rows are inserted first into workload (=> processed last) + //priority index nicely considers both grids at the same time! + std::sort(prefetchLoad.begin(), prefetchLoad.end(), [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); + + //last inserted items are processed first in icon buffer: + std::vector newLoad; + for (const auto& item : prefetchLoad) + newLoad.push_back(item.second); + + provRight_.updateNewAndGetUnbufferedIcons(newLoad); + provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); + + iconBuffer_.setWorkload(newLoad); + + if (newLoad.empty()) //let's only pay for IconUpdater when needed + stop(); + } + + GridDataLeft& provLeft_; + GridDataRight& provRight_; + IconBuffer& iconBuffer_; + wxTimer timer_; +}; + + +//resolve circular linker dependencies +inline +void IconManager::startIconUpdater() { if (iconUpdater) iconUpdater->start(); } +} + + +void filegrid::setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool show, IconBuffer::IconSize sz) +{ + auto* provLeft = dynamic_cast(gridLeft .getDataProvider()); + auto* provRight = dynamic_cast(gridRight.getDataProvider()); + + if (provLeft && provRight) + { + int iconHeight = 0; + if (show) + { + auto iconMgr = std::make_shared(*provLeft, *provRight, sz); + provLeft ->setIconManager(iconMgr); + provRight->setIconManager(iconMgr); + iconHeight = iconMgr->refIconBuffer().getSize(); + } + else + { + provLeft ->setIconManager(nullptr); + provRight->setIconManager(nullptr); + iconHeight = IconBuffer::getSize(IconBuffer::SIZE_SMALL); + } + + const int newRowHeight = std::max(iconHeight, gridLeft.getMainWin().GetCharHeight()) + 1; //add some space + + gridLeft .setRowHeight(newRowHeight); + gridCenter.setRowHeight(newRowHeight); + gridRight .setRowHeight(newRowHeight); + } + else + assert(false); +} + + +void filegrid::setItemPathForm(Grid& grid, ItemPathFormat fmt) +{ + if (auto* provLeft = dynamic_cast(grid.getDataProvider())) + provLeft->setItemPathForm(fmt); + else if (auto* provRight = dynamic_cast(grid.getDataProvider())) + provRight->setItemPathForm(fmt); + else + assert(false); + grid.Refresh(); +} + + +void filegrid::refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight) +{ + gridLeft .Refresh(); + gridCenter.Refresh(); + gridRight .Refresh(); +} + + +void filegrid::setScrollMaster(Grid& grid) +{ + if (auto prov = dynamic_cast(grid.getDataProvider())) + if (auto evtMgr = prov->getEventManager()) + { + evtMgr->setScrollMaster(grid); + return; + } + assert(false); +} + + +void filegrid::setNavigationMarker(Grid& gridLeft, + std::unordered_set&& markedFilesAndLinks, + std::unordered_set&& markedContainer) +{ + if (auto provLeft = dynamic_cast(gridLeft.getDataProvider())) + provLeft->setNavigationMarker(std::move(markedFilesAndLinks), std::move(markedContainer)); + else + assert(false); + gridLeft.Refresh(); +} + + +void filegrid::highlightSyncAction(Grid& gridCenter, bool value) +{ + if (auto provCenter = dynamic_cast(gridCenter.getDataProvider())) + provCenter->highlightSyncAction(value); + else + assert(false); + gridCenter.Refresh(); +} + + +wxBitmap zen::getSyncOpImage(SyncOperation syncOp) +{ + switch (syncOp) //evaluate comparison result and sync direction + { + case SO_CREATE_NEW_LEFT: + return getResourceImage(L"so_create_left_small"); + case SO_CREATE_NEW_RIGHT: + return getResourceImage(L"so_create_right_small"); + case SO_DELETE_LEFT: + return getResourceImage(L"so_delete_left_small"); + case SO_DELETE_RIGHT: + return getResourceImage(L"so_delete_right_small"); + case SO_MOVE_LEFT_FROM: + return getResourceImage(L"so_move_left_source_small"); + case SO_MOVE_LEFT_TO: + return getResourceImage(L"so_move_left_target_small"); + case SO_MOVE_RIGHT_FROM: + return getResourceImage(L"so_move_right_source_small"); + case SO_MOVE_RIGHT_TO: + return getResourceImage(L"so_move_right_target_small"); + case SO_OVERWRITE_LEFT: + return getResourceImage(L"so_update_left_small"); + case SO_OVERWRITE_RIGHT: + return getResourceImage(L"so_update_right_small"); + case SO_COPY_METADATA_TO_LEFT: + return getResourceImage(L"so_move_left_small"); + case SO_COPY_METADATA_TO_RIGHT: + return getResourceImage(L"so_move_right_small"); + case SO_DO_NOTHING: + return getResourceImage(L"so_none_small"); + case SO_EQUAL: + return getResourceImage(L"cat_equal_small"); + case SO_UNRESOLVED_CONFLICT: + return getResourceImage(L"cat_conflict_small"); + } + assert(false); + return wxNullBitmap; +} + + +wxBitmap zen::getCmpResultImage(CompareFilesResult cmpResult) +{ + switch (cmpResult) + { + case FILE_LEFT_SIDE_ONLY: + return getResourceImage(L"cat_left_only_small"); + case FILE_RIGHT_SIDE_ONLY: + return getResourceImage(L"cat_right_only_small"); + case FILE_LEFT_NEWER: + return getResourceImage(L"cat_left_newer_small"); + case FILE_RIGHT_NEWER: + return getResourceImage(L"cat_right_newer_small"); + case FILE_DIFFERENT_CONTENT: + return getResourceImage(L"cat_different_small"); + case FILE_EQUAL: + case FILE_DIFFERENT_METADATA: //= sub-category of equal + return getResourceImage(L"cat_equal_small"); + case FILE_CONFLICT: + return getResourceImage(L"cat_conflict_small"); + } + assert(false); + return wxNullBitmap; +} diff --git a/FreeFileSync/Source/ui/file_grid.h b/FreeFileSync/Source/ui/file_grid.h new file mode 100755 index 00000000..40853ceb --- /dev/null +++ b/FreeFileSync/Source/ui/file_grid.h @@ -0,0 +1,83 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef CUSTOM_GRID_H_8405817408327894 +#define CUSTOM_GRID_H_8405817408327894 + +#include +#include "file_view.h" +#include "file_grid_attr.h" +#include "../lib/icon_buffer.h" + + +namespace zen +{ +//setup grid to show grid view within three components: +namespace filegrid +{ +void init(Grid& gridLeft, Grid& gridCenter, Grid& gridRight); +FileView& getDataView(Grid& grid); + + +void highlightSyncAction(Grid& gridCenter, bool value); + +void setupIcons(Grid& gridLeft, Grid& gridCenter, Grid& gridRight, bool show, IconBuffer::IconSize sz); + +void setItemPathForm(Grid& grid, ItemPathFormat fmt); //only for left/right grid + +void refresh(Grid& gridLeft, Grid& gridCenter, Grid& gridRight); + +void setScrollMaster(Grid& grid); + +//mark rows selected in overview panel and navigate to leading object +void setNavigationMarker(Grid& gridLeft, + std::unordered_set&& markedFilesAndLinks,//mark files/symlinks directly within a container + std::unordered_set&& markedContainer); //mark full container including child-objects +} + +wxBitmap getSyncOpImage(SyncOperation syncOp); +wxBitmap getCmpResultImage(CompareFilesResult cmpResult); + + +//---------- custom events for middle grid ---------- + +//(UN-)CHECKING ROWS FROM SYNCHRONIZATION +extern const wxEventType EVENT_GRID_CHECK_ROWS; +//SELECTING SYNC DIRECTION +extern const wxEventType EVENT_GRID_SYNC_DIRECTION; + +struct CheckRowsEvent : public wxCommandEvent +{ + CheckRowsEvent(size_t rowFirst, size_t rowLast, bool setIncluded) : wxCommandEvent(EVENT_GRID_CHECK_ROWS), rowFirst_(rowFirst), rowLast_(rowLast), setIncluded_(setIncluded) { assert(rowFirst <= rowLast); } + wxEvent* Clone() const override { return new CheckRowsEvent(*this); } + + const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) + const size_t rowLast_; //range is empty when clearing selection + const bool setIncluded_; +}; + + +struct SyncDirectionEvent : public wxCommandEvent +{ + SyncDirectionEvent(size_t rowFirst, size_t rowLast, SyncDirection direction) : wxCommandEvent(EVENT_GRID_SYNC_DIRECTION), rowFirst_(rowFirst), rowLast_(rowLast), direction_(direction) { assert(rowFirst <= rowLast); } + wxEvent* Clone() const override { return new SyncDirectionEvent(*this); } + + const size_t rowFirst_; //see CheckRowsEvent + const size_t rowLast_; // + const SyncDirection direction_; +}; + +using CheckRowsEventFunction = void (wxEvtHandler::*)(CheckRowsEvent&); +using SyncDirectionEventFunction = void (wxEvtHandler::*)(SyncDirectionEvent&); + +#define CheckRowsEventHandler(func) \ + (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(CheckRowsEventFunction, &func) + +#define SyncDirectionEventHandler(func) \ + (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(SyncDirectionEventFunction, &func) +} + +#endif //CUSTOM_GRID_H_8405817408327894 diff --git a/FreeFileSync/Source/ui/file_grid_attr.h b/FreeFileSync/Source/ui/file_grid_attr.h new file mode 100755 index 00000000..0257f268 --- /dev/null +++ b/FreeFileSync/Source/ui/file_grid_attr.h @@ -0,0 +1,91 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef COLUMN_ATTR_H_189467891346732143213 +#define COLUMN_ATTR_H_189467891346732143213 + +#include +#include + + +namespace zen +{ +enum class ColumnTypeRim +{ + ITEM_PATH, + SIZE, + DATE, + EXTENSION, +}; + +struct ColAttributesRim +{ + ColumnTypeRim type = ColumnTypeRim::ITEM_PATH; + int offset = 0; + int stretch = 0; + bool visible = false; +}; + +inline +std::vector getFileGridDefaultColAttribsLeft() +{ + return //harmonize with main_dlg.cpp::onGridLabelContextRim() => expects stretched ITEM_PATH and non-stretched other columns! + { + { ColumnTypeRim::ITEM_PATH, -100, 1, true }, + { ColumnTypeRim::EXTENSION, 60, 0, false }, + { ColumnTypeRim::DATE, 140, 0, false }, + { ColumnTypeRim::SIZE, 100, 0, true }, + }; +} + +inline +std::vector getFileGridDefaultColAttribsRight() +{ + return getFileGridDefaultColAttribsLeft(); //*currently* same default +} + + +inline +bool getDefaultSortDirection(ColumnTypeRim type) //true: ascending; false: descending +{ + switch (type) + { + case ColumnTypeRim::SIZE: + case ColumnTypeRim::DATE: + return false; + + case ColumnTypeRim::ITEM_PATH: + case ColumnTypeRim::EXTENSION: + return true; + } + assert(false); + return true; +} + + +enum class ItemPathFormat +{ + FULL_PATH, + RELATIVE_PATH, + ITEM_NAME, +}; + +const ItemPathFormat defaultItemPathFormatLeftGrid = ItemPathFormat::RELATIVE_PATH; +const ItemPathFormat defaultItemPathFormatRightGrid = ItemPathFormat::RELATIVE_PATH; + +//------------------------------------------------------------------ + +enum class ColumnTypeCenter +{ + CHECKBOX, + CMP_CATEGORY, + SYNC_ACTION, +}; + +//------------------------------------------------------------------ +} + +#endif //COLUMN_ATTR_H_189467891346732143213 diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp new file mode 100755 index 00000000..f2527520 --- /dev/null +++ b/FreeFileSync/Source/ui/file_view.cpp @@ -0,0 +1,534 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "file_view.h" +#include "sorting.h" +#include "../synchronization.h" +#include +#include + +using namespace zen; + + +template +void addNumbers(const FileSystemObject& fsObj, StatusResult& result) +{ + visitFSObject(fsObj, [&](const FolderPair& folder) + { + if (!folder.isEmpty()) + ++result.foldersOnLeftView; + + if (!folder.isEmpty()) + ++result.foldersOnRightView; + }, + + [&](const FilePair& file) + { + if (!file.isEmpty()) + { + result.filesizeLeftView += file.getFileSize(); + ++result.filesOnLeftView; + } + if (!file.isEmpty()) + { + result.filesizeRightView += file.getFileSize(); + ++result.filesOnRightView; + } + }, + + [&](const SymlinkPair& symlink) + { + if (!symlink.isEmpty()) + ++result.filesOnLeftView; + + if (!symlink.isEmpty()) + ++result.filesOnRightView; + }); +} + + +template +void FileView::updateView(Predicate pred) +{ + viewRef_.clear(); + rowPositions_.clear(); + rowPositionsFirstChild_.clear(); + + for (const RefIndex& ref : sortedRef_) + { + if (const FileSystemObject* fsObj = FileSystemObject::retrieve(ref.objId)) + if (pred(*fsObj)) + { + //save row position for direct random access to FilePair or FolderPair + this->rowPositions_.emplace(ref.objId, viewRef_.size()); //costs: 0.28 s per call - MSVC based on std::set + //"this->" required by two-pass lookup as enforced by GCC 4.7 + + //save row position to identify first child *on sorted subview* of FolderPair or BaseFolderPair in case latter are filtered out + const ContainerObject* parent = &fsObj->parent(); + for (;;) //map all yet unassociated parents to this row + { + const auto rv = this->rowPositionsFirstChild_.emplace(parent, viewRef_.size()); + if (!rv.second) + break; + + if (auto folder = dynamic_cast(parent)) + parent = &(folder->parent()); + else + break; + } + + //build subview + this->viewRef_.push_back(ref.objId); + } + } +} + + +ptrdiff_t FileView::findRowDirect(FileSystemObject::ObjectIdConst objId) const +{ + auto it = rowPositions_.find(objId); + return it != rowPositions_.end() ? it->second : -1; +} + + +ptrdiff_t FileView::findRowFirstChild(const ContainerObject* hierObj) const +{ + auto it = rowPositionsFirstChild_.find(hierObj); + return it != rowPositionsFirstChild_.end() ? it->second : -1; +} + + +FileView::StatusCmpResult FileView::updateCmpResult(bool showExcluded, //maps sortedRef to viewRef + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive) +{ + StatusCmpResult output; + + updateView([&](const FileSystemObject& fsObj) -> bool + { + if (!fsObj.isActive()) + { + output.existsExcluded = true; + if (!showExcluded) + return false; + } + + switch (fsObj.getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + output.existsLeftOnly = true; + if (!leftOnlyFilesActive) return false; + break; + case FILE_RIGHT_SIDE_ONLY: + output.existsRightOnly = true; + if (!rightOnlyFilesActive) return false; + break; + case FILE_LEFT_NEWER: + output.existsLeftNewer = true; + if (!leftNewerFilesActive) return false; + break; + case FILE_RIGHT_NEWER: + output.existsRightNewer = true; + if (!rightNewerFilesActive) return false; + break; + case FILE_DIFFERENT_CONTENT: + output.existsDifferent = true; + if (!differentFilesActive) return false; + break; + case FILE_EQUAL: + case FILE_DIFFERENT_METADATA: //= sub-category of equal + output.existsEqual = true; + if (!equalFilesActive) return false; + break; + case FILE_CONFLICT: + output.existsConflict = true; + if (!conflictFilesActive) return false; + break; + } + //calculate total number of bytes for each side + addNumbers(fsObj, output); + return true; + }); + + return output; +} + + +FileView::StatusSyncPreview FileView::updateSyncPreview(bool showExcluded, //maps sortedRef to viewRef + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive) +{ + StatusSyncPreview output; + + updateView([&](const FileSystemObject& fsObj) -> bool + { + if (!fsObj.isActive()) + { + output.existsExcluded = true; + if (!showExcluded) + return false; + } + + switch (fsObj.getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_CREATE_NEW_LEFT: + output.existsSyncCreateLeft = true; + if (!syncCreateLeftActive) return false; + break; + case SO_CREATE_NEW_RIGHT: + output.existsSyncCreateRight = true; + if (!syncCreateRightActive) return false; + break; + case SO_DELETE_LEFT: + output.existsSyncDeleteLeft = true; + if (!syncDeleteLeftActive) return false; + break; + case SO_DELETE_RIGHT: + output.existsSyncDeleteRight = true; + if (!syncDeleteRightActive) return false; + break; + case SO_OVERWRITE_RIGHT: + case SO_COPY_METADATA_TO_RIGHT: //no extra button on screen + case SO_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + output.existsSyncDirRight = true; + if (!syncDirOverwRightActive) return false; + break; + case SO_OVERWRITE_LEFT: + case SO_COPY_METADATA_TO_LEFT: //no extra button on screen + case SO_MOVE_LEFT_TO: + case SO_MOVE_LEFT_FROM: + output.existsSyncDirLeft = true; + if (!syncDirOverwLeftActive) return false; + break; + case SO_DO_NOTHING: + output.existsSyncDirNone = true; + if (!syncDirNoneActive) return false; + break; + case SO_EQUAL: + output.existsEqual = true; + if (!syncEqualActive) return false; + break; + case SO_UNRESOLVED_CONFLICT: + output.existsConflict = true; + if (!conflictFilesActive) return false; + break; + } + + //calculate total number of bytes for each side + addNumbers(fsObj, output); + return true; + }); + + return output; +} + + +std::vector FileView::getAllFileRef(const std::vector& rows) +{ + const size_t viewSize = viewRef_.size(); + + std::vector output; + + for (size_t pos : rows) + if (pos < viewSize) + if (FileSystemObject* fsObj = FileSystemObject::retrieve(viewRef_[pos])) + output.push_back(fsObj); + + return output; +} + + +void FileView::removeInvalidRows() +{ + viewRef_.clear(); + rowPositions_.clear(); + rowPositionsFirstChild_.clear(); + + //remove rows that have been deleted meanwhile + erase_if(sortedRef_, [&](const RefIndex& refIdx) { return !FileSystemObject::retrieve(refIdx.objId); }); +} + + +class FileView::SerializeHierarchy +{ +public: + static void execute(ContainerObject& hierObj, std::vector& sortedRef, size_t index) { SerializeHierarchy(sortedRef, index).recurse(hierObj); } + +private: + SerializeHierarchy(std::vector& sortedRef, size_t index) : + index_(index), + output_(sortedRef) {} +#if 0 + /* + Spend additional CPU cycles to sort the standard file list? + + Test case: 690.000 item pairs, Windows 7 x64 (C:\ vs I:\) + ---------------------- + CmpNaturalSort: 850 ms + CmpFilePath: 233 ms + CmpAsciiNoCase: 189 ms + No sorting: 30 ms + */ + template + static std::vector getItemsSorted(FixedList& itemList) + { + std::vector output; + for (ItemPair& item : itemList) + output.push_back(&item); + + std::sort(output.begin(), output.end(), [](const ItemPair* lhs, const ItemPair* rhs) { return LessNaturalSort()(lhs->getPairItemName(), rhs->getPairItemName()); }); + return output; + } +#endif + void recurse(ContainerObject& hierObj) + { + for (FilePair& file : hierObj.refSubFiles()) + output_.push_back({ index_, file.getId() }); + + for (SymlinkPair& symlink : hierObj.refSubLinks()) + output_.push_back({ index_, symlink.getId() }); + + for (FolderPair& folder : hierObj.refSubFolders()) + { + output_.push_back({ index_, folder.getId() }); + recurse(folder); //add recursion here to list sub-objects directly below parent! + } + } + + const size_t index_; + std::vector& output_; +}; + + +void FileView::setData(FolderComparison& folderCmp) +{ + //clear everything + std::vector().swap(viewRef_); //free mem + std::vector().swap(sortedRef_); // + currentSort_ = NoValue(); + + folderPairCount_ = std::count_if(begin(folderCmp), end(folderCmp), + [](const BaseFolderPair& baseObj) //count non-empty pairs to distinguish single/multiple folder pair cases + { + return !AFS::isNullPath(baseObj.getAbstractPath< LEFT_SIDE>()) || + !AFS::isNullPath(baseObj.getAbstractPath()); + }); + + for (auto it = begin(folderCmp); it != end(folderCmp); ++it) + SerializeHierarchy::execute(*it, sortedRef_, it - begin(folderCmp)); +} + + +//------------------------------------ SORTING TEMPLATES ------------------------------------------------ +template +struct FileView::LessFullPath +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessFullPath(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessRelativeFolder +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + //presort by folder pair + if (a.folderIndex != b.folderIndex) + return ascending ? + a.folderIndex < b.folderIndex : + a.folderIndex > b.folderIndex; + + return lessRelativeFolder(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessShortFileName +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessShortFileName(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessFilesize +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessFilesize(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessFiletime +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessFiletime(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessExtension +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessExtension(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessCmpResult +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessCmpResult(*fsObjA, *fsObjB); + } +}; + + +template +struct FileView::LessSyncDirection +{ + bool operator()(const RefIndex a, const RefIndex b) const + { + const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); + const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); + if (!fsObjA) //invalid rows shall appear at the end + return false; + else if (!fsObjB) + return true; + + return lessSyncDirection(*fsObjA, *fsObjB); + } +}; + +//------------------------------------------------------------------------------------------------------- + +void FileView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, bool ascending) +{ + viewRef_.clear(); + rowPositions_.clear(); + rowPositionsFirstChild_.clear(); + currentSort_ = SortInfo({ type, onLeft, ascending }); + + switch (type) + { + case ColumnTypeRim::ITEM_PATH: + switch (pathFmt) + { + case ItemPathFormat::FULL_PATH: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); + break; + + case ItemPathFormat::RELATIVE_PATH: + if ( ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder()); + else if (!ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder()); + break; + + case ItemPathFormat::ITEM_NAME: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); + break; + } + break; + + case ColumnTypeRim::SIZE: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); + break; + case ColumnTypeRim::DATE: + if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); + break; + case ColumnTypeRim::EXTENSION: + if ( ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + else if ( ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + else if (!ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + else if (!ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); + break; + } +} diff --git a/FreeFileSync/Source/ui/file_view.h b/FreeFileSync/Source/ui/file_view.h new file mode 100755 index 00000000..ad399401 --- /dev/null +++ b/FreeFileSync/Source/ui/file_view.h @@ -0,0 +1,203 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef GRID_VIEW_H_9285028345703475842569 +#define GRID_VIEW_H_9285028345703475842569 + +#include +#include +#include "file_grid_attr.h" +#include "../file_hierarchy.h" + + +namespace zen +{ +//grid view of FolderComparison +class FileView +{ +public: + FileView() {} + + //direct data access via row number + const FileSystemObject* getObject(size_t row) const; //returns nullptr if object is not found; complexity: constant! + /**/ + FileSystemObject* getObject(size_t row); // + size_t rowsOnView() const { return viewRef_ .size(); } //only visible elements + size_t rowsTotal () const { return sortedRef_.size(); } //total rows available + + //get references to FileSystemObject: no nullptr-check needed! Everything's bound. + std::vector getAllFileRef(const std::vector& rows); + + struct StatusCmpResult + { + bool existsExcluded = false; + bool existsEqual = false; + bool existsConflict = false; + + bool existsLeftOnly = false; + bool existsRightOnly = false; + bool existsLeftNewer = false; + bool existsRightNewer = false; + bool existsDifferent = false; + + unsigned int filesOnLeftView = 0; + unsigned int foldersOnLeftView = 0; + unsigned int filesOnRightView = 0; + unsigned int foldersOnRightView = 0; + + uint64_t filesizeLeftView = 0; + uint64_t filesizeRightView = 0; + }; + + //comparison results view + StatusCmpResult updateCmpResult(bool showExcluded, + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive); + + struct StatusSyncPreview + { + bool existsExcluded = false; + bool existsEqual = false; + bool existsConflict = false; + + bool existsSyncCreateLeft = false; + bool existsSyncCreateRight = false; + bool existsSyncDeleteLeft = false; + bool existsSyncDeleteRight = false; + bool existsSyncDirLeft = false; + bool existsSyncDirRight = false; + bool existsSyncDirNone = false; + + unsigned int filesOnLeftView = 0; + unsigned int foldersOnLeftView = 0; + unsigned int filesOnRightView = 0; + unsigned int foldersOnRightView = 0; + + uint64_t filesizeLeftView = 0; + uint64_t filesizeRightView = 0; + }; + + //synchronization preview + StatusSyncPreview updateSyncPreview(bool showExcluded, + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive); + + void setData(FolderComparison& newData); + void removeInvalidRows(); //remove references to rows that have been deleted meanwhile: call after manual deletion and synchronization! + + //sorting... + void sortView(zen::ColumnTypeRim type, zen::ItemPathFormat pathFmt, bool onLeft, bool ascending); //always call this method for sorting, never sort externally! + + struct SortInfo + { + zen::ColumnTypeRim type = zen::ColumnTypeRim::ITEM_PATH; + bool onLeft = false; + bool ascending = false; + }; + const SortInfo* getSortInfo() const { return currentSort_.get(); } //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 + + size_t getFolderPairCount() const { return folderPairCount_; } //count non-empty pairs to distinguish single/multiple folder pair cases + +private: + FileView (const FileView&) = delete; + FileView& operator=(const FileView&) = delete; + + struct RefIndex + { + size_t folderIndex = 0; //because of alignment there's no benefit in using "unsigned int" in 64-bit code here! + FileSystemObject::ObjectId objId = nullptr; + }; + + template void updateView(Predicate pred); + + + std::unordered_map rowPositions_; //find row positions on sortedRef directly + std::unordered_map rowPositionsFirstChild_; //find first child on sortedRef of a hierarchy object + //void* instead of ContainerObject*: these are weak pointers and should *never be dereferenced*! + + std::vector viewRef_; //partial view on sortedRef + /* /|\ + | (update...) + | */ + std::vector sortedRef_; //flat view of weak pointers on folderCmp; may be sorted + /* /|\ + | (setData...) + | */ + //std::shared_ptr folderCmp; //actual comparison data: owned by FileView! + size_t folderPairCount_ = 0; //number of non-empty folder pairs + + + class SerializeHierarchy; + + //sorting classes + template + struct LessFullPath; + + template + struct LessRelativeFolder; + + template + struct LessShortFileName; + + template + struct LessFilesize; + + template + struct LessFiletime; + + template + struct LessExtension; + + template + struct LessCmpResult; + + template + struct LessSyncDirection; + + Opt currentSort_; +}; + + + + + + + +//##################### implementation ######################################### + +inline +const FileSystemObject* FileView::getObject(size_t row) const +{ + return row < viewRef_.size() ? + FileSystemObject::retrieve(viewRef_[row]) : nullptr; +} + +inline +FileSystemObject* FileView::getObject(size_t row) +{ + //code re-use of const method: see Meyers Effective C++ + return const_cast(static_cast(*this).getObject(row)); +} +} + + +#endif //GRID_VIEW_H_9285028345703475842569 diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp index 40033094..73d4d461 100755 --- a/FreeFileSync/Source/ui/folder_selector.cpp +++ b/FreeFileSync/Source/ui/folder_selector.cpp @@ -69,7 +69,7 @@ FolderSelector::FolderSelector(wxWindow& dropWindow, auto setupDragDrop = [&](wxWindow& dropWin) { setupFileDrop(dropWin); - dropWin.Connect(EVENT_DROP_FILE, FileDropEventHandler(FolderSelector::onKeyFileDropped), nullptr, this); + dropWin.Connect(EVENT_DROP_FILE, FileDropEventHandler(FolderSelector::onItemPathDropped), nullptr, this); }; setupDragDrop(dropWindow_); @@ -88,10 +88,10 @@ FolderSelector::FolderSelector(wxWindow& dropWindow, FolderSelector::~FolderSelector() { - dropWindow_.Disconnect(EVENT_DROP_FILE, FileDropEventHandler(FolderSelector::onKeyFileDropped), nullptr, this); + dropWindow_.Disconnect(EVENT_DROP_FILE, FileDropEventHandler(FolderSelector::onItemPathDropped), nullptr, this); if (dropWindow2_) - dropWindow2_->Disconnect(EVENT_DROP_FILE, FileDropEventHandler(FolderSelector::onKeyFileDropped), nullptr, this); + dropWindow2_->Disconnect(EVENT_DROP_FILE, FileDropEventHandler(FolderSelector::onItemPathDropped), nullptr, this); folderComboBox_ .Disconnect(wxEVT_MOUSEWHEEL, wxMouseEventHandler (FolderSelector::onMouseWheel ), nullptr, this); folderComboBox_ .Disconnect(wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler(FolderSelector::onEditFolderPath ), nullptr, this); @@ -119,7 +119,7 @@ void FolderSelector::onMouseWheel(wxMouseEvent& event) } -void FolderSelector::onKeyFileDropped(FileDropEvent& event) +void FolderSelector::onItemPathDropped(FileDropEvent& event) { const auto& itemPaths = event.getPaths(); if (itemPaths.empty()) diff --git a/FreeFileSync/Source/ui/folder_selector.h b/FreeFileSync/Source/ui/folder_selector.h index df3293e3..a0271f6f 100755 --- a/FreeFileSync/Source/ui/folder_selector.h +++ b/FreeFileSync/Source/ui/folder_selector.h @@ -50,7 +50,7 @@ private: virtual bool shouldSetDroppedPaths(const std::vector& shellItemPaths) { return true; } //return true if drop should be processed void onMouseWheel (wxMouseEvent& event); - void onKeyFileDropped (FileDropEvent& event); + void onItemPathDropped(FileDropEvent& event); void onEditFolderPath (wxCommandEvent& event); void onSelectFolder (wxCommandEvent& event); void onSelectAltFolder(wxCommandEvent& event); diff --git a/FreeFileSync/Source/ui/grid_view.cpp b/FreeFileSync/Source/ui/grid_view.cpp deleted file mode 100755 index 0de75f14..00000000 --- a/FreeFileSync/Source/ui/grid_view.cpp +++ /dev/null @@ -1,549 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#include "grid_view.h" -#include "sorting.h" -#include "../synchronization.h" -#include -#include - -using namespace zen; - - -template -void addNumbers(const FileSystemObject& fsObj, StatusResult& result) -{ - visitFSObject(fsObj, [&](const FolderPair& folder) - { - if (!folder.isEmpty()) - ++result.foldersOnLeftView; - - if (!folder.isEmpty()) - ++result.foldersOnRightView; - }, - - [&](const FilePair& file) - { - if (!file.isEmpty()) - { - result.filesizeLeftView += file.getFileSize(); - ++result.filesOnLeftView; - } - if (!file.isEmpty()) - { - result.filesizeRightView += file.getFileSize(); - ++result.filesOnRightView; - } - }, - - [&](const SymlinkPair& symlink) - { - if (!symlink.isEmpty()) - ++result.filesOnLeftView; - - if (!symlink.isEmpty()) - ++result.filesOnRightView; - }); -} - - -template -void GridView::updateView(Predicate pred) -{ - viewRef_.clear(); - rowPositions_.clear(); - rowPositionsFirstChild_.clear(); - - for (const RefIndex& ref : sortedRef_) - { - if (const FileSystemObject* fsObj = FileSystemObject::retrieve(ref.objId)) - if (pred(*fsObj)) - { - //save row position for direct random access to FilePair or FolderPair - this->rowPositions_.emplace(ref.objId, viewRef_.size()); //costs: 0.28 s per call - MSVC based on std::set - //"this->" required by two-pass lookup as enforced by GCC 4.7 - - //save row position to identify first child *on sorted subview* of FolderPair or BaseFolderPair in case latter are filtered out - const ContainerObject* parent = &fsObj->parent(); - for (;;) //map all yet unassociated parents to this row - { - const auto rv = this->rowPositionsFirstChild_.emplace(parent, viewRef_.size()); - if (!rv.second) - break; - - if (auto folder = dynamic_cast(parent)) - parent = &(folder->parent()); - else - break; - } - - //build subview - this->viewRef_.push_back(ref.objId); - } - } -} - - -ptrdiff_t GridView::findRowDirect(FileSystemObject::ObjectIdConst objId) const -{ - auto it = rowPositions_.find(objId); - return it != rowPositions_.end() ? it->second : -1; -} - -ptrdiff_t GridView::findRowFirstChild(const ContainerObject* hierObj) const -{ - auto it = rowPositionsFirstChild_.find(hierObj); - return it != rowPositionsFirstChild_.end() ? it->second : -1; -} - - -GridView::StatusCmpResult GridView::updateCmpResult(bool showExcluded, //maps sortedRef to viewRef - bool leftOnlyFilesActive, - bool rightOnlyFilesActive, - bool leftNewerFilesActive, - bool rightNewerFilesActive, - bool differentFilesActive, - bool equalFilesActive, - bool conflictFilesActive) -{ - StatusCmpResult output; - - updateView([&](const FileSystemObject& fsObj) -> bool - { - if (!fsObj.isActive()) - { - output.existsExcluded = true; - if (!showExcluded) - return false; - } - - switch (fsObj.getCategory()) - { - case FILE_LEFT_SIDE_ONLY: - output.existsLeftOnly = true; - if (!leftOnlyFilesActive) return false; - break; - case FILE_RIGHT_SIDE_ONLY: - output.existsRightOnly = true; - if (!rightOnlyFilesActive) return false; - break; - case FILE_LEFT_NEWER: - output.existsLeftNewer = true; - if (!leftNewerFilesActive) return false; - break; - case FILE_RIGHT_NEWER: - output.existsRightNewer = true; - if (!rightNewerFilesActive) return false; - break; - case FILE_DIFFERENT_CONTENT: - output.existsDifferent = true; - if (!differentFilesActive) return false; - break; - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal - output.existsEqual = true; - if (!equalFilesActive) return false; - break; - case FILE_CONFLICT: - output.existsConflict = true; - if (!conflictFilesActive) return false; - break; - } - //calculate total number of bytes for each side - addNumbers(fsObj, output); - return true; - }); - - return output; -} - - -GridView::StatusSyncPreview GridView::updateSyncPreview(bool showExcluded, //maps sortedRef to viewRef - bool syncCreateLeftActive, - bool syncCreateRightActive, - bool syncDeleteLeftActive, - bool syncDeleteRightActive, - bool syncDirOverwLeftActive, - bool syncDirOverwRightActive, - bool syncDirNoneActive, - bool syncEqualActive, - bool conflictFilesActive) -{ - StatusSyncPreview output; - - updateView([&](const FileSystemObject& fsObj) -> bool - { - if (!fsObj.isActive()) - { - output.existsExcluded = true; - if (!showExcluded) - return false; - } - - switch (fsObj.getSyncOperation()) //evaluate comparison result and sync direction - { - case SO_CREATE_NEW_LEFT: - output.existsSyncCreateLeft = true; - if (!syncCreateLeftActive) return false; - break; - case SO_CREATE_NEW_RIGHT: - output.existsSyncCreateRight = true; - if (!syncCreateRightActive) return false; - break; - case SO_DELETE_LEFT: - output.existsSyncDeleteLeft = true; - if (!syncDeleteLeftActive) return false; - break; - case SO_DELETE_RIGHT: - output.existsSyncDeleteRight = true; - if (!syncDeleteRightActive) return false; - break; - case SO_OVERWRITE_RIGHT: - case SO_COPY_METADATA_TO_RIGHT: //no extra button on screen - case SO_MOVE_RIGHT_FROM: - case SO_MOVE_RIGHT_TO: - output.existsSyncDirRight = true; - if (!syncDirOverwRightActive) return false; - break; - case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: //no extra button on screen - case SO_MOVE_LEFT_TO: - case SO_MOVE_LEFT_FROM: - output.existsSyncDirLeft = true; - if (!syncDirOverwLeftActive) return false; - break; - case SO_DO_NOTHING: - output.existsSyncDirNone = true; - if (!syncDirNoneActive) return false; - break; - case SO_EQUAL: - output.existsEqual = true; - if (!syncEqualActive) return false; - break; - case SO_UNRESOLVED_CONFLICT: - output.existsConflict = true; - if (!conflictFilesActive) return false; - break; - } - - //calculate total number of bytes for each side - addNumbers(fsObj, output); - return true; - }); - - return output; -} - - -std::vector GridView::getAllFileRef(const std::vector& rows) -{ - const size_t viewSize = viewRef_.size(); - - std::vector output; - - for (size_t pos : rows) - if (pos < viewSize) - if (FileSystemObject* fsObj = FileSystemObject::retrieve(viewRef_[pos])) - output.push_back(fsObj); - - return output; -} - - -void GridView::removeInvalidRows() -{ - viewRef_.clear(); - rowPositions_.clear(); - rowPositionsFirstChild_.clear(); - - //remove rows that have been deleted meanwhile - erase_if(sortedRef_, [&](const RefIndex& refIdx) { return !FileSystemObject::retrieve(refIdx.objId); }); -} - - -class GridView::SerializeHierarchy -{ -public: - static void execute(ContainerObject& hierObj, std::vector& sortedRef, size_t index) { SerializeHierarchy(sortedRef, index).recurse(hierObj); } - -private: - SerializeHierarchy(std::vector& sortedRef, size_t index) : - index_(index), - output_(sortedRef) {} - /* - Spend additional CPU cycles to sort the standard file list? - - Test case: 690.000 item pairs, Windows 7 x64 (C:\ vs I:\) - ---------------------- - CmpNaturalSort: 850 ms - CmpFilePath: 233 ms - CmpAsciiNoCase: 189 ms - No sorting: 30 ms - */ -#if 0 - template - static std::vector getItemsSorted(FixedList& itemList) - { - std::vector output; - for (ItemPair& item : itemList) - output.push_back(&item); - - std::sort(output.begin(), output.end(), [](const ItemPair* lhs, const ItemPair* rhs) { return LessNaturalSort()(lhs->getPairItemName(), rhs->getPairItemName()); }); - return output; - } -#endif - void recurse(ContainerObject& hierObj) - { - for (FilePair& file : hierObj.refSubFiles()) - output_.emplace_back(index_, file.getId()); - - for (SymlinkPair& symlink : hierObj.refSubLinks()) - output_.emplace_back(index_, symlink.getId()); - - for (FolderPair& folder : hierObj.refSubFolders()) - { - output_.emplace_back(index_, folder.getId()); - recurse(folder); //add recursion here to list sub-objects directly below parent! - } - } - - const size_t index_; - std::vector& output_; -}; - - -void GridView::setData(FolderComparison& folderCmp) -{ - //clear everything - std::vector().swap(viewRef_); //free mem - std::vector().swap(sortedRef_); // - currentSort_ = NoValue(); - - folderPairCount_ = std::count_if(begin(folderCmp), end(folderCmp), - [](const BaseFolderPair& baseObj) //count non-empty pairs to distinguish single/multiple folder pair cases - { - return !AFS::isNullPath(baseObj.getAbstractPath< LEFT_SIDE>()) || - !AFS::isNullPath(baseObj.getAbstractPath()); - }); - - for (auto it = begin(folderCmp); it != end(folderCmp); ++it) - SerializeHierarchy::execute(*it, sortedRef_, it - begin(folderCmp)); -} - - -//------------------------------------ SORTING TEMPLATES ------------------------------------------------ -template -struct GridView::LessFullPath -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessFullPath(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessRelativeFolder -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - //presort by folder pair - if (a.folderIndex != b.folderIndex) - return ascending ? - a.folderIndex < b.folderIndex : - a.folderIndex > b.folderIndex; - - return lessRelativeFolder(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessShortFileName -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessShortFileName(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessFilesize -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessFilesize(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessFiletime -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessFiletime(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessExtension -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessExtension(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessCmpResult -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessCmpResult(*fsObjA, *fsObjB); - } -}; - - -template -struct GridView::LessSyncDirection -{ - bool operator()(const RefIndex a, const RefIndex b) const - { - const FileSystemObject* fsObjA = FileSystemObject::retrieve(a.objId); - const FileSystemObject* fsObjB = FileSystemObject::retrieve(b.objId); - if (!fsObjA) //invalid rows shall appear at the end - return false; - else if (!fsObjB) - return true; - - return lessSyncDirection(*fsObjA, *fsObjB); - } -}; - -//------------------------------------------------------------------------------------------------------- -bool GridView::getDefaultSortDirection(ColumnTypeRim type) //true: ascending; false: descending -{ - switch (type) - { - case ColumnTypeRim::SIZE: - case ColumnTypeRim::DATE: - return false; - - case ColumnTypeRim::ITEM_PATH: - case ColumnTypeRim::EXTENSION: - return true; - } - assert(false); - return true; -} - - -void GridView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, bool ascending) -{ - viewRef_.clear(); - rowPositions_.clear(); - rowPositionsFirstChild_.clear(); - currentSort_ = SortInfo(type, onLeft, ascending); - - switch (type) - { - case ColumnTypeRim::ITEM_PATH: - switch (pathFmt) - { - case ItemPathFormat::FULL_PATH: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFullPath()); - break; - - case ItemPathFormat::RELATIVE_PATH: - if ( ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder()); - else if (!ascending) std::sort(sortedRef_.begin(), sortedRef_.end(), LessRelativeFolder()); - break; - - case ItemPathFormat::ITEM_NAME: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessShortFileName()); - break; - } - break; - - case ColumnTypeRim::SIZE: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); - else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); - else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFilesize()); - break; - case ColumnTypeRim::DATE: - if ( ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); - else if ( ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); - else if (!ascending && onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); - else if (!ascending && !onLeft) std::sort(sortedRef_.begin(), sortedRef_.end(), LessFiletime()); - break; - case ColumnTypeRim::EXTENSION: - if ( ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); - else if ( ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); - else if (!ascending && onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); - else if (!ascending && !onLeft) std::stable_sort(sortedRef_.begin(), sortedRef_.end(), LessExtension()); - break; - } -} diff --git a/FreeFileSync/Source/ui/grid_view.h b/FreeFileSync/Source/ui/grid_view.h deleted file mode 100755 index 70838122..00000000 --- a/FreeFileSync/Source/ui/grid_view.h +++ /dev/null @@ -1,209 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#ifndef GRID_VIEW_H_9285028345703475842569 -#define GRID_VIEW_H_9285028345703475842569 - -#include -#include -#include "column_attr.h" -#include "../file_hierarchy.h" - - -namespace zen -{ -//grid view of FolderComparison -class GridView -{ -public: - GridView() {} - - //direct data access via row number - const FileSystemObject* getObject(size_t row) const; //returns nullptr if object is not found; complexity: constant! - /**/ - FileSystemObject* getObject(size_t row); // - size_t rowsOnView() const { return viewRef_ .size(); } //only visible elements - size_t rowsTotal () const { return sortedRef_.size(); } //total rows available - - //get references to FileSystemObject: no nullptr-check needed! Everything's bound. - std::vector getAllFileRef(const std::vector& rows); - - struct StatusCmpResult - { - bool existsExcluded = false; - bool existsEqual = false; - bool existsConflict = false; - - bool existsLeftOnly = false; - bool existsRightOnly = false; - bool existsLeftNewer = false; - bool existsRightNewer = false; - bool existsDifferent = false; - - unsigned int filesOnLeftView = 0; - unsigned int foldersOnLeftView = 0; - unsigned int filesOnRightView = 0; - unsigned int foldersOnRightView = 0; - - uint64_t filesizeLeftView = 0; - uint64_t filesizeRightView = 0; - }; - - //comparison results view - StatusCmpResult updateCmpResult(bool showExcluded, - bool leftOnlyFilesActive, - bool rightOnlyFilesActive, - bool leftNewerFilesActive, - bool rightNewerFilesActive, - bool differentFilesActive, - bool equalFilesActive, - bool conflictFilesActive); - - struct StatusSyncPreview - { - bool existsExcluded = false; - bool existsEqual = false; - bool existsConflict = false; - - bool existsSyncCreateLeft = false; - bool existsSyncCreateRight = false; - bool existsSyncDeleteLeft = false; - bool existsSyncDeleteRight = false; - bool existsSyncDirLeft = false; - bool existsSyncDirRight = false; - bool existsSyncDirNone = false; - - unsigned int filesOnLeftView = 0; - unsigned int foldersOnLeftView = 0; - unsigned int filesOnRightView = 0; - unsigned int foldersOnRightView = 0; - - uint64_t filesizeLeftView = 0; - uint64_t filesizeRightView = 0; - }; - - //synchronization preview - StatusSyncPreview updateSyncPreview(bool showExcluded, - bool syncCreateLeftActive, - bool syncCreateRightActive, - bool syncDeleteLeftActive, - bool syncDeleteRightActive, - bool syncDirOverwLeftActive, - bool syncDirOverwRightActive, - bool syncDirNoneActive, - bool syncEqualActive, - bool conflictFilesActive); - - void setData(FolderComparison& newData); - void removeInvalidRows(); //remove references to rows that have been deleted meanwhile: call after manual deletion and synchronization! - - //sorting... - bool static getDefaultSortDirection(zen::ColumnTypeRim type); //true: ascending; false: descending - - void sortView(zen::ColumnTypeRim type, zen::ItemPathFormat pathFmt, bool onLeft, bool ascending); //always call this method for sorting, never sort externally! - - struct SortInfo - { - SortInfo(zen::ColumnTypeRim type, bool onLeft, bool ascending) : type_(type), onLeft_(onLeft), ascending_(ascending) {} - zen::ColumnTypeRim type_; - bool onLeft_; - bool ascending_; - }; - const SortInfo* getSortInfo() const { return currentSort_.get(); } //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 - - size_t getFolderPairCount() const { return folderPairCount_; } //count non-empty pairs to distinguish single/multiple folder pair cases - -private: - GridView (const GridView&) = delete; - GridView& operator=(const GridView&) = delete; - - struct RefIndex - { - RefIndex(size_t folderInd, FileSystemObject::ObjectId id) : - folderIndex(folderInd), - objId(id) {} - size_t folderIndex; //because of alignment there's no benefit in using "unsigned int" in 64-bit code here! - FileSystemObject::ObjectId objId; - }; - - template void updateView(Predicate pred); - - - std::unordered_map rowPositions_; //find row positions on sortedRef directly - std::unordered_map rowPositionsFirstChild_; //find first child on sortedRef of a hierarchy object - //void* instead of ContainerObject*: these are weak pointers and should *never be dereferenced*! - - std::vector viewRef_; //partial view on sortedRef - /* /|\ - | (update...) - | */ - std::vector sortedRef_; //flat view of weak pointers on folderCmp; may be sorted - /* /|\ - | (setData...) - | */ - //std::shared_ptr folderCmp; //actual comparison data: owned by GridView! - size_t folderPairCount_ = 0; //number of non-empty folder pairs - - - class SerializeHierarchy; - - //sorting classes - template - struct LessFullPath; - - template - struct LessRelativeFolder; - - template - struct LessShortFileName; - - template - struct LessFilesize; - - template - struct LessFiletime; - - template - struct LessExtension; - - template - struct LessCmpResult; - - template - struct LessSyncDirection; - - Opt currentSort_; -}; - - - - - - - -//##################### implementation ######################################### - -inline -const FileSystemObject* GridView::getObject(size_t row) const -{ - return row < viewRef_.size() ? - FileSystemObject::retrieve(viewRef_[row]) : nullptr; -} - -inline -FileSystemObject* GridView::getObject(size_t row) -{ - //code re-use of const method: see Meyers Effective C++ - return const_cast(static_cast(*this).getObject(row)); -} -} - - -#endif //GRID_VIEW_H_9285028345703475842569 diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index 6fa0d35b..cbe00bd7 100755 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -610,8 +610,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_panelConfig = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); bSizerConfig = new wxBoxSizer( wxHORIZONTAL ); - wxBoxSizer* bSizer151; - bSizer151 = new wxBoxSizer( wxHORIZONTAL ); + bSizerCfgHistoryButtons = new wxBoxSizer( wxHORIZONTAL ); wxBoxSizer* bSizer17611; bSizer17611 = new wxBoxSizer( wxVERTICAL ); @@ -626,7 +625,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer17611->Add( m_staticText951, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); - bSizer151->Add( bSizer17611, 0, 0, 5 ); + bSizerCfgHistoryButtons->Add( bSizer17611, 0, 0, 5 ); wxBoxSizer* bSizer1761; bSizer1761 = new wxBoxSizer( wxVERTICAL ); @@ -641,7 +640,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer1761->Add( m_staticText95, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); - bSizer151->Add( bSizer1761, 0, 0, 5 ); + bSizerCfgHistoryButtons->Add( bSizer1761, 0, 0, 5 ); wxBoxSizer* bSizer175; bSizer175 = new wxBoxSizer( wxVERTICAL ); @@ -656,7 +655,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer175->Add( m_staticText961, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); - bSizer151->Add( bSizer175, 0, 0, 5 ); + bSizerCfgHistoryButtons->Add( bSizer175, 0, 0, 5 ); wxBoxSizer* bSizer174; bSizer174 = new wxBoxSizer( wxVERTICAL ); @@ -682,15 +681,14 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer174->Add( m_staticText97, 0, wxALIGN_CENTER_HORIZONTAL|wxRIGHT|wxLEFT, 2 ); - bSizer151->Add( bSizer174, 0, 0, 5 ); + bSizerCfgHistoryButtons->Add( bSizer174, 0, 0, 5 ); - bSizerConfig->Add( bSizer151, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); + bSizerConfig->Add( bSizerCfgHistoryButtons, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL, 5 ); - m_listBoxHistory = new wxListBox( m_panelConfig, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, NULL, wxLB_EXTENDED|wxLB_NEEDED_SB ); - m_listBoxHistory->SetMinSize( wxSize( -1, 40 ) ); - - bSizerConfig->Add( m_listBoxHistory, 1, wxEXPAND, 5 ); + m_gridCfgHistory = new zen::Grid( m_panelConfig, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_gridCfgHistory->SetScrollRate( 5, 5 ); + bSizerConfig->Add( m_gridCfgHistory, 1, wxEXPAND, 5 ); m_panelConfig->SetSizer( bSizerConfig ); @@ -1019,10 +1017,6 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_bpButtonSave->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnConfigSave ), NULL, this ); m_bpButtonSaveAs->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnConfigSaveAs ), NULL, this ); m_bpButtonSaveAsBatch->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnSaveAsBatchJob ), NULL, this ); - m_listBoxHistory->Connect( wxEVT_KEY_DOWN, wxKeyEventHandler( MainDialogGenerated::OnCfgHistoryKeyEvent ), NULL, this ); - m_listBoxHistory->Connect( wxEVT_COMMAND_LISTBOX_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnLoadFromHistory ), NULL, this ); - m_listBoxHistory->Connect( wxEVT_COMMAND_LISTBOX_DOUBLECLICKED, wxCommandEventHandler( MainDialogGenerated::OnLoadFromHistoryDoubleClick ), NULL, this ); - m_listBoxHistory->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnCfgHistoryRightClick ), NULL, this ); m_bpButtonViewTypeSyncAction->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewType ), NULL, this ); m_bpButtonShowExcluded->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); m_bpButtonShowExcluded->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); @@ -1655,8 +1649,23 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer233->Add( bSizerKeepVerticalHeight, 0, 0, 5 ); + bSizerDatabase = new wxWrapSizer( wxVERTICAL ); + m_bitmapDatabase = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizer233->Add( m_bitmapDatabase, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxLEFT, 10 ); + bSizerDatabase->Add( m_bitmapDatabase, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizerDatabase->Add( 0, 3, 0, 0, 5 ); + + m_staticText145 = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("sync.ffs_db"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText145->Wrap( -1 ); + m_staticText145->SetFont( wxFont( 9, wxFONTFAMILY_SWISS, wxFONTSTYLE_ITALIC, wxFONTWEIGHT_NORMAL, false, wxT("Arial") ) ); + m_staticText145->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) ); + + bSizerDatabase->Add( m_staticText145, 0, wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer233->Add( bSizerDatabase, 0, wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); m_staticTextSyncVarDescription = new wxStaticText( m_panelSyncSettings, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); m_staticTextSyncVarDescription->Wrap( -1 ); @@ -1831,7 +1840,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w bSizer242 = new wxBoxSizer( wxHORIZONTAL ); m_bitmapIgnoreErrors = new wxStaticBitmap( m_panelSyncSettings, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer242->Add( m_bitmapIgnoreErrors, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + bSizer242->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); m_checkBoxIgnoreErrors = new wxCheckBox( m_panelSyncSettings, wxID_ANY, _("&Ignore errors"), wxDefaultPosition, wxDefaultSize, 0 ); m_checkBoxIgnoreErrors->SetToolTip( _("Show pop-up on errors or warnings") ); @@ -2108,7 +2117,7 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_staticline371 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxLI_HORIZONTAL ); bSizer134->Add( m_staticline371, 0, wxEXPAND, 5 ); - m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panel41->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); wxBoxSizer* bSizer185; @@ -2312,7 +2321,7 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_staticline57 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizerSftpTweaks->Add( m_staticline57, 0, wxEXPAND, 5 ); - m_panel411 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panel411 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panel411->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); wxBoxSizer* bSizer1851; @@ -2386,7 +2395,7 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_staticline573 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizerFtpTweaks->Add( m_staticline573, 0, wxEXPAND, 5 ); - m_panel4111 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panel4111 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panel4111->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); wxBoxSizer* bSizer18511; @@ -2492,7 +2501,7 @@ AbstractFolderPickerGenerated::AbstractFolderPickerGenerated( wxWindow* parent, m_staticline371 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxSize( -1, -1 ), wxLI_HORIZONTAL ); bSizer134->Add( m_staticline371, 0, wxEXPAND, 5 ); - m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 ); + m_panel41 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panel41->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); wxBoxSizer* bSizer185; @@ -2861,7 +2870,7 @@ CompareProgressDlgGenerated::CompareProgressDlgGenerated( wxWindow* parent, wxWi bSizerProgressFooter = new wxBoxSizer( wxHORIZONTAL ); m_bitmapIgnoreErrors = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizerProgressFooter->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + bSizerProgressFooter->Add( m_bitmapIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL, 5 ); m_checkBoxIgnoreErrors = new wxCheckBox( this, wxID_ANY, _("&Ignore errors"), wxDefaultPosition, wxDefaultSize, 0 ); bSizerProgressFooter->Add( m_checkBoxIgnoreErrors, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 ); @@ -3310,7 +3319,7 @@ BatchDlgGenerated::BatchDlgGenerated( wxWindow* parent, wxWindowID id, const wxS bSizer236 = new wxBoxSizer( wxHORIZONTAL ); m_bitmapMinimizeToTray = new wxStaticBitmap( m_panel35, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer236->Add( m_bitmapMinimizeToTray, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + bSizer236->Add( m_bitmapMinimizeToTray, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); m_checkBoxRunMinimized = new wxCheckBox( m_panel35, wxID_ANY, _("Run minimized"), wxDefaultPosition, wxDefaultSize, 0 ); bSizer236->Add( m_checkBoxRunMinimized, 1, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); @@ -4631,7 +4640,7 @@ ActivationDlgGenerated::ActivationDlgGenerated( wxWindow* parent, wxWindowID id, bSizer237->Add( bSizer236, 0, wxEXPAND, 5 ); - m_textCtrlManualActivationUrl = new wxTextCtrl( m_panel351, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( 220, -1 ), wxTE_MULTILINE|wxTE_READONLY|wxWANTS_CHARS ); + m_textCtrlManualActivationUrl = new wxTextCtrl( m_panel351, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( 220, 55 ), wxTE_MULTILINE|wxTE_READONLY|wxWANTS_CHARS ); bSizer237->Add( m_textCtrlManualActivationUrl, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); wxBoxSizer* bSizer235; @@ -4691,3 +4700,70 @@ ActivationDlgGenerated::ActivationDlgGenerated( wxWindow* parent, wxWindowID id, ActivationDlgGenerated::~ActivationDlgGenerated() { } + +CfgHighlightDlgGenerated::CfgHighlightDlgGenerated( 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->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer96; + bSizer96 = new wxBoxSizer( wxVERTICAL ); + + m_panel35 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel35->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer98; + bSizer98 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bSizer238; + bSizer238 = new wxBoxSizer( wxVERTICAL ); + + m_staticText145 = new wxStaticText( m_panel35, wxID_ANY, _("Highlight configurations that have not been run for more than the following number of days:"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticText145->Wrap( 300 ); + bSizer238->Add( m_staticText145, 0, wxTOP|wxRIGHT|wxLEFT, 5 ); + + m_spinCtrlSyncOverdueDays = new wxSpinCtrl( m_panel35, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( 70, -1 ), wxSP_ARROW_KEYS, 0, 2000000000, 0 ); + bSizer238->Add( m_spinCtrlSyncOverdueDays, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + + bSizer98->Add( bSizer238, 1, wxALL|wxEXPAND, 5 ); + + + m_panel35->SetSizer( bSizer98 ); + m_panel35->Layout(); + bSizer98->Fit( m_panel35 ); + bSizer96->Add( m_panel35, 0, 0, 5 ); + + m_staticline21 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer96->Add( m_staticline21, 0, wxEXPAND, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonOkay = new wxButton( this, wxID_OK, _("OK"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonOkay->SetDefault(); + m_buttonOkay->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); + + bSizerStdButtons->Add( m_buttonOkay, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer96->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + this->SetSizer( bSizer96 ); + this->Layout(); + bSizer96->Fit( this ); + + this->Centre( wxBOTH ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( CfgHighlightDlgGenerated::OnClose ) ); + m_buttonOkay->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CfgHighlightDlgGenerated::OnOkay ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( CfgHighlightDlgGenerated::OnCancel ), NULL, this ); +} + +CfgHighlightDlgGenerated::~CfgHighlightDlgGenerated() +{ +} diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h index c0b02b82..2272410e 100755 --- a/FreeFileSync/Source/ui/gui_generated.h +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -39,13 +39,14 @@ namespace zen { class TripleSplitter; } #include #include #include -#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -151,6 +152,7 @@ protected: wxCheckBox* m_checkBoxMatchCase; wxPanel* m_panelConfig; wxBoxSizer* bSizerConfig; + wxBoxSizer* bSizerCfgHistoryButtons; wxBitmapButton* m_bpButtonNew; wxStaticText* m_staticText951; wxBitmapButton* m_bpButtonOpen; @@ -160,7 +162,7 @@ protected: wxBitmapButton* m_bpButtonSaveAs; wxBitmapButton* m_bpButtonSaveAsBatch; wxStaticText* m_staticText97; - wxListBox* m_listBoxHistory; + zen::Grid* m_gridCfgHistory; wxPanel* m_panelViewFilter; wxBoxSizer* bSizerViewFilter; wxStaticText* m_staticTextViewType; @@ -234,10 +236,6 @@ protected: virtual void OnTopLocalSyncCfg( wxCommandEvent& event ) { event.Skip(); } virtual void OnHideSearchPanel( wxCommandEvent& event ) { event.Skip(); } virtual void OnSearchGridEnter( wxCommandEvent& event ) { event.Skip(); } - virtual void OnCfgHistoryKeyEvent( wxKeyEvent& event ) { event.Skip(); } - virtual void OnLoadFromHistory( wxCommandEvent& event ) { event.Skip(); } - virtual void OnLoadFromHistoryDoubleClick( wxCommandEvent& event ) { event.Skip(); } - virtual void OnCfgHistoryRightClick( wxMouseEvent& event ) { event.Skip(); } virtual void OnToggleViewType( wxCommandEvent& event ) { event.Skip(); } virtual void OnToggleViewButton( wxCommandEvent& event ) { event.Skip(); } virtual void OnViewButtonRightClick( wxMouseEvent& event ) { event.Skip(); } @@ -363,7 +361,9 @@ protected: wxStaticText* m_staticText120; wxStaticText* m_staticText140; wxStaticText* m_staticText1401; + wxWrapSizer* bSizerDatabase; wxStaticBitmap* m_bitmapDatabase; + wxStaticText* m_staticText145; wxStaticText* m_staticTextSyncVarDescription; wxStaticLine* m_staticline431; wxCheckBox* m_checkBoxDetectMove; @@ -1157,4 +1157,33 @@ public: }; +/////////////////////////////////////////////////////////////////////////////// +/// Class CfgHighlightDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class CfgHighlightDlgGenerated : public wxDialog +{ +private: + +protected: + wxPanel* m_panel35; + wxStaticText* m_staticText145; + wxSpinCtrl* m_spinCtrlSyncOverdueDays; + wxStaticLine* m_staticline21; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonOkay; + wxButton* m_buttonCancel; + + // Virtual event handlers, overide them in your derived class + virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } + virtual void OnOkay( wxCommandEvent& event ) { event.Skip(); } + virtual void OnCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + CfgHighlightDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Highlight Configurations"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + ~CfgHighlightDlgGenerated(); + +}; + #endif //__GUI_GENERATED_H__ diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp index 54b4ee93..ac9f2479 100755 --- a/FreeFileSync/Source/ui/gui_status_handler.cpp +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -346,6 +346,7 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog() { //post sync action bool showSummary = true; + bool triggerSleep = false; if (!getAbortStatus() || *getAbortStatus() != AbortTrigger::USER) //user cancelled => don't run post sync action! switch (progressDlg_->getOptionPostSyncAction()) { @@ -356,11 +357,7 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog() exitAfterSync_ = true; //program shutdown must be handled by calling context! break; case PostSyncAction::SLEEP: - try - { - tryReportingError([&] { suspendSystem(); /*throw FileError*/ }, *this); //throw X - } - catch (...) {} + triggerSleep = true; break; case PostSyncAction::SHUTDOWN: showSummary = false; @@ -379,6 +376,13 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog() else progressDlg_->closeDirectly(false /*restoreParentFrame*/); + if (triggerSleep) //sleep *after* showing results dialog (consider total time!) + try + { + tryReportingError([&] { suspendSystem(); /*throw FileError*/ }, *this); //throw X + } + catch (...) {} + //wait until progress dialog notified shutdown via onProgressDialogTerminate() //-> required since it has our "this" pointer captured in lambda "notifyWindowTerminate"! //-> nicely manages dialog lifetime diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index ddd864fd..41ccf802 100755 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -23,8 +23,10 @@ #include #include #include +#include #include #include +#include "cfg_grid.h" #include "version_check.h" #include "gui_status_handler.h" #include "small_dlgs.h" @@ -55,14 +57,6 @@ namespace const size_t EXT_APP_MASS_INVOKE_THRESHOLD = 10; //more than this is likely a user mistake (Explorer uses limit of 15) -struct wxClientHistoryData : public wxClientData //we need a wxClientData derived class to tell wxWidgets to take object ownership! -{ - wxClientHistoryData(const Zstring& cfgFile, int lastUseIndex) : cfgFile_(cfgFile), lastUseIndex_(lastUseIndex) {} - - Zstring cfgFile_; - int lastUseIndex_; //support sorting history by last usage, the higher the index the more recent the usage -}; - IconBuffer::IconSize convert(xmlAccess::FileIconSize isize) { using namespace xmlAccess; @@ -78,56 +72,6 @@ IconBuffer::IconSize convert(xmlAccess::FileIconSize isize) return IconBuffer::SIZE_SMALL; } -//pretty much the same like "bool wxWindowBase::IsDescendant(wxWindowBase* child) const" but without the obvious misnomer -inline -bool isComponentOf(const wxWindow* child, const wxWindow* top) -{ - for (const wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) - if (wnd == top) - return true; - return false; -} - - -inline -wxTopLevelWindow* getTopLevelWindow(wxWindow* child) -{ - for (wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) - if (auto tlw = dynamic_cast(wnd)) //why does wxWidgets use wxWindows::IsTopLevel() ?? - return tlw; - return nullptr; -} - - -/* -Preserving input focus has to be more clever than: - wxWindow* oldFocus = wxWindow::FindFocus(); - ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus()); - -=> wxWindow::SetFocus() internally calls Win32 ::SetFocus, which calls ::SetActiveWindow, which - lord knows why - changes the foreground window to the focus window - even if the user is currently busy using a different app! More curiosity: this foreground focus stealing happens only during the *first* SetFocus() after app start! - It also can be avoided by changing focus back and forth with some other app after start => wxWidgets bug or Win32 feature??? -*/ -struct FocusPreserver -{ - ~FocusPreserver() - { - //wxTopLevelWindow::IsActive() does NOT call Win32 ::GetActiveWindow()! - //Instead it checks if ::GetFocus() is set somewhere inside the top level - //Note: Both Win32 active and focus windows are *thread-local* values, while foreground window is global! https://blogs.msdn.microsoft.com/oldnewthing/20131016-00/?p=2913 - if (oldFocus_) - if (wxTopLevelWindow* topWin = getTopLevelWindow(oldFocus_)) - if (topWin->IsActive()) //Linux/macOS: already behaves just like ::GetForegroundWindow() on Windows! - oldFocus_->SetFocus(); - } - - wxWindow* getFocus() const { return oldFocus_; } - void setFocus(wxWindow* win) { oldFocus_ = win; } - -private: - wxWindow* oldFocus_ = wxWindow::FindFocus(); -}; - bool acceptDialogFileDrop(const std::vector& shellItemPaths) { @@ -158,6 +102,7 @@ public: { if (acceptDialogFileDrop(shellItemPaths)) { + assert(!shellItemPaths.empty()); mainDlg_.loadConfiguration(shellItemPaths); return false; } @@ -351,21 +296,13 @@ xmlAccess::XmlGlobalSettings tryLoadGlobalConfig(const Zstring& globalConfigFile } -Zstring MainDialog::getLastRunConfigPath() -{ - return zen::getConfigDirPathPf() + Zstr("LastRun.ffs_gui"); -} - - void MainDialog::create(const Zstring& globalConfigFilePath) { using namespace xmlAccess; const XmlGlobalSettings globalSettings = tryLoadGlobalConfig(globalConfigFilePath); - std::vector cfgFilePaths; - for (const ConfigFileItem& item : globalSettings.gui.lastUsedConfigFiles) - cfgFilePaths.push_back(item.filePath_); + std::vector cfgFilePaths = globalSettings.gui.mainDlg.lastUsedConfigFiles; //------------------------------------------------------------------------------------------ //check existence of all files in parallel: @@ -451,14 +388,15 @@ void MainDialog::create(const Zstring& globalConfigFilePath, } -MainDialog::MainDialog(const Zstring& globalConfigFile, +MainDialog::MainDialog(const Zstring& globalConfigFilePath, const xmlAccess::XmlGuiConfig& guiCfg, const std::vector& referenceFiles, const xmlAccess::XmlGlobalSettings& globalSettings, bool startComparison) : MainDialogGenerated(nullptr), - globalConfigFile_(globalConfigFile), + globalConfigFilePath_(globalConfigFilePath), lastRunConfigPath_(getLastRunConfigPath()) + { m_folderPathLeft ->init(folderHistoryLeft_); m_folderPathRight->init(folderHistoryRight_); @@ -539,7 +477,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFile, wxAuiPaneInfo().Name(L"ViewFilterPanel").Layer(2).Bottom().Row(1).Caption(_("View Settings")).CaptionVisible(false).PaneBorder(false).Gripper().MinSize(m_bpButtonViewTypeSyncAction->GetSize().GetWidth(), m_panelViewFilter->GetSize().GetHeight())); auiMgr_.AddPane(m_panelConfig, - wxAuiPaneInfo().Name(L"ConfigPanel").Layer(3).Left().Position(1).Caption(_("Configuration")).MinSize(m_listBoxHistory->GetSize().GetWidth(), m_panelConfig->GetSize().GetHeight())); + wxAuiPaneInfo().Name(L"ConfigPanel").Layer(3).Left().Position(1).Caption(_("Configuration")).MinSize(bSizerCfgHistoryButtons->GetSize())); auiMgr_.AddPane(m_gridOverview, wxAuiPaneInfo().Name(L"OverviewPanel").Layer(3).Left().Position(2).Caption(_("Overview")).MinSize(300, m_gridOverview->GetSize().GetHeight())); //MinSize(): just default size, see comment below @@ -573,25 +511,34 @@ MainDialog::MainDialog(const Zstring& globalConfigFile, m_panelStatusBar ->Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(MainDialog::OnContextSetLayout), nullptr, this); //---------------------------------------------------------------------------------- - //sort grids + //file grid: sorting m_gridMainL->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(MainDialog::onGridLabelLeftClickL), nullptr, this); m_gridMainC->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(MainDialog::onGridLabelLeftClickC), nullptr, this); m_gridMainR->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(MainDialog::onGridLabelLeftClickR), nullptr, this); - m_gridMainL->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onGridLabelContextL ), nullptr, this); - m_gridMainC->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onGridLabelContextC ), nullptr, this); - m_gridMainR->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onGridLabelContextR ), nullptr, this); - - //grid context menu - m_gridMainL ->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextL), nullptr, this); - m_gridMainC ->Connect(EVENT_GRID_MOUSE_RIGHT_DOWN, GridClickEventHandler(MainDialog::onMainGridContextC), nullptr, this); - m_gridMainR ->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextR), nullptr, this); - m_gridOverview->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onNaviGridContext ), nullptr, this); - - m_gridMainL->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onGridDoubleClickL), nullptr, this ); - m_gridMainR->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onGridDoubleClickR), nullptr, this ); - - m_gridOverview->Connect(EVENT_GRID_SELECT_RANGE, GridRangeSelectEventHandler(MainDialog::onNaviSelection), nullptr, this); + m_gridMainL->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onGridLabelContextL), nullptr, this); + m_gridMainC->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onGridLabelContextC), nullptr, this); + m_gridMainR->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onGridLabelContextR), nullptr, this); + + //file grid: context menu + m_gridMainL->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextL), nullptr, this); + m_gridMainC->Connect(EVENT_GRID_MOUSE_RIGHT_DOWN, GridClickEventHandler(MainDialog::onMainGridContextC), nullptr, this); + m_gridMainR->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onMainGridContextR), nullptr, this); + + m_gridMainL->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onGridDoubleClickL), nullptr, this); + m_gridMainR->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onGridDoubleClickR), nullptr, this); + + //tree grid: + m_gridOverview->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onTreeGridContext), nullptr, this); + m_gridOverview->Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(MainDialog::onTreeGridSelection), nullptr, this); + + //cfg grid: + m_gridCfgHistory->Connect(EVENT_GRID_SELECT_RANGE, GridSelectEventHandler(MainDialog::onCfgGridSelection), nullptr, this); + m_gridCfgHistory->Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(MainDialog::onCfgGridDoubleClick), nullptr, this); + m_gridCfgHistory->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(MainDialog::onCfgGridKeyEvent), nullptr, this); + m_gridCfgHistory->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(MainDialog::onCfgGridContext), nullptr, this); + m_gridCfgHistory->Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(MainDialog::onCfgGridLabelContext ), nullptr, this); + m_gridCfgHistory->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(MainDialog::onCfgGridLabelLeftClick), nullptr, this); //---------------------------------------------------------------------------------- m_panelSearch->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(MainDialog::OnSearchPanelKeyPressed), nullptr, this); @@ -611,9 +558,6 @@ MainDialog::MainDialog(const Zstring& globalConfigFile, m_bpButtonCmpContext ->SetToolTip(m_bpButtonCmpConfig ->GetToolTipText()); m_bpButtonSyncContext->SetToolTip(m_bpButtonSyncConfig->GetToolTipText()); - gridDataView_ = std::make_shared(); - treeDataView_ = std::make_shared(); - { const wxBitmap& bmpFile = IconBuffer::genericFileIcon(IconBuffer::SIZE_SMALL); @@ -701,8 +645,9 @@ MainDialog::MainDialog(const Zstring& globalConfigFile, initViewFilterButtons(); //init grid settings - gridview::init(*m_gridMainL, *m_gridMainC, *m_gridMainR, gridDataView_); - treeview::init(*m_gridOverview, treeDataView_); + filegrid::init(*m_gridMainL, *m_gridMainC, *m_gridMainR); + treegrid::init(*m_gridOverview); + cfggrid ::init(*m_gridCfgHistory); //initialize and load configuration setGlobalCfgOnInit(globalSettings); @@ -753,19 +698,16 @@ MainDialog::MainDialog(const Zstring& globalConfigFile, OnResizeLeftFolderWidth(evtDummy); // //scroll cfg history to last used position. We cannot do this earlier e.g. in setGlobalCfgOnInit() - //1. setConfig() indirectly calls addFileToCfgHistory() which changes cfg history scroll position - //2. EnsureVisible() requires final window height! => do this after window resizing is complete - if (!m_listBoxHistory->IsEmpty()) - m_listBoxHistory->SetFirstItem(numeric::clampCpy(globalSettings.gui.cfgFileHistFirstItemPos, //must be set *after* wxAuiManager::LoadPerspective() to have any effect - 0, static_cast(m_listBoxHistory->GetCount()) - 1)); - - //first selected item must be visible: - for (int i = 0; i < static_cast(m_listBoxHistory->GetCount()); ++i) - if (m_listBoxHistory->IsSelected(i)) - { - m_listBoxHistory->EnsureVisible(i); - break; - } + //1. setConfig() indirectly calls cfggrid::addAndSelect() which changes cfg history scroll position + //2. Grid::makeRowVisible() requires final window height! => do this after window resizing is complete + if (m_gridCfgHistory->getRowCount() > 0) + m_gridCfgHistory->scrollTo(numeric::clampCpy(globalSettings.gui.mainDlg.cfgGridTopRowPos, //must be set *after* wxAuiManager::LoadPerspective() to have any effect + 0, m_gridCfgHistory->getRowCount() - 1)); + + //first selected item should always be visible: + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + if (!selectedRows.empty()) + m_gridCfgHistory->makeRowVisible(selectedRows.front()); m_buttonCompare->SetFocus(); @@ -824,11 +766,12 @@ MainDialog::MainDialog(const Zstring& globalConfigFile, !firstMissingDir.get(); //= all directories exist if (startComparisonNow) + { + wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); + //better!? => m_buttonCompare->Command(dummy2); //simulate click if (wxEvtHandler* evtHandler = m_buttonCompare->GetEventHandler()) - { - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); evtHandler->AddPendingEvent(dummy2); //simulate button click on "compare" - } + } } } } @@ -841,7 +784,7 @@ MainDialog::~MainDialog() Opt firstError; try //save "GlobalSettings.xml" { - writeConfig(getGlobalCfgBeforeExit(), globalConfigFile_); //throw FileError + writeConfig(getGlobalCfgBeforeExit(), globalConfigFilePath_); //throw FileError } catch (const FileError& e) { firstError = e; } @@ -849,13 +792,16 @@ MainDialog::~MainDialog() { writeConfig(getConfig(), lastRunConfigPath_); //throw FileError } - catch (const FileError& e) { firstError = e; } + catch (const FileError& e) + { + if (!firstError) + firstError = e; + } //don't annoy users on read-only drives: it's enough to show a single error message when saving global config if (firstError) showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(firstError->toString())); - auiMgr_.UnInit(); for (wxMenuItem* item : detachedMenuItems_) @@ -871,7 +817,7 @@ void MainDialog::onQueryEndSession() using namespace xmlAccess; //we try our best to do something useful in this extreme situation - no reason to notify or even log errors here! - try { writeConfig(getGlobalCfgBeforeExit(), globalConfigFile_); } + try { writeConfig(getGlobalCfgBeforeExit(), globalConfigFilePath_); } catch (const FileError&) {} try { writeConfig(getConfig(), lastRunConfigPath_); } @@ -955,40 +901,52 @@ void MainDialog::setGlobalCfgOnInit(const xmlAccess::XmlGlobalSettings& globalSe } //set column attributes - m_gridMainL ->setColumnConfig(gridview::convertConfig(globalSettings.gui.mainDlg.columnAttribLeft)); - m_gridMainR ->setColumnConfig(gridview::convertConfig(globalSettings.gui.mainDlg.columnAttribRight)); + m_gridMainL ->setColumnConfig(convertColAttributes(globalSettings.gui.mainDlg.columnAttribLeft, getFileGridDefaultColAttribsLeft())); + m_gridMainR ->setColumnConfig(convertColAttributes(globalSettings.gui.mainDlg.columnAttribRight, getFileGridDefaultColAttribsLeft())); m_splitterMain->setSashOffset(globalSettings.gui.mainDlg.sashOffset); - m_gridOverview->setColumnConfig(treeview::convertConfig(globalSettings.gui.mainDlg.columnAttribNavi)); - treeview::setShowPercentage(*m_gridOverview, globalSettings.gui.mainDlg.naviGridShowPercentBar); + m_gridOverview->setColumnConfig(convertColAttributes(globalSettings.gui.mainDlg.treeGridColumnAttribs, getTreeGridDefaultColAttribs())); + treegrid::setShowPercentage(*m_gridOverview, globalSettings.gui.mainDlg.treeGridShowPercentBar); - treeDataView_->setSortDirection(globalSettings.gui.mainDlg.naviGridLastSortColumn, globalSettings.gui.mainDlg.naviGridLastSortAscending); + treegrid::getDataView(*m_gridOverview).setSortDirection(globalSettings.gui.mainDlg.treeGridLastSortColumn, globalSettings.gui.mainDlg.treeGridLastSortAscending); //-------------------------------------------------------------------------------- - //load list of last used configuration files + //load list of configuration files std::vector cfgFilePaths; - for (const xmlAccess::ConfigFileItem& item : globalSettings.gui.cfgFileHistory) - cfgFilePaths.push_back(item.filePath_); - std::reverse(cfgFilePaths.begin(), cfgFilePaths.end()); - //list is stored with last used files first in xml, however addFileToCfgHistory() needs them last!!! - + std::vector> lastSyncTimes; + //list is stored with last used files first in XML, however m_gridCfgHistory needs them last!!! + std::for_each(globalSettings.gui.mainDlg.cfgFileHistory.crbegin(), + globalSettings.gui.mainDlg.cfgFileHistory.crend(), + [&](const xmlAccess::ConfigFileItem& item) + { + cfgFilePaths.push_back(item.filePath); + lastSyncTimes.emplace_back(item.filePath, item.lastSyncTime); + }); + warn_static("finish") cfgFilePaths.push_back(lastRunConfigPath_); //make sure is always part of history list (if existing) - addFileToCfgHistory(cfgFilePaths); + cfggrid::getDataView(*m_gridCfgHistory).addCfgFiles(cfgFilePaths); + cfggrid::getDataView(*m_gridCfgHistory).setLastSyncTime(lastSyncTimes); + m_gridCfgHistory->Refresh(); + + m_gridCfgHistory->setColumnConfig(convertColAttributes(globalSettings.gui.mainDlg.cfgGridColumnAttribs, getCfgGridDefaultColAttribs())); + cfggrid::getDataView(*m_gridCfgHistory).setSortDirection(globalSettings.gui.mainDlg.cfgGridLastSortColumn, globalSettings.gui.mainDlg.cfgGridLastSortAscending); + cfggrid::setSyncOverdueDays(*m_gridCfgHistory, globalSettings.gui.mainDlg.cfgGridSyncOverdueDays); + //m_gridCfgHistory->Refresh(); <- implicit in last call - removeObsoleteCfgHistoryItems(cfgFilePaths); //remove non-existent items (we need this only on startup) + cfgHistoryRemoveObsolete(cfgFilePaths); //remove non-existent items (we need this only on startup) //globalSettings.gui.cfgFileHistFirstItemPos => defer evaluation until later within MainDialog constructor //-------------------------------------------------------------------------------- //load list of last used folders - *folderHistoryLeft_ = FolderHistory(globalSettings.gui.folderHistoryLeft, globalSettings.gui.folderHistMax); - *folderHistoryRight_ = FolderHistory(globalSettings.gui.folderHistoryRight, globalSettings.gui.folderHistMax); + *folderHistoryLeft_ = FolderHistory(globalSettings.gui.mainDlg.folderHistoryLeft, globalSettings.gui.mainDlg.folderHistItemsMax); + *folderHistoryRight_ = FolderHistory(globalSettings.gui.mainDlg.folderHistoryRight, globalSettings.gui.mainDlg.folderHistItemsMax); //show/hide file icons - gridview::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalSettings.gui.mainDlg.showIcons, convert(globalSettings.gui.mainDlg.iconSize)); + filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalSettings.gui.mainDlg.showIcons, convert(globalSettings.gui.mainDlg.iconSize)); - gridview::setItemPathForm(*m_gridMainL, globalSettings.gui.mainDlg.itemPathFormatLeftGrid); - gridview::setItemPathForm(*m_gridMainR, globalSettings.gui.mainDlg.itemPathFormatRightGrid); + filegrid::setItemPathForm(*m_gridMainL, globalSettings.gui.mainDlg.itemPathFormatLeftGrid); + filegrid::setItemPathForm(*m_gridMainR, globalSettings.gui.mainDlg.itemPathFormatRightGrid); //------------------------------------------------------------------------------------------------ m_checkBoxMatchCase->SetValue(globalCfg_.gui.mainDlg.textSearchRespectCase); @@ -1026,45 +984,48 @@ xmlAccess::XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit() globalSettings.programLanguage = getLanguage(); //retrieve column attributes - globalSettings.gui.mainDlg.columnAttribLeft = gridview::convertConfig(m_gridMainL->getColumnConfig()); - globalSettings.gui.mainDlg.columnAttribRight = gridview::convertConfig(m_gridMainR->getColumnConfig()); + globalSettings.gui.mainDlg.columnAttribLeft = convertColAttributes(m_gridMainL->getColumnConfig()); + globalSettings.gui.mainDlg.columnAttribRight = convertColAttributes(m_gridMainR->getColumnConfig()); globalSettings.gui.mainDlg.sashOffset = m_splitterMain->getSashOffset(); - globalSettings.gui.mainDlg.columnAttribNavi = treeview::convertConfig(m_gridOverview->getColumnConfig()); - globalSettings.gui.mainDlg.naviGridShowPercentBar = treeview::getShowPercentage(*m_gridOverview); + globalSettings.gui.mainDlg.treeGridColumnAttribs = convertColAttributes(m_gridOverview->getColumnConfig()); + globalSettings.gui.mainDlg.treeGridShowPercentBar = treegrid::getShowPercentage(*m_gridOverview); - const std::pair sortInfo = treeDataView_->getSortDirection(); - globalSettings.gui.mainDlg.naviGridLastSortColumn = sortInfo.first; - globalSettings.gui.mainDlg.naviGridLastSortAscending = sortInfo.second; + std::tie(globalSettings.gui.mainDlg.treeGridLastSortColumn, + globalSettings.gui.mainDlg.treeGridLastSortAscending) = treegrid::getDataView(*m_gridOverview).getSortDirection(); //-------------------------------------------------------------------------------- - //write list of last used configuration files - std::map historyDetail; //(cfg-file/last use index) - for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) - if (auto clientString = dynamic_cast(m_listBoxHistory->GetClientObject(i))) - historyDetail.emplace(clientString->lastUseIndex_, clientString->cfgFile_); + //write list of configuration files + std::map cfgItemsSorted; //(last use index/cfg file path) + for (size_t i = 0; i < m_gridCfgHistory->getRowCount(); ++i) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(i)) + cfgItemsSorted.emplace(cfg->lastUseIndex, xmlAccess::ConfigFileItem{ cfg->filePath, cfg->lastSyncTime }); else assert(false); - //sort by last use; put most recent items *first* (looks better in xml than reverted) - std::vector history; - for (const auto& item : historyDetail) - history.emplace_back(item.second); - std::reverse(history.begin(), history.end()); + //sort by last use; put most recent items *first* (looks better in XML than reverted) + std::vector cfgHistory; + std::for_each(cfgItemsSorted.crbegin(), + cfgItemsSorted.crend(), + [&](const auto& item) { cfgHistory.emplace_back(item.second); }); - if (history.size() > globalSettings.gui.cfgFileHistMax) //erase oldest elements - history.resize(globalSettings.gui.cfgFileHistMax); + if (cfgHistory.size() > globalSettings.gui.mainDlg.cfgHistItemsMax) //erase oldest elements + cfgHistory.resize(globalSettings.gui.mainDlg.cfgHistItemsMax); - globalSettings.gui.cfgFileHistory = history; - globalSettings.gui.cfgFileHistFirstItemPos = m_listBoxHistory->GetTopItem(); + globalSettings.gui.mainDlg.cfgFileHistory = cfgHistory; + + globalSettings.gui.mainDlg.cfgGridTopRowPos = m_gridCfgHistory->getTopRow(); + globalSettings.gui.mainDlg.cfgGridColumnAttribs = convertColAttributes(m_gridCfgHistory->getColumnConfig()); + globalSettings.gui.mainDlg.cfgGridSyncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory); + + std::tie(globalSettings.gui.mainDlg.cfgGridLastSortColumn, + globalSettings.gui.mainDlg.cfgGridLastSortAscending) = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection(); //-------------------------------------------------------------------------------- - globalSettings.gui.lastUsedConfigFiles.clear(); - for (const Zstring& cfgFilePath : activeConfigFiles_) - globalSettings.gui.lastUsedConfigFiles.emplace_back(cfgFilePath); + globalSettings.gui.mainDlg.lastUsedConfigFiles = activeConfigFiles_; //write list of last used folders - globalSettings.gui.folderHistoryLeft = folderHistoryLeft_ ->getList(); - globalSettings.gui.folderHistoryRight = folderHistoryRight_->getList(); + globalSettings.gui.mainDlg.folderHistoryLeft = folderHistoryLeft_ ->getList(); + globalSettings.gui.mainDlg.folderHistoryRight = folderHistoryRight_->getList(); globalSettings.gui.mainDlg.textSearchRespectCase = m_checkBoxMatchCase->GetValue(); @@ -1143,18 +1104,18 @@ void MainDialog::copySelectionToClipboard(const std::vector& gridRe { if (auto prov = grid.getDataProvider()) { - std::vector colAttr = grid.getColumnConfig(); - erase_if(colAttr, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); + std::vector colAttr = grid.getColumnConfig(); + erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); if (!colAttr.empty()) for (size_t row : grid.getSelectedRows()) { std::for_each(colAttr.begin(), colAttr.end() - 1, - [&](const Grid::ColumnAttribute& ca) + [&](const Grid::ColAttributes& ca) { - clipboardString += copyStringTo(prov->getValue(row, ca.type_)); + clipboardString += copyStringTo(prov->getValue(row, ca.type)); clipboardString += L'\t'; }); - clipboardString += copyStringTo(prov->getValue(row, colAttr.back().type_)); + clipboardString += copyStringTo(prov->getValue(row, colAttr.back().type)); clipboardString += L'\n'; } } @@ -1190,7 +1151,7 @@ std::vector MainDialog::getGridSelection(bool fromLeft, bool removeDuplicates(selectedRows); assert(std::is_sorted(selectedRows.begin(), selectedRows.end())); - return gridDataView_->getAllFileRef(selectedRows); + return filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows); } @@ -1199,7 +1160,7 @@ std::vector MainDialog::getTreeSelection() const std::vector output; for (size_t row : m_gridOverview->getSelectedRows()) - if (std::unique_ptr node = treeDataView_->getLine(row)) + if (std::unique_ptr node = treegrid::getDataView(*m_gridOverview).getLine(row)) { if (auto root = dynamic_cast(node.get())) { @@ -1313,7 +1274,7 @@ void MainDialog::deleteSelectedFiles(const std::vector& selec catch (AbortProcess&) {} //do not clear grids, if aborted! //remove rows that are empty: just a beautification, invalid rows shouldn't cause issues - gridDataView_->removeInvalidRows(); + filegrid::getDataView(*m_gridMainC).removeInvalidRows(); updateGui(); } @@ -1554,10 +1515,10 @@ void MainDialog::setStatusBarFileStatistics(size_t filesOnLeftView, setText(*m_staticTextStatusRightBytes, L"(" + formatFilesizeShort(filesizeRightView) + L")"); //------------------------------------------------------------------------------ wxString statusCenterNew; - if (gridDataView_->rowsTotal() > 0) + if (filegrid::getDataView(*m_gridMainC).rowsTotal() > 0) { - statusCenterNew = _P("Showing %y of 1 row", "Showing %y of %x rows", gridDataView_->rowsTotal()); - replace(statusCenterNew, L"%y", formatNumber(gridDataView_->rowsOnView())); //%x is already used as plural form placeholder! + statusCenterNew = _P("Showing %y of 1 row", "Showing %y of %x rows", filegrid::getDataView(*m_gridMainC).rowsTotal()); + replace(statusCenterNew, L"%y", formatNumber(filegrid::getDataView(*m_gridMainC).rowsOnView())); //%x is already used as plural form placeholder! } //fill middle text (considering flashStatusInformation()) @@ -1655,6 +1616,7 @@ void MainDialog::disableAllElements(bool enableAbort) m_panelViewFilter ->Disable(); m_panelConfig ->Disable(); m_gridOverview ->Disable(); + m_gridCfgHistory ->Disable(); m_panelSearch ->Disable(); m_bpButtonCmpContext ->Disable(); m_bpButtonSyncContext->Disable(); @@ -1700,6 +1662,7 @@ void MainDialog::enableAllElements() m_panelViewFilter ->Enable(); m_panelConfig ->Enable(); m_gridOverview ->Enable(); + m_gridCfgHistory ->Enable(); m_panelSearch ->Enable(); m_bpButtonCmpContext ->Enable(); m_bpButtonSyncContext->Enable(); @@ -1969,24 +1932,21 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without //case WXK_F6: //{ // wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); //simulate button click - // if (wxEvtHandler* evtHandler = m_bpButtonCmpConfig->GetEventHandler()) - // evtHandler->ProcessEvent(dummy2); //synchronous call + // m_bpButtonCmpConfig->Command(dummy2); //simulate click //} //return; //-> swallow event! //case WXK_F7: //{ // wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); //simulate button click - // if (wxEvtHandler* evtHandler = m_bpButtonFilter->GetEventHandler()) - // evtHandler->ProcessEvent(dummy2); //synchronous call + // m_bpButtonFilter->Command(dummy2); //simulate click //} //return; //-> swallow event! //case WXK_F8: //{ // wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); //simulate button click - // if (wxEvtHandler* evtHandler = m_bpButtonSyncConfig->GetEventHandler()) - // evtHandler->ProcessEvent(dummy2); //synchronous call + // m_bpButtonSyncConfig->Command(dummy2); //simulate click //} //return; //-> swallow event! @@ -2018,9 +1978,9 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without !isComponentOf(focus, m_gridMainC ) && //don't propagate keyboard commands if grid is already in focus !isComponentOf(focus, m_gridMainR ) && // !isComponentOf(focus, m_gridOverview ) && - !isComponentOf(focus, m_listBoxHistory) && //don't propagate if selecting config + !isComponentOf(focus, m_gridCfgHistory) && //don't propagate if selecting config !isComponentOf(focus, m_panelSearch ) && - !isComponentOf(focus, m_panelTopLeft ) && //don't propagate if changing directory fields + !isComponentOf(focus, m_panelTopLeft ) && //don't propagate if changing directory fields !isComponentOf(focus, m_panelTopCenter) && !isComponentOf(focus, m_panelTopRight ) && !isComponentOf(focus, m_scrolledWindowFolderPairs) && @@ -2042,26 +2002,26 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without } -void MainDialog::onNaviSelection(GridRangeSelectEvent& event) +void MainDialog::onTreeGridSelection(GridSelectEvent& event) { //scroll m_gridMain to user's new selection on m_gridOverview ptrdiff_t leadRow = -1; if (event.positive_ && event.rowFirst_ != event.rowLast_) - if (std::unique_ptr node = treeDataView_->getLine(event.rowFirst_)) + if (std::unique_ptr node = treegrid::getDataView(*m_gridOverview).getLine(event.rowFirst_)) { if (const TreeView::RootNode* root = dynamic_cast(node.get())) - leadRow = gridDataView_->findRowFirstChild(&(root->baseFolder_)); + leadRow = filegrid::getDataView(*m_gridMainC).findRowFirstChild(&(root->baseFolder_)); else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) { - leadRow = gridDataView_->findRowDirect(&(dir->folder_)); + leadRow = filegrid::getDataView(*m_gridMainC).findRowDirect(&(dir->folder_)); if (leadRow < 0) //directory was filtered out! still on tree view (but NOT on grid view) - leadRow = gridDataView_->findRowFirstChild(&(dir->folder_)); + leadRow = filegrid::getDataView(*m_gridMainC).findRowFirstChild(&(dir->folder_)); } else if (const TreeView::FilesNode* files = dynamic_cast(node.get())) { assert(!files->filesAndLinks_.empty()); if (!files->filesAndLinks_.empty()) - leadRow = gridDataView_->findRowDirect(files->filesAndLinks_[0]->getId()); + leadRow = filegrid::getDataView(*m_gridMainC).findRowDirect(files->filesAndLinks_[0]->getId()); } } @@ -2076,12 +2036,12 @@ void MainDialog::onNaviSelection(GridRangeSelectEvent& event) m_gridOverview->getMainWin().Update(); //draw cursor immediately rather than on next idle event (required for slow CPUs, netbook) } - //get selection on navigation tree and set corresponding markers on main grid + //get selection on overview panel and set corresponding markers on main grid std::unordered_set markedFilesAndLinks; //mark files/symlinks directly std::unordered_set markedContainer; //mark full container including child-objects for (size_t row : m_gridOverview->getSelectedRows()) - if (std::unique_ptr node = treeDataView_->getLine(row)) + if (std::unique_ptr node = treegrid::getDataView(*m_gridOverview).getLine(row)) { if (const TreeView::RootNode* root = dynamic_cast(node.get())) markedContainer.insert(&(root->baseFolder_)); @@ -2091,13 +2051,13 @@ void MainDialog::onNaviSelection(GridRangeSelectEvent& event) markedFilesAndLinks.insert(files->filesAndLinks_.begin(), files->filesAndLinks_.end()); } - gridview::setNavigationMarker(*m_gridMainL, std::move(markedFilesAndLinks), std::move(markedContainer)); + filegrid::setNavigationMarker(*m_gridMainL, std::move(markedFilesAndLinks), std::move(markedContainer)); event.Skip(); } -void MainDialog::onNaviGridContext(GridClickEvent& event) +void MainDialog::onTreeGridContext(GridClickEvent& event) { const auto& selection = getTreeSelection(); //referenced by lambdas! ContextMenu menu; @@ -2198,13 +2158,13 @@ void MainDialog::onMainGridContextC(GridClickEvent& event) { zen::setActiveStatus(true, folderCmp_); updateGui(); - }, nullptr, gridDataView_->rowsTotal() > 0); + }, nullptr, filegrid::getDataView(*m_gridMainC).rowsTotal() > 0); menu.addItem(_("Exclude all"), [&] { zen::setActiveStatus(false, folderCmp_); updateGuiDelayedIf(!m_bpButtonShowExcluded->isActive()); //show update GUI before removing rows - }, nullptr, gridDataView_->rowsTotal() > 0); + }, nullptr, filegrid::getDataView(*m_gridMainC).rowsTotal() > 0); menu.popup(*this); } @@ -2476,38 +2436,38 @@ void MainDialog::onGridLabelContextR(GridLabelClickEvent& event) void MainDialog::onGridLabelContextRim(Grid& grid, ColumnTypeRim type, bool left) { ContextMenu menu; - + //-------------------------------------------------------------------------------------------------------- auto toggleColumn = [&](ColumnType ct) { auto colAttr = grid.getColumnConfig(); - Grid::ColumnAttribute* caItemPath = nullptr; - Grid::ColumnAttribute* caToggle = nullptr; + Grid::ColAttributes* caItemPath = nullptr; + Grid::ColAttributes* caToggle = nullptr; - for (Grid::ColumnAttribute& ca : colAttr) - if (ca.type_ == static_cast(ColumnTypeRim::ITEM_PATH)) + for (Grid::ColAttributes& ca : colAttr) + if (ca.type == static_cast(ColumnTypeRim::ITEM_PATH)) caItemPath = &ca; - else if (ca.type_ == ct) + else if (ca.type == ct) caToggle = &ca; - assert(caItemPath && caItemPath->stretch_ > 0 && caItemPath->visible_); - assert(caToggle && caToggle->stretch_ == 0); + assert(caItemPath && caItemPath->stretch > 0 && caItemPath->visible); + assert(caToggle && caToggle ->stretch == 0); if (caItemPath && caToggle) { - caToggle->visible_ = !caToggle->visible_; + caToggle->visible = !caToggle->visible; //take width of newly visible column from stretched item path column - caItemPath->offset_ -= caToggle->visible_ ? caToggle->offset_ : -caToggle->offset_; + caItemPath->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset; grid.setColumnConfig(colAttr); } }; if (const GridData* prov = grid.getDataProvider()) - for (const Grid::ColumnAttribute& ca : grid.getColumnConfig()) - menu.addCheckBox(prov->getColumnLabel(ca.type_), [ct = ca.type_, toggleColumn] { toggleColumn(ct); }, - ca.visible_, ca.type_ != static_cast(ColumnTypeRim::ITEM_PATH)); //do not allow user to hide this column! + for (const Grid::ColAttributes& ca : grid.getColumnConfig()) + menu.addCheckBox(prov->getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); }, + ca.visible, ca.type != static_cast(ColumnTypeRim::ITEM_PATH)); //do not allow user to hide this column! //---------------------------------------------------------------------------------------------- menu.addSeparator(); @@ -2516,7 +2476,7 @@ void MainDialog::onGridLabelContextRim(Grid& grid, ColumnTypeRim type, bool left auto setItemPathFormat = [&](ItemPathFormat fmt) { itemPathFormat = fmt; - gridview::setItemPathForm(grid, fmt); + filegrid::setItemPathForm(grid, fmt); }; auto addFormatEntry = [&](const wxString& label, ItemPathFormat fmt) { @@ -2527,37 +2487,39 @@ void MainDialog::onGridLabelContextRim(Grid& grid, ColumnTypeRim type, bool left addFormatEntry(_("Item name" ), ItemPathFormat::ITEM_NAME); //---------------------------------------------------------------------------------------------- - menu.addSeparator(); + auto setIconSize = [&](xmlAccess::FileIconSize sz, bool showIcons) + { + globalCfg_.gui.mainDlg.iconSize = sz; + globalCfg_.gui.mainDlg.showIcons = showIcons; + filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.gui.mainDlg.showIcons, convert(globalCfg_.gui.mainDlg.iconSize)); + }; + auto setDefault = [&] { - grid.setColumnConfig(gridview::convertConfig(left ? getDefaultColumnAttributesLeft() : getDefaultColumnAttributesRight())); + const xmlAccess::XmlGlobalSettings defaultCfg; + + grid.setColumnConfig(convertColAttributes(left ? defaultCfg.gui.mainDlg.columnAttribLeft : defaultCfg.gui.mainDlg.columnAttribRight, defaultCfg.gui.mainDlg.columnAttribLeft)); + + setItemPathFormat(left ? defaultCfg.gui.mainDlg.itemPathFormatLeftGrid : defaultCfg.gui.mainDlg.itemPathFormatRightGrid); + + setIconSize(defaultCfg.gui.mainDlg.iconSize, defaultCfg.gui.mainDlg.showIcons); }; menu.addItem(_("&Default"), setDefault); //'&' -> reuse text from "default" buttons elsewhere //---------------------------------------------------------------------------------------------- menu.addSeparator(); - menu.addCheckBox(_("Show icons:"), [&] - { - globalCfg_.gui.mainDlg.showIcons = !globalCfg_.gui.mainDlg.showIcons; - gridview::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.gui.mainDlg.showIcons, convert(globalCfg_.gui.mainDlg.iconSize)); - - }, globalCfg_.gui.mainDlg.showIcons); + menu.addCheckBox(_("Show icons:"), [&] { setIconSize(globalCfg_.gui.mainDlg.iconSize, !globalCfg_.gui.mainDlg.showIcons); }, globalCfg_.gui.mainDlg.showIcons); - auto setIconSize = [&](xmlAccess::FileIconSize sz) - { - globalCfg_.gui.mainDlg.iconSize = sz; - gridview::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.gui.mainDlg.showIcons, convert(sz)); - }; auto addSizeEntry = [&](const wxString& label, xmlAccess::FileIconSize sz) { - menu.addRadio(label, [sz, &setIconSize] { setIconSize(sz); }, globalCfg_.gui.mainDlg.iconSize == sz, globalCfg_.gui.mainDlg.showIcons); + menu.addRadio(label, [sz, &setIconSize] { setIconSize(sz, true /*showIcons*/); }, globalCfg_.gui.mainDlg.iconSize == sz, globalCfg_.gui.mainDlg.showIcons); }; addSizeEntry(L" " + _("Small" ), xmlAccess::ICON_SIZE_SMALL ); addSizeEntry(L" " + _("Medium"), xmlAccess::ICON_SIZE_MEDIUM); addSizeEntry(L" " + _("Large" ), xmlAccess::ICON_SIZE_LARGE ); //---------------------------------------------------------------------------------------------- - if (type == ColumnTypeRim::DATE) + // if (type == ColumnTypeRim::DATE) { menu.addSeparator(); @@ -2572,8 +2534,9 @@ void MainDialog::onGridLabelContextRim(Grid& grid, ColumnTypeRim type, bool left }; menu.addItem(_("Select time span..."), selectTimeSpan); } - + //-------------------------------------------------------------------------------------------------------- menu.popup(*this); + //event.Skip(); } @@ -2702,6 +2665,7 @@ void MainDialog::OnSyncSettingsContext(wxEvent& event) void MainDialog::onDialogFilesDropped(FileDropEvent& event) { + assert(!event.getPaths().empty()); loadConfiguration(event.getPaths()); //event.Skip(); } @@ -2722,95 +2686,7 @@ void MainDialog::onDirManualCorrection(wxCommandEvent& event) } -Zstring getFormattedHistoryElement(const Zstring& filepath) -{ - Zstring output = afterLast(filepath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); - if (endsWith(output, Zstr(".ffs_gui"))) - output = beforeLast(output, Zstr('.'), IF_MISSING_RETURN_NONE); - return output; -} - - -void MainDialog::addFileToCfgHistory(const std::vector& filePaths) -{ - //determine highest "last use" index number of m_listBoxHistory - int lastUseIndexMax = 0; - for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) - if (auto histData = dynamic_cast(m_listBoxHistory->GetClientObject(i))) - lastUseIndexMax = std::max(lastUseIndexMax, histData->lastUseIndex_); - else - assert(false); - - std::deque selections(m_listBoxHistory->GetCount()); //items to select after update of history list - - for (const Zstring& filePath : filePaths) - { - //Do we need to additionally check for aliases of the same physical files here? (and aliases for lastRunConfigName?) - - const auto itemPos = [&]() -> std::pair - { - for (unsigned int i = 0; i < m_listBoxHistory->GetCount(); ++i) - if (auto histData = dynamic_cast(m_listBoxHistory->GetClientObject(i))) - { - if (equalFilePath(filePath, histData->cfgFile_)) - return std::make_pair(histData, i); - } - else - assert(false); - return std::make_pair(nullptr, 0); - }(); - - if (itemPos.first) //update - { - itemPos.first->lastUseIndex_ = ++lastUseIndexMax; - selections[itemPos.second] = true; - } - else //insert - { - const wxString lastSessionLabel = L"<" + _("Last session") + L">"; - - wxString newLabel; - unsigned int newPos = 0; - - if (equalFilePath(filePath, lastRunConfigPath_)) - newLabel = lastSessionLabel; - else - { - //workaround wxWidgets 2.9 bug on GTK screwing up the client data if the list box is sorted: - const Zstring labelFmt = getFormattedHistoryElement(filePath); - newLabel = utfTo(labelFmt); - - //"linear-time insertion sort": - for (; newPos < m_listBoxHistory->GetCount(); ++newPos) - { - if (auto histData = dynamic_cast(m_listBoxHistory->GetClientObject(newPos))) - if (equalFilePath(histData->cfgFile_, lastRunConfigPath_)) - continue; //last session label should always be at top position! - - if (LessNaturalSort()(labelFmt, utfTo(m_listBoxHistory->GetString(newPos)))) - break; - } - } - - assert(!m_listBoxHistory->IsSorted()); - m_listBoxHistory->Insert(newLabel, newPos, new wxClientHistoryData(filePath, ++lastUseIndexMax)); - - selections.insert(selections.begin() + newPos, true); - } - } - - assert(selections.size() == m_listBoxHistory->GetCount()); - - //do not apply selections immediately but only when needed! - //this prevents problems with m_listBoxHistory losing keyboard selection focus if identical selection is redundantly reapplied - for (int pos = 0; pos < static_cast(selections.size()); ++pos) - if (m_listBoxHistory->IsSelected(pos) != selections[pos]) - m_listBoxHistory->SetSelection(pos, selections[pos]); - //note: a *positive* SetSelection() will include a EnsureVisible()! -} - - -void MainDialog::removeObsoleteCfgHistoryItems(const std::vector& filePaths) +void MainDialog::cfgHistoryRemoveObsolete(const std::vector& filePaths) { auto getUnavailableCfgFilesAsync = [filePaths] //don't use wxString: NOT thread-safe! (e.g. non-atomic ref-count) { @@ -2822,33 +2698,21 @@ void MainDialog::removeObsoleteCfgHistoryItems(const std::vector& fileP //potentially slow network access => limit maximum wait time! wait_for_all_timed(availableFiles.begin(), availableFiles.end(), std::chrono::milliseconds(1000)); - std::vector filePathsForRemoval; + std::vector pathsToRemove; auto itFut = availableFiles.begin(); for (auto it = filePaths.begin(); it != filePaths.end(); ++it, ++itFut) if (isReady(*itFut) && !itFut->get()) //remove only files that are confirmed to be non-existent - filePathsForRemoval.push_back(*it); //file access error? probably not accessible network share or usb stick => remove cfg + pathsToRemove.push_back(*it); //file access error? probably not accessible network share or usb stick => remove cfg - return filePathsForRemoval; + return pathsToRemove; }; - guiQueue_.processAsync(getUnavailableCfgFilesAsync, [this](const std::vector& files) { removeCfgHistoryItems(files); }); -} - - -void MainDialog::removeCfgHistoryItems(const std::vector& filePaths) -{ - for (const Zstring& filepath : filePaths) + guiQueue_.processAsync(getUnavailableCfgFilesAsync, [this](const std::vector& filePaths2) { - const int histSize = m_listBoxHistory->GetCount(); - for (int i = 0; i < histSize; ++i) - if (auto histData = dynamic_cast(m_listBoxHistory->GetClientObject(i))) - if (equalFilePath(filepath, histData->cfgFile_)) - { - m_listBoxHistory->Delete(i); - break; - } - } + cfggrid::getDataView(*m_gridCfgHistory).removeItems(filePaths2); + m_gridCfgHistory->Refresh(); + }); } @@ -2856,7 +2720,7 @@ void MainDialog::updateUnsavedCfgStatus() { const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); - const bool haveUnsavedCfg = lastConfigurationSaved_ != getConfig(); + const bool haveUnsavedCfg = lastSavedCfg_ != getConfig(); //update save config button const bool allowSave = haveUnsavedCfg || @@ -2952,7 +2816,7 @@ bool MainDialog::trySaveConfig(const Zstring* guiFilename) //return true if save { Zstring defaultFileName = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstr("SyncSettings.ffs_gui"); //attention: activeConfigFiles may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - if (endsWith(defaultFileName, Zstr(".ffs_batch"))) + if (endsWith(defaultFileName, Zstr(".ffs_batch"), CmpFilePath())) defaultFileName = beforeLast(defaultFileName, Zstr("."), IF_MISSING_RETURN_NONE) + Zstr(".ffs_gui"); wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! @@ -3037,7 +2901,7 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchFileToUpdate) Zstring defaultFileName = !activeCfgFilePath.empty() ? activeCfgFilePath : Zstr("BatchRun.ffs_batch"); //attention: activeConfigFiles may be a *.ffs_gui file! We don't want to overwrite it with a BATCH config! - if (endsWith(defaultFileName, Zstr(".ffs_gui"))) + if (endsWith(defaultFileName, Zstr(".ffs_gui"), CmpFilePath())) defaultFileName = beforeLast(defaultFileName, Zstr("."), IF_MISSING_RETURN_NONE) + Zstr(".ffs_batch"); wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! @@ -3073,7 +2937,7 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchFileToUpdate) bool MainDialog::saveOldConfig() //return false on user abort { - if (lastConfigurationSaved_ != getConfig()) + if (lastSavedCfg_ != getConfig()) { const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); @@ -3125,7 +2989,7 @@ bool MainDialog::saveOldConfig() //return false on user abort } //discard current reference file(s), this ensures next app start will load instead of the original non-modified config selection - setLastUsedConfig(std::vector(), lastConfigurationSaved_); + setLastUsedConfig(std::vector(), lastSavedCfg_); //this seems to make theoretical sense also: the job of this function is to make sure current (volatile) config and reference file name are in sync // => if user does not save cfg, it is not attached to a physical file names anymore! } @@ -3152,6 +3016,7 @@ void MainDialog::OnConfigLoad(wxCommandEvent& event) for (const wxString& path : tmp) filePaths.push_back(utfTo(path)); + assert(!filePaths.empty()); loadConfiguration(filePaths); } } @@ -3159,6 +3024,9 @@ void MainDialog::OnConfigLoad(wxCommandEvent& event) void MainDialog::OnConfigNew(wxCommandEvent& event) { + warn_static("replace by loadConfiguration({});") + warn_static("replace excludeFilter handling below with: cfgGrid context menu /new default configuration/") + if (!saveOldConfig()) //notify user about changed settings return; @@ -3175,86 +3043,70 @@ void MainDialog::OnConfigNew(wxCommandEvent& event) } -void MainDialog::OnLoadFromHistory(wxCommandEvent& event) +void MainDialog::onCfgGridSelection(GridSelectEvent& event) { - wxArrayInt selections; - m_listBoxHistory->GetSelections(selections); + if (event.mouseSelect_ && !event.mouseSelect_->complete) + return; //skip the preliminary "clear range" event for mouse-down! + //the mouse is still captured, so we don't want to show a modal dialog (e.g. save changes?) before mouse-up! + //what if mouse capture is lost? minor glitch: grid selection is empty, but parameter owner is "activeConfigFiles_" in any case - std::vector filepaths; - for (int pos : selections) - if (auto histData = dynamic_cast(m_listBoxHistory->GetClientObject(pos))) - filepaths.push_back(histData->cfgFile_); + std::vector filePaths; + for (size_t row : m_gridCfgHistory->getSelectedRows()) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row)) + filePaths.push_back(cfg->filePath); else assert(false); - - if (!filepaths.empty()) - loadConfiguration(filepaths); - - //user changed m_listBoxHistory selection so it's this method's responsibility to synchronize with activeConfigFiles: - //- if user cancelled saving old config - //- there's an error loading new config - //- filepaths is empty and user tried to unselect the current config - addFileToCfgHistory(activeConfigFiles_); +#if 1 + if (!loadConfiguration(filePaths)) + //user changed m_gridCfgHistory selection so it's this method's responsibility to synchronize with activeConfigFiles: + //- if user cancelled saving old config + //- there's an error loading new config + //- filePaths is empty and user tried to unselect the current config + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); +#endif + warn_static("support the last one??? does NOT support newConfig.mainCfg.globalFilter.excludeFilter!!!") } -void MainDialog::OnLoadFromHistoryDoubleClick(wxCommandEvent& event) +void MainDialog::onCfgGridDoubleClick(GridClickEvent& event) { - wxArrayInt selections; - m_listBoxHistory->GetSelections(selections); - - std::vector filepaths; - for (int pos : selections) - if (auto histData = dynamic_cast(m_listBoxHistory->GetClientObject(pos))) - filepaths.push_back(histData->cfgFile_); - else - assert(false); - - if (!filepaths.empty()) - if (loadConfiguration(filepaths)) - { - //simulate button click on "compare" - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); - if (wxEvtHandler* evtHandler = m_buttonCompare->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call - } - - //synchronize m_listBoxHistory and activeConfigFiles, see OnLoadFromHistory() - addFileToCfgHistory(activeConfigFiles_); + if (!activeConfigFiles_.empty()) + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonCompare->Command(dummy); //simulate click + } } -bool MainDialog::loadConfiguration(const std::vector& filepaths) +bool MainDialog::loadConfiguration(const std::vector& filePaths) { - if (filepaths.empty()) - return true; - if (!saveOldConfig()) return false; //cancelled by user - //load XML - xmlAccess::XmlGuiConfig newGuiCfg; //structure to receive gui settings, already defaulted!! - try - { - //allow reading batch configurations also - std::wstring warningMsg; - xmlAccess::readAnyConfig(filepaths, newGuiCfg, warningMsg); //throw FileError + xmlAccess::XmlGuiConfig newGuiCfg; //contains default values - if (!warningMsg.empty()) + if (!filePaths.empty()) //empty cfg file list means "use default" + try { - showNotificationDialog(this, DialogInfoType::WARNING, PopupDialogCfg().setDetailInstructions(warningMsg)); - setConfig(newGuiCfg, filepaths); - setLastUsedConfig(filepaths, xmlAccess::XmlGuiConfig()); //simulate changed config due to parsing errors + //allow reading batch configurations also + std::wstring warningMsg; + xmlAccess::readAnyConfig(filePaths, newGuiCfg, warningMsg); //throw FileError + + if (!warningMsg.empty()) + { + showNotificationDialog(this, DialogInfoType::WARNING, PopupDialogCfg().setDetailInstructions(warningMsg)); + setConfig(newGuiCfg, filePaths); + setLastUsedConfig(filePaths, xmlAccess::XmlGuiConfig()); //simulate changed config due to parsing errors + return true; + } + } + catch (const FileError& e) + { + showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); return false; } - } - catch (const FileError& e) - { - showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); - return false; - } - setConfig(newGuiCfg, filepaths); + setConfig(newGuiCfg, filePaths); //flashStatusInformation("Configuration loaded"); -> irrelevant!? return true; } @@ -3262,33 +3114,33 @@ bool MainDialog::loadConfiguration(const std::vector& filepaths) void MainDialog::deleteSelectedCfgHistoryItems() { - wxArrayInt tmp; - m_listBoxHistory->GetSelections(tmp); - - std::set selections(tmp.begin(), tmp.end()); //sort ascending! - //delete starting with high positions: - std::for_each(selections.rbegin(), selections.rend(), [&](int pos) { m_listBoxHistory->Delete(pos); }); - - //set active selection on next element to allow "batch-deletion" by holding down DEL key - if (!selections.empty() && !m_listBoxHistory->IsEmpty()) + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + if (!selectedRows.empty()) { - int newSelection = *selections.begin(); - if (newSelection >= static_cast(m_listBoxHistory->GetCount())) - newSelection = m_listBoxHistory->GetCount() - 1; - m_listBoxHistory->SetSelection(newSelection); - } -} + std::vector filePaths; + for (size_t row : selectedRows) + if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row)) + filePaths.push_back(cfg->filePath); + else + assert(false); + cfggrid::getDataView(*m_gridCfgHistory).removeItems(filePaths); + m_gridCfgHistory->Refresh(); //grid size changed => clears selection! -void MainDialog::OnCfgHistoryRightClick(wxMouseEvent& event) -{ - ContextMenu menu; - menu.addItem(_("Remove entry from list") + L"\tDel", [this] { deleteSelectedCfgHistoryItems(); }); - menu.popup(*this); + //set active selection on next element to allow "batch-deletion" by holding down DEL key + if (m_gridCfgHistory->getRowCount() > 0) + { + size_t nextRow = selectedRows.front(); + if (nextRow >= m_gridCfgHistory->getRowCount()) + nextRow = m_gridCfgHistory->getRowCount() - 1; + + m_gridCfgHistory->selectRow(nextRow, GridEventPolicy::DENY_GRID_EVENT); + } + } } -void MainDialog::OnCfgHistoryKeyEvent(wxKeyEvent& event) +void MainDialog::onCfgGridKeyEvent(wxKeyEvent& event) { const int keyCode = event.GetKeyCode(); if (keyCode == WXK_DELETE || @@ -3301,17 +3153,110 @@ void MainDialog::OnCfgHistoryKeyEvent(wxKeyEvent& event) } +void MainDialog::onCfgGridContext(GridClickEvent& event) +{ + ContextMenu menu; + //-------------------------------------------------------------------------------------------------------- + const std::vector selectedRows = m_gridCfgHistory->getSelectedRows(); + + menu.addItem(_("Hide configuration") + L"\tDel", [this] { deleteSelectedCfgHistoryItems(); }, nullptr, !selectedRows.empty()); + //-------------------------------------------------------------------------------------------------------- + menu.popup(*this); + //event.Skip(); +} + + +void MainDialog::onCfgGridLabelContext(GridLabelClickEvent& event) +{ + ContextMenu menu; + //-------------------------------------------------------------------------------------------------------- + auto toggleColumn = [&](ColumnType ct) + { + auto colAttr = m_gridCfgHistory->getColumnConfig(); + + Grid::ColAttributes* caName = nullptr; + Grid::ColAttributes* caToggle = nullptr; + + for (Grid::ColAttributes& ca : colAttr) + if (ca.type == static_cast(ColumnTypeCfg::NAME)) + caName = &ca; + else if (ca.type == ct) + caToggle = &ca; + + assert(caName && caName->stretch > 0 && caName->visible); + assert(caToggle && caToggle->stretch == 0); + + if (caName && caToggle) + { + caToggle->visible = !caToggle->visible; + + //take width of newly visible column from stretched folder name column + caName->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset; + + m_gridCfgHistory->setColumnConfig(colAttr); + } + }; + + if (auto prov = m_gridCfgHistory->getDataProvider()) + for (const Grid::ColAttributes& ca : m_gridCfgHistory->getColumnConfig()) + menu.addCheckBox(prov->getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); }, + ca.visible, ca.type != static_cast(ColumnTypeCfg::NAME)); //do not allow user to hide name column! + else assert(false); + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setDefault = [&] + { + const xmlAccess::XmlGlobalSettings defaultCfg; + m_gridCfgHistory->setColumnConfig(convertColAttributes(defaultCfg.gui.mainDlg.cfgGridColumnAttribs, getCfgGridDefaultColAttribs())); + }; + menu.addItem(_("&Default"), setDefault); //'&' -> reuse text from "default" buttons elsewhere + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setCfgHighlight = [&] + { + int cfgGridSyncOverdueDays = cfggrid::getSyncOverdueDays(*m_gridCfgHistory); + + if (showCfgHighlightDlg(this, cfgGridSyncOverdueDays) == ReturnSmallDlg::BUTTON_OKAY) + cfggrid::setSyncOverdueDays(*m_gridCfgHistory, cfgGridSyncOverdueDays); + }; + menu.addItem(_("Highlight..."), setCfgHighlight); + //-------------------------------------------------------------------------------------------------------- + + menu.popup(*m_gridCfgHistory); + //event.Skip(); +} + + +void MainDialog::onCfgGridLabelLeftClick(GridLabelClickEvent& event) +{ + const auto colType = static_cast(event.colType_); + bool sortAscending = getDefaultSortDirection(colType); + + const auto sortInfo = cfggrid::getDataView(*m_gridCfgHistory).getSortDirection(); + if (sortInfo.first == colType) + sortAscending = !sortInfo.second; + + cfggrid::getDataView(*m_gridCfgHistory).setSortDirection(colType, sortAscending); + m_gridCfgHistory->Refresh(); + + //re-apply selection: + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); +} + + void MainDialog::onCheckRows(CheckRowsEvent& event) { std::vector selectedRows; - const size_t rowLast = std::min(event.rowLast_, gridDataView_->rowsOnView()); //consider dummy rows + const size_t rowLast = std::min(event.rowLast_, filegrid::getDataView(*m_gridMainC).rowsOnView()); //consider dummy rows for (size_t i = event.rowFirst_; i < rowLast; ++i) selectedRows.push_back(i); if (!selectedRows.empty()) { - std::vector objects = gridDataView_->getAllFileRef(selectedRows); + std::vector objects = filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows); setFilterManually(objects, event.setIncluded_); } } @@ -3321,13 +3266,13 @@ void MainDialog::onSetSyncDirection(SyncDirectionEvent& event) { std::vector selectedRows; - const size_t rowLast = std::min(event.rowLast_, gridDataView_->rowsOnView()); //consider dummy rows + const size_t rowLast = std::min(event.rowLast_, filegrid::getDataView(*m_gridMainC).rowsOnView()); //consider dummy rows for (size_t i = event.rowFirst_; i < rowLast; ++i) selectedRows.push_back(i); if (!selectedRows.empty()) { - std::vector objects = gridDataView_->getAllFileRef(selectedRows); + std::vector objects = filegrid::getDataView(*m_gridMainC).getAllFileRef(selectedRows); setSyncDirManually(objects, event.direction_); } } @@ -3337,9 +3282,9 @@ void MainDialog::setLastUsedConfig(const std::vector& cfgFilePaths, const xmlAccess::XmlGuiConfig& guiConfig) { activeConfigFiles_ = cfgFilePaths; - lastConfigurationSaved_ = guiConfig; + lastSavedCfg_ = guiConfig; - addFileToCfgHistory(activeConfigFiles_); //put filepath on list of last used config files + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, true /*scrollToSelection*/); //put filepath on list of last used config files updateUnsavedCfgStatus(); } @@ -3400,7 +3345,7 @@ void MainDialog::updateGuiDelayedIf(bool condition) if (condition) { - gridview::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); + filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); m_gridMainL->Update(); m_gridMainC->Update(); m_gridMainR->Update(); @@ -3467,7 +3412,7 @@ void MainDialog::showConfigDialog(SyncConfigPanel panelToShow, int localPairInde currentCfg_.mainCfg.postSyncCommand, currentCfg_.mainCfg.postSyncCondition, globalCfg_.gui.commandHistory, - globalCfg_.gui.commandHistoryMax) == ReturnSyncConfig::BUTTON_OKAY) + globalCfg_.gui.commandHistItemsMax) == ReturnSyncConfig::BUTTON_OKAY) { assert(folderPairConfig.size() == folderPairConfigOld.size()); @@ -3780,8 +3725,8 @@ void MainDialog::OnCompare(wxCommandEvent& event) return; } - gridDataView_->setData(folderCmp_); //update view on data - treeDataView_->setData(folderCmp_); // + filegrid::getDataView(*m_gridMainC).setData(folderCmp_); //update view on data + treegrid::getDataView(*m_gridOverview).setData(folderCmp_); // updateGui(); m_gridMainL->clearSelection(ALLOW_GRID_EVENT); @@ -3844,8 +3789,8 @@ void MainDialog::clearGrid(ptrdiff_t pos) folderCmp_.erase(folderCmp_.begin() + pos); } - gridDataView_->setData(folderCmp_); - treeDataView_->setData(folderCmp_); + filegrid::getDataView(*m_gridMainC).setData(folderCmp_); + treegrid::getDataView(*m_gridOverview).setData(folderCmp_); updateGui(); } @@ -3912,9 +3857,8 @@ void MainDialog::OnStartSync(wxCommandEvent& event) if (folderCmp_.empty()) { //quick sync: simulate button click on "compare" - wxCommandEvent dummy2(wxEVT_COMMAND_BUTTON_CLICKED); - if (wxEvtHandler* evtHandler = m_buttonCompare->GetEventHandler()) - evtHandler->ProcessEvent(dummy2); //synchronous call + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_buttonCompare->Command(dummy); //simulate click if (folderCmp_.empty()) //check if user aborted or error occurred, ect... return; @@ -3999,11 +3943,21 @@ void MainDialog::OnStartSync(wxCommandEvent& event) folderCmp_, globalCfg_.optDialogs, statusHandler); + + //not cancelled? => update last sync date for selected cfg files + std::vector> lastSyncTimes; + for (const Zstring& cfgPath : activeConfigFiles_) + lastSyncTimes.emplace_back(cfgPath, std::time(nullptr)); + + cfggrid::getDataView(*m_gridCfgHistory).setLastSyncTime(lastSyncTimes); + //re-apply selection: sort order changed if sorted by last sync time + cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/); + //m_gridCfgHistory->Refresh(); <- implicit in last call } catch (AbortProcess&) {} //remove empty rows: just a beautification, invalid rows shouldn't cause issues - gridDataView_->removeInvalidRows(); + filegrid::getDataView(*m_gridMainC).removeInvalidRows(); updateGui(); @@ -4030,7 +3984,7 @@ void MainDialog::onGridDoubleClickRim(size_t row, bool leftSide) { std::vector selectionLeft; std::vector selectionRight; - if (FileSystemObject* fsObj = gridDataView_->getObject(row)) //selection must be a list of BOUND pointers! + if (FileSystemObject* fsObj = filegrid::getDataView(*m_gridMainC).getObject(row)) //selection must be a list of BOUND pointers! (leftSide ? selectionLeft : selectionRight) = { fsObj }; openExternalApplication(globalCfg_.gui.externelApplications[0].second, leftSide, selectionLeft, selectionRight); @@ -4040,15 +3994,15 @@ void MainDialog::onGridDoubleClickRim(size_t row, bool leftSide) void MainDialog::onGridLabelLeftClick(bool onLeft, ColumnTypeRim type) { - auto sortInfo = gridDataView_->getSortInfo(); + auto sortInfo = filegrid::getDataView(*m_gridMainC).getSortInfo(); - bool sortAscending = GridView::getDefaultSortDirection(type); - if (sortInfo && sortInfo->onLeft_ == onLeft && sortInfo->type_ == type) - sortAscending = !sortInfo->ascending_; + bool sortAscending = getDefaultSortDirection(type); + if (sortInfo && sortInfo->onLeft == onLeft && sortInfo->type == type) + sortAscending = !sortInfo->ascending; const ItemPathFormat itemPathFormat = onLeft ? globalCfg_.gui.mainDlg.itemPathFormatLeftGrid : globalCfg_.gui.mainDlg.itemPathFormatRightGrid; - gridDataView_->sortView(type, itemPathFormat, onLeft, sortAscending); + filegrid::getDataView(*m_gridMainC).sortView(type, itemPathFormat, onLeft, sortAscending); m_gridMainL->clearSelection(ALLOW_GRID_EVENT); m_gridMainC->clearSelection(ALLOW_GRID_EVENT); @@ -4143,16 +4097,16 @@ void MainDialog::updateGridViewData() if (m_bpButtonViewTypeSyncAction->isActive()) { - const GridView::StatusSyncPreview result = gridDataView_->updateSyncPreview(m_bpButtonShowExcluded ->isActive(), - m_bpButtonShowCreateLeft ->isActive(), - m_bpButtonShowCreateRight->isActive(), - m_bpButtonShowDeleteLeft ->isActive(), - m_bpButtonShowDeleteRight->isActive(), - m_bpButtonShowUpdateLeft ->isActive(), - m_bpButtonShowUpdateRight->isActive(), - m_bpButtonShowDoNothing ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); + const FileView::StatusSyncPreview result = filegrid::getDataView(*m_gridMainC).updateSyncPreview(m_bpButtonShowExcluded ->isActive(), + m_bpButtonShowCreateLeft ->isActive(), + m_bpButtonShowCreateRight->isActive(), + m_bpButtonShowDeleteLeft ->isActive(), + m_bpButtonShowDeleteRight->isActive(), + m_bpButtonShowUpdateLeft ->isActive(), + m_bpButtonShowUpdateRight->isActive(), + m_bpButtonShowDoNothing ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); filesOnLeftView = result.filesOnLeftView; foldersOnLeftView = result.foldersOnLeftView; filesOnRightView = result.filesOnRightView; @@ -4181,14 +4135,14 @@ void MainDialog::updateGridViewData() } else { - const GridView::StatusCmpResult result = gridDataView_->updateCmpResult(m_bpButtonShowExcluded ->isActive(), - m_bpButtonShowLeftOnly ->isActive(), - m_bpButtonShowRightOnly ->isActive(), - m_bpButtonShowLeftNewer ->isActive(), - m_bpButtonShowRightNewer->isActive(), - m_bpButtonShowDifferent ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); + const FileView::StatusCmpResult result = filegrid::getDataView(*m_gridMainC).updateCmpResult(m_bpButtonShowExcluded ->isActive(), + m_bpButtonShowLeftOnly ->isActive(), + m_bpButtonShowRightOnly ->isActive(), + m_bpButtonShowLeftNewer ->isActive(), + m_bpButtonShowRightNewer->isActive(), + m_bpButtonShowDifferent ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); filesOnLeftView = result.filesOnLeftView; foldersOnLeftView = result.foldersOnLeftView; filesOnRightView = result.filesOnRightView; @@ -4242,29 +4196,29 @@ void MainDialog::updateGridViewData() m_panelViewFilter->Layout(); //all three grids retrieve their data directly via gridDataView - gridview::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); + filegrid::refresh(*m_gridMainL, *m_gridMainC, *m_gridMainR); - //navigation tree + //overview panel if (m_bpButtonViewTypeSyncAction->isActive()) - treeDataView_->updateSyncPreview(m_bpButtonShowExcluded ->isActive(), - m_bpButtonShowCreateLeft ->isActive(), - m_bpButtonShowCreateRight->isActive(), - m_bpButtonShowDeleteLeft ->isActive(), - m_bpButtonShowDeleteRight->isActive(), - m_bpButtonShowUpdateLeft ->isActive(), - m_bpButtonShowUpdateRight->isActive(), - m_bpButtonShowDoNothing ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); + treegrid::getDataView(*m_gridOverview).updateSyncPreview(m_bpButtonShowExcluded ->isActive(), + m_bpButtonShowCreateLeft ->isActive(), + m_bpButtonShowCreateRight->isActive(), + m_bpButtonShowDeleteLeft ->isActive(), + m_bpButtonShowDeleteRight->isActive(), + m_bpButtonShowUpdateLeft ->isActive(), + m_bpButtonShowUpdateRight->isActive(), + m_bpButtonShowDoNothing ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); else - treeDataView_->updateCmpResult(m_bpButtonShowExcluded ->isActive(), - m_bpButtonShowLeftOnly ->isActive(), - m_bpButtonShowRightOnly ->isActive(), - m_bpButtonShowLeftNewer ->isActive(), - m_bpButtonShowRightNewer->isActive(), - m_bpButtonShowDifferent ->isActive(), - m_bpButtonShowEqual ->isActive(), - m_bpButtonShowConflict ->isActive()); + treegrid::getDataView(*m_gridOverview).updateCmpResult(m_bpButtonShowExcluded ->isActive(), + m_bpButtonShowLeftOnly ->isActive(), + m_bpButtonShowRightOnly ->isActive(), + m_bpButtonShowLeftNewer ->isActive(), + m_bpButtonShowRightNewer->isActive(), + m_bpButtonShowDifferent ->isActive(), + m_bpButtonShowEqual ->isActive(), + m_bpButtonShowConflict ->isActive()); m_gridOverview->Refresh(); //update status bar information @@ -4387,7 +4341,7 @@ void MainDialog::startFindNext(bool searchAscending) //F3 or ENTER in m_textCtrl { assert(result.second >= 0); - gridview::setScrollMaster(*grid); + filegrid::setScrollMaster(*grid); grid->setGridCursor(result.second); focusWindowAfterSearch_ = &grid->getMainWin(); @@ -4682,8 +4636,8 @@ void MainDialog::moveAddFolderPairUp(size_t pos) if (!folderCmp_.empty()) std::swap(folderCmp_[pos], folderCmp_[pos + 1]); //invariant: folderCmp is empty or matches number of all folder pairs - gridDataView_->setData(folderCmp_); - treeDataView_->setData(folderCmp_); + filegrid::getDataView(*m_gridMainC ).setData(folderCmp_); + treegrid::getDataView(*m_gridOverview).setData(folderCmp_); updateGui(); } } @@ -4784,31 +4738,31 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) auto colAttrCenter = m_gridMainC->getColumnConfig(); auto colAttrRight = m_gridMainR->getColumnConfig(); - erase_if(colAttrLeft, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); - erase_if(colAttrCenter, [](const Grid::ColumnAttribute& ca) { return !ca.visible_ || static_cast(ca.type_) == ColumnTypeCenter::CHECKBOX; }); - erase_if(colAttrRight, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); + erase_if(colAttrLeft, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + erase_if(colAttrCenter, [](const Grid::ColAttributes& ca) { return !ca.visible || static_cast(ca.type) == ColumnTypeCenter::CHECKBOX; }); + erase_if(colAttrRight, [](const Grid::ColAttributes& ca) { return !ca.visible; }); if (provLeft && provCenter && provRight) { - for (const Grid::ColumnAttribute& ca : colAttrLeft) + for (const Grid::ColAttributes& ca : colAttrLeft) { - header += fmtValue(provLeft->getColumnLabel(ca.type_)); + header += fmtValue(provLeft->getColumnLabel(ca.type)); header += CSV_SEP; } - for (const Grid::ColumnAttribute& ca : colAttrCenter) + for (const Grid::ColAttributes& ca : colAttrCenter) { - header += fmtValue(provCenter->getColumnLabel(ca.type_)); + header += fmtValue(provCenter->getColumnLabel(ca.type)); header += CSV_SEP; } if (!colAttrRight.empty()) { std::for_each(colAttrRight.begin(), colAttrRight.end() - 1, - [&](const Grid::ColumnAttribute& ca) + [&](const Grid::ColAttributes& ca) { - header += fmtValue(provRight->getColumnLabel(ca.type_)); + header += fmtValue(provRight->getColumnLabel(ca.type)); header += CSV_SEP; }); - header += fmtValue(provRight->getColumnLabel(colAttrRight.back().type_)); + header += fmtValue(provRight->getColumnLabel(colAttrRight.back().type)); } header += LINE_BREAK; @@ -4828,21 +4782,21 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) const size_t rowCount = m_gridMainL->getRowCount(); for (size_t row = 0; row < rowCount; ++row) { - for (const Grid::ColumnAttribute& ca : colAttrLeft) + for (const Grid::ColAttributes& ca : colAttrLeft) { - buffer += fmtValue(provLeft->getValue(row, ca.type_)); + buffer += fmtValue(provLeft->getValue(row, ca.type)); buffer += CSV_SEP; } - for (const Grid::ColumnAttribute& ca : colAttrCenter) + for (const Grid::ColAttributes& ca : colAttrCenter) { - buffer += fmtValue(provCenter->getValue(row, ca.type_)); + buffer += fmtValue(provCenter->getValue(row, ca.type)); buffer += CSV_SEP; } - for (const Grid::ColumnAttribute& ca : colAttrRight) + for (const Grid::ColAttributes& ca : colAttrRight) { - buffer += fmtValue(provRight->getValue(row, ca.type_)); + buffer += fmtValue(provRight->getValue(row, ca.type)); buffer += CSV_SEP; } buffer += LINE_BREAK; @@ -4951,7 +4905,7 @@ void MainDialog::switchProgramLanguage(wxLanguage langId) newGlobalCfg.programLanguage = langId; //show new dialog, then delete old one - MainDialog::create(globalConfigFile_, &newGlobalCfg, getConfig(), activeConfigFiles_, false); + MainDialog::create(globalConfigFilePath_, &newGlobalCfg, getConfig(), activeConfigFiles_, false); //we don't use Close(): //1. we don't want to show the prompt to save current config in OnClose() @@ -4970,7 +4924,7 @@ void MainDialog::setViewTypeSyncAction(bool value) m_bpButtonViewTypeSyncAction->SetToolTip((value ? _("Action") : _("Category")) + L" (F10)"); //toggle display of sync preview in middle grid - gridview::highlightSyncAction(*m_gridMainC, value); + filegrid::highlightSyncAction(*m_gridMainC, value); updateGui(); } diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h index 045e94f1..ffc0ec52 100755 --- a/FreeFileSync/Source/ui/main_dlg.h +++ b/FreeFileSync/Source/ui/main_dlg.h @@ -11,16 +11,14 @@ #include #include #include -//#include #include #include #include #include "gui_generated.h" -#include "custom_grid.h" +#include "file_grid.h" +#include "tree_grid.h" #include "sync_cfg.h" -#include "tree_view.h" #include "folder_history_box.h" -//#include "../lib/process_xml.h" #include "../algorithm.h" class FolderPairFirst; @@ -32,12 +30,12 @@ class MainDialog : public MainDialogGenerated { public: //default behavior, application start, restores last used config - static void create(const Zstring& globalConfigFile); + static void create(const Zstring& globalConfigFilePath); //when loading dynamically assembled config, //when switching language, //or switching from batch run to GUI on warnings - static void create(const Zstring& globalConfigFile, + static void create(const Zstring& globalConfigFilePath, const xmlAccess::XmlGlobalSettings* globalSettings, //optional: take over ownership => save on exit const xmlAccess::XmlGuiConfig& guiCfg, const std::vector& referenceFiles, @@ -49,7 +47,7 @@ public: void onQueryEndSession(); //last chance to do something useful before killing the application! private: - MainDialog(const Zstring& globalConfigFile, + MainDialog(const Zstring& globalConfigFilePath, const xmlAccess::XmlGuiConfig& guiCfg, const std::vector& referenceFiles, const xmlAccess::XmlGlobalSettings& globalSettings, //take over ownership => save on exit @@ -87,9 +85,7 @@ private: void initViewFilterButtons(); void setViewFilterDefault(); - void addFileToCfgHistory(const std::vector& filepaths); //= update/insert + apply selection - void removeObsoleteCfgHistoryItems(const std::vector& filepaths); - void removeCfgHistoryItems(const std::vector& filepaths); + void cfgHistoryRemoveObsolete(const std::vector& filepaths); void insertAddFolderPair(const std::vector& newPairs, size_t pos); void moveAddFolderPairUp(size_t pos); @@ -162,9 +158,9 @@ private: void onMainGridContextR(zen::GridClickEvent& event); void onMainGridContextRim(bool leftSide); - void onNaviGridContext(zen::GridClickEvent& event); + void onTreeGridContext(zen::GridClickEvent& event); - void onNaviSelection(zen::GridRangeSelectEvent& event); + void onTreeGridSelection(zen::GridSelectEvent& event); void onDialogFilesDropped(zen::FileDropEvent& event); @@ -196,13 +192,16 @@ private: void OnConfigSaveAs (wxCommandEvent& event) override; void OnSaveAsBatchJob (wxCommandEvent& event) override; void OnConfigLoad (wxCommandEvent& event) override; - void OnLoadFromHistory(wxCommandEvent& event) override; - void OnLoadFromHistoryDoubleClick(wxCommandEvent& event) override; + + void onCfgGridSelection (zen::GridSelectEvent& event); + void onCfgGridDoubleClick(zen::GridClickEvent& event); + void onCfgGridKeyEvent (wxKeyEvent& event); + void onCfgGridContext (zen::GridClickEvent& event); + void onCfgGridLabelContext (zen::GridLabelClickEvent& event); + void onCfgGridLabelLeftClick(zen::GridLabelClickEvent& event); void deleteSelectedCfgHistoryItems(); - void OnCfgHistoryRightClick(wxMouseEvent& event) override; - void OnCfgHistoryKeyEvent (wxKeyEvent& event) override; void OnRegularUpdateCheck (wxIdleEvent& event); void OnLayoutWindowAsync (wxIdleEvent& event); @@ -281,25 +280,20 @@ private: //global settings shared by GUI and batch mode xmlAccess::XmlGlobalSettings globalCfg_; - const Zstring globalConfigFile_; + const Zstring globalConfigFilePath_; //------------------------------------- //program configuration xmlAccess::XmlGuiConfig currentCfg_; //used when saving configuration - std::vector activeConfigFiles_; //name of currently loaded config file (may be more than 1) + std::vector activeConfigFiles_; //name of currently loaded config files: NOT owned by m_gridCfgHistory, see onCfgGridSelection() - xmlAccess::XmlGuiConfig lastConfigurationSaved_; //support for: "Save changed configuration?" dialog + xmlAccess::XmlGuiConfig lastSavedCfg_; //support for: "Save changed configuration?" dialog - static Zstring getLastRunConfigPath(); const Zstring lastRunConfigPath_; //let's not use another static... //------------------------------------- - //UI view of FolderComparison structure (partially owns folderCmp) - std::shared_ptr gridDataView_; //always bound! - std::shared_ptr treeDataView_; // - //the prime data structure of this tool *bling*: zen::FolderComparison folderCmp_; //optional!: sync button not available if empty diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp index 3738b3ce..bd97f8d5 100755 --- a/FreeFileSync/Source/ui/progress_indicator.cpp +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -595,50 +595,51 @@ private: //----------------------------------------------------------------------------- -enum ColumnTypeMsg +enum class ColumnTypeMsg { - COL_TYPE_MSG_TIME, - COL_TYPE_MSG_CATEGORY, - COL_TYPE_MSG_TEXT, + TIME, + CATEGORY, + TEXT, }; //Grid data implementation referencing MessageView class GridDataMessages : public GridData { public: - GridDataMessages(const std::shared_ptr& msgView) : msgView_(msgView) {} + GridDataMessages(const ErrorLog& log) : msgView_(log) {} - size_t getRowCount() const override { return msgView_ ? msgView_->rowsOnView() : 0; } + MessageView& getDataView() { return msgView_; } + + size_t getRowCount() const override { return msgView_.rowsOnView(); } std::wstring getValue(size_t row, ColumnType colType) const override { - if (msgView_) - if (Opt entry = msgView_->getEntry(row)) - switch (static_cast(colType)) - { - case COL_TYPE_MSG_TIME: - if (entry->firstLine) - return formatTime(FORMAT_TIME, getLocalTime(entry->time)); - break; + if (Opt entry = msgView_.getEntry(row)) + switch (static_cast(colType)) + { + case ColumnTypeMsg::TIME: + if (entry->firstLine) + return formatTime(FORMAT_TIME, getLocalTime(entry->time)); + break; - case COL_TYPE_MSG_CATEGORY: - if (entry->firstLine) - switch (entry->type) - { - case TYPE_INFO: - return _("Info"); - case TYPE_WARNING: - return _("Warning"); - case TYPE_ERROR: - return _("Error"); - case TYPE_FATAL_ERROR: - return _("Serious Error"); - } - break; + case ColumnTypeMsg::CATEGORY: + if (entry->firstLine) + switch (entry->type) + { + case TYPE_INFO: + return _("Info"); + case TYPE_WARNING: + return _("Warning"); + case TYPE_ERROR: + return _("Error"); + case TYPE_FATAL_ERROR: + return _("Serious Error"); + } + break; - case COL_TYPE_MSG_TEXT: - return copyStringTo(entry->messageLine); - } + case ColumnTypeMsg::TEXT: + return copyStringTo(entry->messageLine); + } return std::wstring(); } @@ -647,73 +648,71 @@ public: wxRect rectTmp = rect; //-------------- draw item separation line ----------------- - wxDCPenChanger dummy2(dc, getColorGridLine()); - const bool drawBottomLine = [&] //don't separate multi-line messages { - if (msgView_) - if (Opt nextEntry = msgView_->getEntry(row + 1)) + wxDCPenChanger dummy2(dc, getColorGridLine()); + const bool drawBottomLine = [&] //don't separate multi-line messages + { + if (Opt nextEntry = msgView_.getEntry(row + 1)) return nextEntry->firstLine; - return true; - }(); + return true; + }(); - if (drawBottomLine) - { - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); - --rectTmp.height; + if (drawBottomLine) + { + dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); + --rectTmp.height; + } } - //-------------------------------------------------------- - if (msgView_) - if (Opt entry = msgView_->getEntry(row)) - switch (static_cast(colType)) - { - case COL_TYPE_MSG_TIME: - drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER); - break; + if (Opt entry = msgView_.getEntry(row)) + switch (static_cast(colType)) + { + case ColumnTypeMsg::TIME: + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER); + break; - case COL_TYPE_MSG_CATEGORY: - if (entry->firstLine) - switch (entry->type) - { - case TYPE_INFO: - drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_info_small"), rectTmp, wxALIGN_CENTER); - break; - case TYPE_WARNING: - drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_warning_small"), rectTmp, wxALIGN_CENTER); - break; - case TYPE_ERROR: - case TYPE_FATAL_ERROR: - drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_error_small"), rectTmp, wxALIGN_CENTER); - break; - } - break; + case ColumnTypeMsg::CATEGORY: + if (entry->firstLine) + switch (entry->type) + { + case TYPE_INFO: + drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_info_small"), rectTmp, wxALIGN_CENTER); + break; + case TYPE_WARNING: + drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_warning_small"), rectTmp, wxALIGN_CENTER); + break; + case TYPE_ERROR: + case TYPE_FATAL_ERROR: + drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_error_small"), rectTmp, wxALIGN_CENTER); + break; + } + break; - case COL_TYPE_MSG_TEXT: - rectTmp.x += COLUMN_GAP_LEFT; - rectTmp.width -= COLUMN_GAP_LEFT; - drawCellText(dc, rectTmp, getValue(row, colType)); - break; - } + case ColumnTypeMsg::TEXT: + rectTmp.x += COLUMN_GAP_LEFT; + rectTmp.width -= COLUMN_GAP_LEFT; + drawCellText(dc, rectTmp, getValue(row, colType)); + break; + } } int getBestSize(wxDC& dc, size_t row, ColumnType colType) override { // -> synchronize renderCell() <-> getBestSize() - if (msgView_) - if (msgView_->getEntry(row)) - switch (static_cast(colType)) - { - case COL_TYPE_MSG_TIME: - return 2 * COLUMN_GAP_LEFT + dc.GetTextExtent(getValue(row, colType)).GetWidth(); + if (msgView_.getEntry(row)) + switch (static_cast(colType)) + { + case ColumnTypeMsg::TIME: + return 2 * COLUMN_GAP_LEFT + dc.GetTextExtent(getValue(row, colType)).GetWidth(); - case COL_TYPE_MSG_CATEGORY: - return getResourceImage(L"msg_info_small").GetWidth(); + case ColumnTypeMsg::CATEGORY: + return getResourceImage(L"msg_info_small").GetWidth(); - case COL_TYPE_MSG_TEXT: - return COLUMN_GAP_LEFT + dc.GetTextExtent(getValue(row, colType)).GetWidth(); - } + case ColumnTypeMsg::TEXT: + return COLUMN_GAP_LEFT + dc.GetTextExtent(getValue(row, colType)).GetWidth(); + } return 0; } @@ -738,11 +737,11 @@ public: { switch (static_cast(colType)) { - case COL_TYPE_MSG_TIME: - case COL_TYPE_MSG_TEXT: + case ColumnTypeMsg::TIME: + case ColumnTypeMsg::TEXT: break; - case COL_TYPE_MSG_CATEGORY: + case ColumnTypeMsg::CATEGORY: return getValue(row, colType); } return std::wstring(); @@ -751,7 +750,7 @@ public: std::wstring getColumnLabel(ColumnType colType) const override { return std::wstring(); } private: - const std::shared_ptr msgView_; + MessageView msgView_; }; } @@ -759,7 +758,7 @@ private: class LogPanel : public LogPanelGenerated { public: - LogPanel(wxWindow* parent, const ErrorLog& log) : LogPanelGenerated(parent), msgView_(std::make_shared(log)) + LogPanel(wxWindow* parent, const ErrorLog& log) : LogPanelGenerated(parent) { const int errorCount = log.getItemCount(TYPE_ERROR | TYPE_FATAL_ERROR); const int warningCount = log.getItemCount(TYPE_WARNING); @@ -788,15 +787,15 @@ public: const int colMsgTimeWidth = GridDataMessages::getColumnTimeDefaultWidth(*m_gridMessages); const int colMsgCategoryWidth = GridDataMessages::getColumnCategoryDefaultWidth(); - m_gridMessages->setDataProvider(std::make_shared(msgView_)); + m_gridMessages->setDataProvider(std::make_shared(log)); m_gridMessages->setColumnLabelHeight(0); m_gridMessages->showRowLabel(false); m_gridMessages->setRowHeight(rowHeight); m_gridMessages->setColumnConfig( { - { static_cast(COL_TYPE_MSG_TIME ), colMsgTimeWidth, 0 }, - { static_cast(COL_TYPE_MSG_CATEGORY), colMsgCategoryWidth, 0 }, - { static_cast(COL_TYPE_MSG_TEXT ), -colMsgTimeWidth - colMsgCategoryWidth, 1 }, + { static_cast(ColumnTypeMsg::TIME ), colMsgTimeWidth, 0, true }, + { static_cast(ColumnTypeMsg::CATEGORY), colMsgCategoryWidth, 0, true }, + { static_cast(ColumnTypeMsg::TEXT ), -colMsgTimeWidth - colMsgCategoryWidth, 1, true }, }); //support for CTRL + C @@ -811,6 +810,14 @@ public: } private: + MessageView& getDataView() + { + if (auto* prov = dynamic_cast(m_gridMessages->getDataProvider())) + return prov->getDataView(); + + throw std::runtime_error("m_gridMessages was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); + } + void OnErrors(wxCommandEvent& event) override { m_bpButtonErrors->toggle(); @@ -841,7 +848,7 @@ private: if (m_bpButtonInfo->isActive()) includedTypes |= TYPE_INFO; - msgView_->updateView(includedTypes); //update MVC "model" + getDataView().updateView(includedTypes); //update MVC "model" m_gridMessages->Refresh(); //update MVC "view" } @@ -960,18 +967,18 @@ private: if (auto prov = m_gridMessages->getDataProvider()) { - std::vector colAttr = m_gridMessages->getColumnConfig(); - erase_if(colAttr, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); + std::vector colAttr = m_gridMessages->getColumnConfig(); + erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); if (!colAttr.empty()) for (size_t row : m_gridMessages->getSelectedRows()) { std::for_each(colAttr.begin(), --colAttr.end(), - [&](const Grid::ColumnAttribute& ca) + [&](const Grid::ColAttributes& ca) { - clipboardString += copyStringTo(prov->getValue(row, ca.type_)); + clipboardString += copyStringTo(prov->getValue(row, ca.type)); clipboardString += L'\t'; }); - clipboardString += copyStringTo(prov->getValue(row, colAttr.back().type_)); + clipboardString += copyStringTo(prov->getValue(row, colAttr.back().type)); clipboardString += L'\n'; } } @@ -990,7 +997,6 @@ private: } } - std::shared_ptr msgView_; //bound! bool processingKeyEventHandler_ = false; }; @@ -1274,8 +1280,8 @@ public: } private: - void OnKeyPressed (wxKeyEvent& event); - void OnKeyPressedParent(wxKeyEvent& event); + void onLocalKeyEvent (wxKeyEvent& event); + void onParentKeyEvent(wxKeyEvent& event); void OnOkay (wxCommandEvent& event); void OnPause (wxCommandEvent& event); void OnCancel (wxCommandEvent& event); @@ -1366,7 +1372,7 @@ SyncProgressDialogImpl::SyncProgressDialogImpl(long style, //wxF //lifetime of event sources is subset of this instance's lifetime => no wxEvtHandler::Disconnect() needed this->Connect(wxEVT_CLOSE_WINDOW, wxCloseEventHandler (SyncProgressDialogImpl::OnClose)); this->Connect(wxEVT_ICONIZE, wxIconizeEventHandler(SyncProgressDialogImpl::OnIconize)); - this->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::OnKeyPressed), nullptr, this); + this->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::onLocalKeyEvent), nullptr, this); pnl_.m_buttonClose->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(SyncProgressDialogImpl::OnOkay ), NULL, this); pnl_.m_buttonPause->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(SyncProgressDialogImpl::OnPause ), NULL, this); pnl_.m_buttonStop ->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(SyncProgressDialogImpl::OnCancel), NULL, this); @@ -1374,7 +1380,7 @@ SyncProgressDialogImpl::SyncProgressDialogImpl(long style, //wxF pnl_.m_checkBoxIgnoreErrors->Connect(wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler(SyncProgressDialogImpl::OnToggleIgnoreErrors), NULL, this); if (parentFrame_) - parentFrame_->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::OnKeyPressedParent), nullptr, this); + parentFrame_->Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::onParentKeyEvent), nullptr, this); assert(pnl_.m_buttonClose->GetId() == wxID_OK); //we cannot use wxID_CLOSE else Esc key won't work: yet another wxWidgets bug?? @@ -1481,7 +1487,7 @@ SyncProgressDialogImpl::~SyncProgressDialogImpl() { if (parentFrame_) { - parentFrame_->Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::OnKeyPressedParent), nullptr, this); + parentFrame_->Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::onParentKeyEvent), nullptr, this); parentFrame_->SetTitle(parentTitleBackup_); //restore title text @@ -1496,26 +1502,19 @@ SyncProgressDialogImpl::~SyncProgressDialogImpl() template -void SyncProgressDialogImpl::OnKeyPressed(wxKeyEvent& event) +void SyncProgressDialogImpl::onLocalKeyEvent(wxKeyEvent& event) { - const int keyCode = event.GetKeyCode(); - if (keyCode == WXK_ESCAPE) + switch (event.GetKeyCode()) { - wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); - - //simulate click on abort button - if (pnl_.m_buttonStop->IsShown()) //delegate to "cancel" button if available - { - if (wxEvtHandler* handler = pnl_.m_buttonStop->GetEventHandler()) - handler->ProcessEvent(dummy); - return; - } - else if (pnl_.m_buttonClose->IsShown()) + case WXK_ESCAPE: { - if (wxEvtHandler* handler = pnl_.m_buttonClose->GetEventHandler()) - handler->ProcessEvent(dummy); + wxButton& activeButton = pnl_.m_buttonStop->IsShown() ? *pnl_.m_buttonStop : *pnl_.m_buttonClose; + + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + activeButton.Command(dummy); //simulate click return; } + break; } event.Skip(); @@ -1523,15 +1522,15 @@ void SyncProgressDialogImpl::OnKeyPressed(wxKeyEvent& event) template -void SyncProgressDialogImpl::OnKeyPressedParent(wxKeyEvent& event) +void SyncProgressDialogImpl::onParentKeyEvent(wxKeyEvent& event) { //redirect keys from main dialog to progress dialog - const int keyCode = event.GetKeyCode(); - if (keyCode == WXK_ESCAPE) + switch (event.GetKeyCode()) { - this->SetFocus(); - this->OnKeyPressed(event); - return; + case WXK_ESCAPE: + this->SetFocus(); + this->onLocalKeyEvent(event); //event will be handled => no event recursion to parent dialog! + return; } event.Skip(); @@ -2014,14 +2013,18 @@ void SyncProgressDialogImpl::showSummary(SyncResult resultId, co //hide current operation status pnl_.bSizerStatusText->Show(false); - //show and prepare final statistics - pnl_.m_notebookResult->Show(); - pnl_.m_staticlineFooter->Hide(); //win: m_notebookResult already has a window frame //hide remaining time pnl_.m_panelTimeRemaining->Hide(); + //------------------------------------------------------------- + + pnl_.m_notebookResult->SetPadding(wxSize(2, 0)); //height cannot be changed + + const size_t pagePosProgress = 0; + const size_t pagePosLog = 1; + //1. re-arrange graph into results listbook const bool wasDetached = pnl_.bSizerRoot->Detach(pnl_.m_panelProgress); assert(wasDetached); @@ -2030,7 +2033,6 @@ void SyncProgressDialogImpl::showSummary(SyncResult resultId, co pnl_.m_notebookResult->AddPage(pnl_.m_panelProgress, _("Progress"), true /*bSelect*/); //2. log file - const size_t posLog = 1; assert(pnl_.m_notebookResult->GetPageCount() == 1); LogPanel* logPanel = new LogPanel(pnl_.m_notebookResult, log); //owned by m_notebookResult pnl_.m_notebookResult->AddPage(logPanel, _("Log"), false /*bSelect*/); @@ -2038,8 +2040,29 @@ void SyncProgressDialogImpl::showSummary(SyncResult resultId, co //show log instead of graph if errors occurred! (not required for ignored warnings) if (log.getItemCount(TYPE_ERROR | TYPE_FATAL_ERROR) > 0) - pnl_.m_notebookResult->ChangeSelection(posLog); - warn_static("/|\ not working on linux") + pnl_.m_notebookResult->ChangeSelection(pagePosLog); + + //fill image list to cope with wxNotebook image setting design desaster... + const int imgListSize = getResourceImage(L"log_file_small").GetHeight(); + assert(imgListSize == 16); //Windows default size for panel caption + auto imgList = std::make_unique(imgListSize, imgListSize); + + auto addToImageList = [&](const wxBitmap& bmp) + { + assert(bmp.GetWidth () <= imgListSize); + assert(bmp.GetHeight() <= imgListSize); + imgList->Add(bmp); + }; + addToImageList(getResourceImage(L"progress_small")); + addToImageList(getResourceImage(L"log_file_small")); + + pnl_.m_notebookResult->AssignImageList(imgList.release()); //pass ownership + + pnl_.m_notebookResult->SetPageImage(pagePosProgress, pagePosProgress); + pnl_.m_notebookResult->SetPageImage(pagePosLog, pagePosLog); + + //Caveat: we need "Show()" *after" the above wxNotebook::ChangeSelection() to get the correct selection on Linux + pnl_.m_notebookResult->Show(); //GetSizer()->SetSizeHints(this); //~=Fit() //not a good idea: will shrink even if window is maximized or was enlarged by the user pnl_.Layout(); diff --git a/FreeFileSync/Source/ui/search.cpp b/FreeFileSync/Source/ui/search.cpp index e22146d4..6bcfed34 100755 --- a/FreeFileSync/Source/ui/search.cpp +++ b/FreeFileSync/Source/ui/search.cpp @@ -47,8 +47,8 @@ ptrdiff_t findRow(const Grid& grid, //return -1 if no matching row found { if (auto prov = grid.getDataProvider()) { - std::vector colAttr = grid.getColumnConfig(); - erase_if(colAttr, [](const Grid::ColumnAttribute& ca) { return !ca.visible_; }); + std::vector colAttr = grid.getColumnConfig(); + erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); if (!colAttr.empty()) { const MatchFound matchFound(searchString); @@ -56,14 +56,14 @@ ptrdiff_t findRow(const Grid& grid, //return -1 if no matching row found if (searchAscending) { for (size_t row = rowFirst; row < rowLast; ++row) - for (const Grid::ColumnAttribute& ca : colAttr) - if (matchFound(prov->getValue(row, ca.type_))) + for (const Grid::ColAttributes& ca : colAttr) + if (matchFound(prov->getValue(row, ca.type))) return row; } else for (size_t row = rowLast; row-- > rowFirst;) - for (const Grid::ColumnAttribute& ca : colAttr) - if (matchFound(prov->getValue(row, ca.type_))) + for (const Grid::ColAttributes& ca : colAttr) + if (matchFound(prov->getValue(row, ca.type))) return row; } } diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 5c5c3851..9c4b9196 100755 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -23,7 +23,6 @@ #include #include #include "gui_generated.h" -#include "custom_grid.h" #include "folder_selector.h" #include "version_check.h" #include "../algorithm.h" @@ -47,6 +46,7 @@ private: void OnOK (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_OKAY); } void OnClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } void OnDonate(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://www.freefilesync.org/donate.php"); } + void onLocalKeyEvent(wxKeyEvent& event); }; @@ -136,6 +136,9 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) } m_bitmapLogo->SetBitmap(headerBmp); + //enable dialog-specific key local events + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(AboutDlg::onLocalKeyEvent), nullptr, this); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! Center(); //needs to be re-applied after a dialog size change! @@ -144,6 +147,12 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) } +void AboutDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + event.Skip(); +} + + void zen::showAboutDialog(wxWindow* parent) { AboutDlg aboutDlg(parent); @@ -171,6 +180,8 @@ private: void OnCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } void OnClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onLocalKeyEvent(wxKeyEvent& event); + std::unique_ptr targetFolder; //always bound std::shared_ptr folderHistory_; @@ -228,6 +239,9 @@ CopyToDialog::CopyToDialog(wxWindow* parent, m_checkBoxOverwriteIfExists->SetValue(overwriteIfExists); //----------------- /set config -------------------------------- + //enable dialog-specific key local events + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(CopyToDialog::onLocalKeyEvent), nullptr, this); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! Center(); //needs to be re-applied after a dialog size change! @@ -236,6 +250,12 @@ CopyToDialog::CopyToDialog(wxWindow* parent, } +void CopyToDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + event.Skip(); +} + + void CopyToDialog::OnOK(wxCommandEvent& event) { //------- parameter validation (BEFORE writing output!) ------- @@ -288,10 +308,12 @@ public: bool& useRecycleBin); private: - void OnOK (wxCommandEvent& event) override; - void OnCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void OnClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } - void OnUseRecycler (wxCommandEvent& event) override; + void OnUseRecycler(wxCommandEvent& event) override { updateGui(); } + void OnOK (wxCommandEvent& event) override; + void OnCancel (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void OnClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + + void onLocalKeyEvent(wxKeyEvent& event); void updateGui(); @@ -322,6 +344,9 @@ DeleteDialog::DeleteDialog(wxWindow* parent, updateGui(); + //enable dialog-specific key local events + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(DeleteDialog::onLocalKeyEvent), nullptr, this); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! Layout(); @@ -368,6 +393,12 @@ void DeleteDialog::updateGui() } +void DeleteDialog::onLocalKeyEvent(wxKeyEvent& event) +{ + event.Skip(); +} + + void DeleteDialog::OnOK(wxCommandEvent& event) { //additional safety net, similar to Windows Explorer: time delta between DEL and ENTER must be at least 50ms to avoid accidental deletion! @@ -380,12 +411,6 @@ void DeleteDialog::OnOK(wxCommandEvent& event) } -void DeleteDialog::OnUseRecycler(wxCommandEvent& event) -{ - updateGui(); -} - - ReturnSmallDlg::ButtonPressed zen::showDeleteDialog(wxWindow* parent, const std::vector& rowsOnLeft, const std::vector& rowsOnRight, @@ -409,6 +434,8 @@ private: void OnCancel (wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } void OnClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void onLocalKeyEvent(wxKeyEvent& event); + //output-only parameters: bool& dontShowAgainOut_; }; @@ -429,6 +456,8 @@ SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent, m_staticTextVariant->SetLabel(variantName); m_checkBoxDontShowAgain->SetValue(dontShowAgain); + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncConfirmationDlg::onLocalKeyEvent), nullptr, this); + //update preview of item count and bytes to be transferred: auto setValue = [](wxStaticText& txtControl, bool isZeroValue, const wxString& valueAsString, wxStaticBitmap& bmpControl, const wchar_t* bmpName) { @@ -467,6 +496,12 @@ SyncConfirmationDlg::SyncConfirmationDlg(wxWindow* parent, } +void SyncConfirmationDlg::onLocalKeyEvent(wxKeyEvent& event) +{ + event.Skip(); +} + + void SyncConfirmationDlg::OnStartSync(wxCommandEvent& event) { dontShowAgainOut_ = m_checkBoxDontShowAgain->GetValue(); @@ -505,6 +540,9 @@ private: void onResize(wxSizeEvent& event); void updateGui(); + //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! + //void onLocalKeyEvent(wxKeyEvent& event); + void OnToggleAutoRetryCount(wxCommandEvent& event) override { updateGui(); } void setExtApp(const xmlAccess::ExternalApps& extApp); @@ -745,6 +783,8 @@ private: m_calendarFrom->SetDate(m_calendarTo->GetDate()); } + void onLocalKeyEvent(wxKeyEvent& event); + //output-only parameters: time_t& timeFromOut_; time_t& timeToOut_; @@ -778,6 +818,9 @@ SelectTimespanDlg::SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& m_calendarFrom->SetDate(timeFromTmp); m_calendarTo ->SetDate(timeToTmp ); + //enable dialog-specific key local events + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SelectTimespanDlg::onLocalKeyEvent), nullptr, this); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! Center(); //needs to be re-applied after a dialog size change! @@ -786,6 +829,12 @@ SelectTimespanDlg::SelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& } +void SelectTimespanDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) +{ + event.Skip(); +} + + void SelectTimespanDlg::OnOkay(wxCommandEvent& event) { wxDateTime from = m_calendarFrom->GetDate(); @@ -825,6 +874,55 @@ ReturnSmallDlg::ButtonPressed zen::showSelectTimespanDlg(wxWindow* parent, time_ //######################################################################################## +class CfgHighlightDlg : public CfgHighlightDlgGenerated +{ +public: + CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); + +private: + void OnOkay (wxCommandEvent& event) override; + void OnCancel(wxCommandEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + void OnClose (wxCloseEvent& event) override { EndModal(ReturnSmallDlg::BUTTON_CANCEL); } + + //work around defunct keyboard focus on macOS (or is it wxMac?) => not needed for this dialog! + //void onLocalKeyEvent(wxKeyEvent& event); + + //output-only parameters: + int& cfgHistSyncOverdueDaysOut_; +}; + + +CfgHighlightDlg::CfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) : + CfgHighlightDlgGenerated(parent), + cfgHistSyncOverdueDaysOut_(cfgHistSyncOverdueDays) +{ + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOkay).setCancel(m_buttonCancel)); + + m_spinCtrlSyncOverdueDays->SetValue(cfgHistSyncOverdueDays); + + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + //=> works like a charm for GTK2 with window resizing problems and title bar corruption; e.g. Debian!!! + Center(); //needs to be re-applied after a dialog size change! + + m_spinCtrlSyncOverdueDays->SetFocus(); +} + + +void CfgHighlightDlg::OnOkay(wxCommandEvent& event) +{ + cfgHistSyncOverdueDaysOut_ = m_spinCtrlSyncOverdueDays->GetValue(); + EndModal(ReturnSmallDlg::BUTTON_OKAY); +} + + +ReturnSmallDlg::ButtonPressed zen::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) +{ + CfgHighlightDlg cfgHighDlg(parent, cfgHistSyncOverdueDays); + return static_cast(cfgHighDlg.ShowModal()); +} + +//######################################################################################## + class ActivationDlg : public ActivationDlgGenerated { public: diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h index ceff867b..e6af0872 100755 --- a/FreeFileSync/Source/ui/small_dlgs.h +++ b/FreeFileSync/Source/ui/small_dlgs.h @@ -50,6 +50,9 @@ ReturnSmallDlg::ButtonPressed showOptionsDlg(wxWindow* parent, xmlAccess::XmlGlo ReturnSmallDlg::ButtonPressed showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo); +ReturnSmallDlg::ButtonPressed showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays); + + enum class ReturnActivationDlg { diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index 7cbd3afe..887df0d6 100755 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "gui_generated.h" #include "command_box.h" #include "folder_selector.h" @@ -55,7 +56,7 @@ public: int localPairIndexToShow, std::vector& folderPairConfig, GlobalSyncConfig& globalCfg, - size_t commandHistoryMax); + size_t commandHistItemsMax); private: void OnOkay (wxCommandEvent& event) override; @@ -180,7 +181,7 @@ private: int selectedPairIndexToShow_ = EMPTY_PAIR_INDEX_SELECTED; static const int EMPTY_PAIR_INDEX_SELECTED = -2; - const size_t commandHistoryMax_; + const size_t commandHistItemsMax_; }; //################################################################################################################# @@ -224,28 +225,30 @@ ConfigDialog::ConfigDialog(wxWindow* parent, int localPairIndexToShow, std::vector& folderPairConfig, GlobalSyncConfig& globalCfg, - size_t commandHistoryMax) : + size_t commandHistItemsMax) : ConfigDlgGenerated(parent), versioningFolder_(*m_panelVersioning, *m_buttonSelectVersioningFolder, *m_bpButtonSelectAltFolder, *m_versioningFolderPath, nullptr /*staticText*/, nullptr /*dropWindow2*/), globalCfgOut_(globalCfg), folderPairConfigOut_(folderPairConfig), globalCfg_(globalCfg), folderPairConfig_(folderPairConfig), - commandHistoryMax_(commandHistoryMax) + commandHistItemsMax_(commandHistItemsMax) { setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOkay).setCancel(m_buttonCancel)); SetTitle(_("Synchronization Settings")); + m_notebook->SetPadding(wxSize(2, 0)); //height cannot be changed + //fill image list to cope with wxNotebook image setting design desaster... - const int imageListSize = getResourceImage(L"cfg_compare_small").GetHeight(); - assert(imageListSize == 16); //Windows default size for panel caption - auto imgList = std::make_unique(imageListSize, imageListSize); + const int imgListSize = getResourceImage(L"cfg_compare_small").GetHeight(); + assert(imgListSize == 16); //Windows default size for panel caption + auto imgList = std::make_unique(imgListSize, imgListSize); auto addToImageList = [&](const wxBitmap& bmp) { - assert(bmp.GetWidth () <= imageListSize); - assert(bmp.GetHeight() <= imageListSize); + assert(bmp.GetWidth () <= imgListSize); + assert(bmp.GetHeight() <= imgListSize); imgList->Add(bmp); imgList->Add(greyScale(bmp)); }; @@ -372,24 +375,25 @@ ConfigDialog::ConfigDialog(wxWindow* parent, void ConfigDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :) { - const int keyCode = event.GetKeyCode(); + auto changeSelection = [&](SyncConfigPanel panel) + { + m_notebook->ChangeSelection(static_cast(panel)); + (m_listBoxFolderPair->IsShown() ? static_cast(m_listBoxFolderPair) : m_notebook)->SetFocus(); //GTK ignores F-keys if focus is on hidden item! + }; - switch (keyCode) + switch (event.GetKeyCode()) { case WXK_F6: - m_notebook->ChangeSelection(static_cast(SyncConfigPanel::COMPARISON)); - break; //handled! + changeSelection(SyncConfigPanel::COMPARISON); + return; //handled! case WXK_F7: - m_notebook->ChangeSelection(static_cast(SyncConfigPanel::FILTER)); - break; + changeSelection(SyncConfigPanel::FILTER); + return; case WXK_F8: - m_notebook->ChangeSelection(static_cast(SyncConfigPanel::SYNC)); - break; - default: - event.Skip(); + changeSelection(SyncConfigPanel::SYNC); return; } - (m_listBoxFolderPair->IsShown() ? static_cast(m_listBoxFolderPair) : m_notebook)->SetFocus(); //GTK ignores F-keys if focus is on hidden item! + event.Skip(); } @@ -927,7 +931,7 @@ void ConfigDialog::updateSyncGui() }; //display only relevant sync options - m_bitmapDatabase ->Show(directionCfg_.var == DirectionConfig::TWO_WAY); + bSizerDatabase ->Show(directionCfg_.var == DirectionConfig::TWO_WAY); bSizerSyncDirections->Show(directionCfg_.var != DirectionConfig::TWO_WAY); if (directionCfg_.var == DirectionConfig::TWO_WAY) @@ -1046,7 +1050,7 @@ MiscSyncConfig ConfigDialog::getMiscSyncOptions() const miscCfg.ignoreErrors = m_checkBoxIgnoreErrors->GetValue(); miscCfg.postSyncCommand = m_comboBoxPostSyncCommand->getValue(); miscCfg.postSyncCondition = getEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition), - miscCfg.commandHistory = m_comboBoxPostSyncCommand->getHistory(); + miscCfg.commandHistory = m_comboBoxPostSyncCommand->getHistory(); return miscCfg; } @@ -1056,7 +1060,7 @@ void ConfigDialog::setMiscSyncOptions(const MiscSyncConfig& miscCfg) m_checkBoxIgnoreErrors->SetValue(miscCfg.ignoreErrors); m_comboBoxPostSyncCommand->setValue(miscCfg.postSyncCommand); setEnumVal(enumPostSyncCondition_, *m_choicePostSyncCondition, miscCfg.postSyncCondition), - m_comboBoxPostSyncCommand->setHistory(miscCfg.commandHistory, commandHistoryMax_); + m_comboBoxPostSyncCommand->setHistory(miscCfg.commandHistory, commandHistItemsMax_); updateMiscGui(); } @@ -1194,7 +1198,7 @@ ReturnSyncConfig::ButtonPressed zen::showSyncConfigDlg(wxWindow* parent, PostSyncCondition& postSyncCondition, std::vector& commandHistory, - size_t commandHistoryMax) + size_t commandHistItemsMax) { GlobalSyncConfig globalCfg; globalCfg.cmpConfig = globalCmpConfig; @@ -1211,7 +1215,7 @@ ReturnSyncConfig::ButtonPressed zen::showSyncConfigDlg(wxWindow* parent, localPairIndexToShow, folderPairConfig, globalCfg, - commandHistoryMax); + commandHistItemsMax); const auto rv = static_cast(syncDlg.ShowModal()); if (rv != ReturnSyncConfig::BUTTON_CANCEL) diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp new file mode 100755 index 00000000..c64a52cf --- /dev/null +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -0,0 +1,1281 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include +#include "tree_grid.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../lib/icon_buffer.h" + +using namespace zen; + + +namespace +{ +const int WIDTH_PERCENTAGE_BAR = 60; +} + + +inline +void TreeView::compressNode(Container& cont) //remove single-element sub-trees -> gain clarity + usability (call *after* inclusion check!!!) +{ + if (cont.subDirs.empty()) //single files node + cont.firstFileId = nullptr; + +#if 0 //let's not go overboard: empty folders should not be condensed => used for file exclusion filter; user expects to see them + if (cont.firstFileId == nullptr && //single dir node... + cont.subDirs.size() == 1 && // + cont.subDirs[0].firstFileId == nullptr && //...that is empty + cont.subDirs[0].subDirs.empty()) // + cont.subDirs.clear(); +#endif +} + + +template //(const FileSystemObject&) -> bool +void TreeView::extractVisibleSubtree(ContainerObject& hierObj, //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(); + // case SyncDirection::RIGHT: + // return file.getFileSize(); + // case SyncDirection::NONE: + // break; + // } + + //prefer file-browser semantics over sync preview (=> always show useful numbers, even for SyncDirection::NONE) + //discussion: https://www.freefilesync.org/forum/viewtopic.php?t=1595 + return std::max(file.getFileSize(), file.getFileSize()); + }; + + cont.firstFileId = nullptr; + for (FilePair& file : hierObj.refSubFiles()) + if (pred(file)) + { + cont.bytesNet += getBytes(file); + ++cont.itemCountNet; + + if (!cont.firstFileId) + cont.firstFileId = file.getId(); + } + + for (SymlinkPair& symlink : hierObj.refSubLinks()) + if (pred(symlink)) + { + ++cont.itemCountNet; + + if (!cont.firstFileId) + cont.firstFileId = symlink.getId(); + } + + cont.bytesGross += cont.bytesNet; + cont.itemCountGross += cont.itemCountNet; + + cont.subDirs.reserve(hierObj.refSubFolders().size()); //avoid expensive reallocations! + + for (FolderPair& folder : hierObj.refSubFolders()) + { + const bool included = pred(folder); + + cont.subDirs.emplace_back(); // + auto& subDirCont = cont.subDirs.back(); + TreeView::extractVisibleSubtree(folder, subDirCont, pred); + if (included) + ++subDirCont.itemCountGross; + + cont.bytesGross += subDirCont.bytesGross; + cont.itemCountGross += subDirCont.itemCountGross; + + if (!included && !subDirCont.firstFileId && subDirCont.subDirs.empty()) + cont.subDirs.pop_back(); + else + { + subDirCont.objId = folder.getId(); + compressNode(subDirCont); + } + } +} + + +namespace +{ +//generate nice percentage numbers which precisely sum up to 100 +void calcPercentage(std::vector>& workList) +{ + const uint64_t total = std::accumulate(workList.begin(), workList.end(), uint64_t(), + [](uint64_t sum, const std::pair& pair) { return sum + pair.first; }); + + if (total == 0U) //this case doesn't work with the error minimizing algorithm below + { + for (auto& pair : workList) + *pair.second = 0; + return; + } + + int remainingPercent = 100; + for (auto& pair : workList) + { + *pair.second = static_cast(pair.first * 100U / total); //round down + remainingPercent -= *pair.second; + } + assert(remainingPercent >= 0); + assert(remainingPercent < static_cast(workList.size())); + + //distribute remaining percent so that overall error is minimized as much as possible: + remainingPercent = std::min(remainingPercent, static_cast(workList.size())); + if (remainingPercent > 0) + { + std::nth_element(workList.begin(), workList.begin() + remainingPercent - 1, workList.end(), + [total](const std::pair& lhs, const std::pair& rhs) + { + return lhs.first * 100U % total > rhs.first * 100U % total; + }); + + std::for_each(workList.begin(), workList.begin() + remainingPercent, [&](std::pair& pair) { ++*pair.second; }); + } +} +} + + +Zstring zen::getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR) +{ + Zstring commonTrail; + AbstractPath tmpPathL = itemPathL; + AbstractPath tmpPathR = itemPathR; + for (;;) + { + Opt parentPathL = AFS::getParentFolderPath(tmpPathL); + Opt parentPathR = AFS::getParentFolderPath(tmpPathR); + if (!parentPathL || !parentPathR) + break; + + const Zstring itemNameL = AFS::getItemName(tmpPathL); + const Zstring itemNameR = AFS::getItemName(tmpPathR); + if (!strEqual(itemNameL, itemNameR, CmpNaturalSort())) //let's compare case-insensitively even on Linux! + break; + + tmpPathL = *parentPathL; + tmpPathR = *parentPathR; + + commonTrail = AFS::appendPaths(itemNameL, commonTrail, FILE_NAME_SEPARATOR); + } + if (!commonTrail.empty()) + return commonTrail; + + auto getLastComponent = [](const AbstractPath& itemPath) + { + if (!AFS::getParentFolderPath(itemPath)) //= device root + return utfTo(AFS::getDisplayPath(itemPath)); + return AFS::getItemName(itemPath); + }; + + if (AFS::isNullPath(itemPathL)) + return getLastComponent(itemPathR); + else if (AFS::isNullPath(itemPathR)) + return getLastComponent(itemPathL); + else + return getLastComponent(itemPathL) + utfTo(SPACED_DASH) + + getLastComponent(itemPathR); +} + + +template +struct TreeView::LessShortName +{ + bool operator()(const TreeLine& lhs, const TreeLine& rhs) const + { + //files last (irrespective of sort direction) + if (lhs.type == TreeView::TYPE_FILES) + return false; + else if (rhs.type == TreeView::TYPE_FILES) + return true; + + if (lhs.type != rhs.type) // + return lhs.type < rhs.type; //shouldn't happen! root nodes not mixed with files or directories + + switch (lhs.type) + { + case TreeView::TYPE_ROOT: + return makeSortDirection(LessNaturalSort() /*even on Linux*/, Int2Type())(static_cast(lhs.node)->displayName, + static_cast(rhs.node)->displayName); + + case TreeView::TYPE_DIRECTORY: + { + const auto* folderL = dynamic_cast(FileSystemObject::retrieve(static_cast(lhs.node)->objId)); + const auto* folderR = dynamic_cast(FileSystemObject::retrieve(static_cast(rhs.node)->objId)); + + if (!folderL) //might be pathologic, but it's covered + return false; + else if (!folderR) + return true; + + return makeSortDirection(LessNaturalSort() /*even on Linux*/, Int2Type())(folderL->getPairItemName(), folderR->getPairItemName()); + } + + case TreeView::TYPE_FILES: + break; + } + assert(false); + return false; //:= all equal + } +}; + + +template +void TreeView::sortSingleLevel(std::vector& items, ColumnTypeTree columnType) +{ + auto getBytes = [](const TreeLine& line) -> uint64_t + { + switch (line.type) + { + case TreeView::TYPE_ROOT: + case TreeView::TYPE_DIRECTORY: + return line.node->bytesGross; + case TreeView::TYPE_FILES: + return line.node->bytesNet; + } + assert(false); + return 0U; + }; + + auto getCount = [](const TreeLine& line) -> int + { + switch (line.type) + { + case TreeView::TYPE_ROOT: + case TreeView::TYPE_DIRECTORY: + return line.node->itemCountGross; + + case TreeView::TYPE_FILES: + return line.node->itemCountNet; + } + assert(false); + return 0; + }; + + const auto lessBytes = [&](const TreeLine& lhs, const TreeLine& rhs) { return getBytes(lhs) < getBytes(rhs); }; + const auto lessCount = [&](const TreeLine& lhs, const TreeLine& rhs) { return getCount(lhs) < getCount(rhs); }; + + switch (columnType) + { + case ColumnTypeTree::FOLDER_NAME: + std::sort(items.begin(), items.end(), LessShortName()); + break; + case ColumnTypeTree::ITEM_COUNT: + std::sort(items.begin(), items.end(), makeSortDirection(lessCount, Int2Type())); + break; + case ColumnTypeTree::BYTES: + std::sort(items.begin(), items.end(), makeSortDirection(lessBytes, Int2Type())); + break; + } +} + + +void TreeView::getChildren(const Container& cont, unsigned int level, std::vector& output) +{ + output.clear(); + output.reserve(cont.subDirs.size() + 1); //keep pointers in "workList" valid + std::vector> workList; + + for (const DirNodeImpl& subDir : cont.subDirs) + { + output.push_back({ level, 0, &subDir, TreeView::TYPE_DIRECTORY }); + workList.emplace_back(subDir.bytesGross, &output.back().percent); + } + + if (cont.firstFileId) + { + output.push_back({ level, 0, &cont, TreeView::TYPE_FILES }); + workList.emplace_back(cont.bytesNet, &output.back().percent); + } + calcPercentage(workList); + + if (sortAscending_) + sortSingleLevel(output, sortColumn_); + else + sortSingleLevel(output, sortColumn_); +} + + +void TreeView::applySubView(std::vector&& newView) +{ + //preserve current node expansion status + auto getHierAlias = [](const TreeView::TreeLine& tl) -> const ContainerObject* + { + switch (tl.type) + { + case TreeView::TYPE_ROOT: + return static_cast(tl.node)->baseFolder.get(); + + case TreeView::TYPE_DIRECTORY: + if (auto folder = dynamic_cast(FileSystemObject::retrieve(static_cast(tl.node)->objId))) + return folder; + break; + + case TreeView::TYPE_FILES: + break; //none!!! + } + return nullptr; + }; + + std::unordered_set expandedNodes; + if (!flatTree_.empty()) + { + auto it = flatTree_.begin(); + for (auto iterNext = flatTree_.begin() + 1; iterNext != flatTree_.end(); ++iterNext, ++it) + if (it->level < iterNext->level) + if (auto hierObj = getHierAlias(*it)) + expandedNodes.insert(hierObj); + } + + //update view on full data + folderCmpView_.swap(newView); //newView may be an alias for folderCmpView! see sorting! + + //set default flat tree + flatTree_.clear(); + + if (folderCmp_.size() == 1) //single folder pair case (empty pairs were already removed!) do NOT use folderCmpView for this check! + { + if (!folderCmpView_.empty()) //possibly empty! + getChildren(folderCmpView_[0], 0, flatTree_); //do not show root + } + else + { + //following is almost identical with TreeView::getChildren(): however we *cannot* reuse code here; + //this were only possible if we replaced "std::vector" with "Container"! + + flatTree_.reserve(folderCmpView_.size()); //keep pointers in "workList" valid + std::vector> workList; + + for (const RootNodeImpl& root : folderCmpView_) + { + flatTree_.push_back({ 0, 0, &root, TreeView::TYPE_ROOT }); + workList.emplace_back(root.bytesGross, &flatTree_.back().percent); + } + + calcPercentage(workList); + + if (sortAscending_) + sortSingleLevel(flatTree_, sortColumn_); + else + sortSingleLevel(flatTree_, sortColumn_); + } + + //restore node expansion status + for (size_t row = 0; row < flatTree_.size(); ++row) //flatTree size changes during loop! + { + const TreeLine& line = flatTree_[row]; + + if (auto hierObj = getHierAlias(line)) + if (expandedNodes.find(hierObj) != expandedNodes.end()) + { + std::vector newLines; + getChildren(*line.node, line.level + 1, newLines); + + flatTree_.insert(flatTree_.begin() + row + 1, newLines.begin(), newLines.end()); + } + } +} + + +template +void TreeView::updateView(Predicate pred) +{ + //update view on full data + std::vector newView; + newView.reserve(folderCmp_.size()); //avoid expensive reallocations! + + for (const std::shared_ptr& baseObj : folderCmp_) + { + newView.emplace_back(); + RootNodeImpl& root = newView.back(); + this->extractVisibleSubtree(*baseObj, root, pred); //"this->" is bogus for a static method, but GCC screws this one up + + //warning: the following lines are almost 1:1 copy from extractVisibleSubtree: + //however we *cannot* reuse code here; this were only possible if we replaced "std::vector" with "Container"! + if (!root.firstFileId && root.subDirs.empty()) + newView.pop_back(); + else + { + root.baseFolder = baseObj; + root.displayName = getShortDisplayNameForFolderPair(baseObj->getAbstractPath< LEFT_SIDE>(), + baseObj->getAbstractPath()); + + this->compressNode(root); //"this->" required by two-pass lookup as enforced by GCC 4.7 + } + } + + lastViewFilterPred_ = pred; + applySubView(std::move(newView)); +} + + +void TreeView::setSortDirection(ColumnTypeTree colType, bool ascending) //apply permanently! +{ + sortColumn_ = colType; + sortAscending_ = ascending; + + //reapply current view + applySubView(std::move(folderCmpView_)); +} + + +TreeView::NodeStatus TreeView::getStatus(size_t row) const +{ + if (row < flatTree_.size()) + { + if (row + 1 < flatTree_.size() && flatTree_[row + 1].level > flatTree_[row].level) + return TreeView::STATUS_EXPANDED; + + //it's either reduced or empty + switch (flatTree_[row].type) + { + case TreeView::TYPE_DIRECTORY: + case TreeView::TYPE_ROOT: + return flatTree_[row].node->firstFileId || !flatTree_[row].node->subDirs.empty() ? TreeView::STATUS_REDUCED : TreeView::STATUS_EMPTY; + + case TreeView::TYPE_FILES: + return TreeView::STATUS_EMPTY; + } + } + return TreeView::STATUS_EMPTY; +} + + +void TreeView::expandNode(size_t row) +{ + if (getStatus(row) != TreeView::STATUS_REDUCED) + { + assert(false); + return; + } + + if (row < flatTree_.size()) + { + std::vector newLines; + + switch (flatTree_[row].type) + { + case TreeView::TYPE_ROOT: + case TreeView::TYPE_DIRECTORY: + getChildren(*flatTree_[row].node, flatTree_[row].level + 1, newLines); + break; + case TreeView::TYPE_FILES: + break; + } + flatTree_.insert(flatTree_.begin() + row + 1, newLines.begin(), newLines.end()); + } +} + + +void TreeView::reduceNode(size_t row) +{ + if (row < flatTree_.size()) + { + const unsigned int parentLevel = flatTree_[row].level; + + bool done = false; + flatTree_.erase(std::remove_if(flatTree_.begin() + row + 1, flatTree_.end(), + [&](const TreeLine& line) -> bool + { + if (done) + return false; + if (line.level > parentLevel) + return true; + else + { + done = true; + return false; + } + }), flatTree_.end()); + } +} + + +ptrdiff_t TreeView::getParent(size_t row) const +{ + if (row < flatTree_.size()) + { + const auto level = flatTree_[row].level; + + while (row-- > 0) + if (flatTree_[row].level < level) + return row; + } + return -1; +} + + +void TreeView::updateCmpResult(bool showExcluded, + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive) +{ + updateView([showExcluded, //make sure the predicate can be stored safely! + leftOnlyFilesActive, + rightOnlyFilesActive, + leftNewerFilesActive, + rightNewerFilesActive, + differentFilesActive, + equalFilesActive, + conflictFilesActive](const FileSystemObject& fsObj) -> bool + { + if (!fsObj.isActive() && !showExcluded) + return false; + + switch (fsObj.getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + return leftOnlyFilesActive; + case FILE_RIGHT_SIDE_ONLY: + return rightOnlyFilesActive; + case FILE_LEFT_NEWER: + return leftNewerFilesActive; + case FILE_RIGHT_NEWER: + return rightNewerFilesActive; + case FILE_DIFFERENT_CONTENT: + return differentFilesActive; + case FILE_EQUAL: + case FILE_DIFFERENT_METADATA: //= sub-category of equal + return equalFilesActive; + case FILE_CONFLICT: + return conflictFilesActive; + } + assert(false); + return true; + }); +} + + +void TreeView::updateSyncPreview(bool showExcluded, + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive) +{ + updateView([showExcluded, //make sure the predicate can be stored safely! + syncCreateLeftActive, + syncCreateRightActive, + syncDeleteLeftActive, + syncDeleteRightActive, + syncDirOverwLeftActive, + syncDirOverwRightActive, + syncDirNoneActive, + syncEqualActive, + conflictFilesActive](const FileSystemObject& fsObj) -> bool + { + if (!fsObj.isActive() && !showExcluded) + return false; + + switch (fsObj.getSyncOperation()) + { + case SO_CREATE_NEW_LEFT: + return syncCreateLeftActive; + case SO_CREATE_NEW_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_MOVE_RIGHT_FROM: + case SO_MOVE_RIGHT_TO: + return syncDirOverwRightActive; + case SO_OVERWRITE_LEFT: + case SO_COPY_METADATA_TO_LEFT: + case SO_MOVE_LEFT_FROM: + case SO_MOVE_LEFT_TO: + return syncDirOverwLeftActive; + case SO_DO_NOTHING: + return syncDirNoneActive; + case SO_EQUAL: + return syncEqualActive; + case SO_UNRESOLVED_CONFLICT: + return conflictFilesActive; + } + assert(false); + return true; + }); +} + + +void TreeView::setData(FolderComparison& newData) +{ + std::vector().swap(flatTree_); //free mem + std::vector().swap(folderCmpView_); // + folderCmp_ = newData; + + //remove truly empty folder pairs as early as this: we want to distinguish single/multiple folder pair cases by looking at "folderCmp" + erase_if(folderCmp_, [](const std::shared_ptr& baseObj) + { + return AFS::isNullPath(baseObj->getAbstractPath< LEFT_SIDE>()) && + AFS::isNullPath(baseObj->getAbstractPath()); + }); +} + + +std::unique_ptr TreeView::getLine(size_t row) const +{ + if (row < flatTree_.size()) + { + const auto level = flatTree_[row].level; + const int percent = flatTree_[row].percent; + + switch (flatTree_[row].type) + { + case TreeView::TYPE_ROOT: + { + const auto& root = *static_cast(flatTree_[row].node); + return std::make_unique(percent, root.bytesGross, root.itemCountGross, getStatus(row), *root.baseFolder, root.displayName); + } + break; + + case TreeView::TYPE_DIRECTORY: + { + const auto* dir = static_cast(flatTree_[row].node); + if (auto folder = dynamic_cast(FileSystemObject::retrieve(dir->objId))) + return std::make_unique(percent, dir->bytesGross, dir->itemCountGross, level, getStatus(row), *folder); + } + break; + + case TreeView::TYPE_FILES: + { + const auto* parentDir = flatTree_[row].node; + if (auto firstFile = FileSystemObject::retrieve(parentDir->firstFileId)) + { + std::vector filesAndLinks; + ContainerObject& parent = firstFile->parent(); + + //lazy evaluation: recheck "lastViewFilterPred" again rather than buffer and bloat "lastViewFilterPred" + for (FileSystemObject& fsObj : parent.refSubFiles()) + if (lastViewFilterPred_(fsObj)) + filesAndLinks.push_back(&fsObj); + + for (FileSystemObject& fsObj : parent.refSubLinks()) + if (lastViewFilterPred_(fsObj)) + filesAndLinks.push_back(&fsObj); + + return std::make_unique(percent, parentDir->bytesNet, parentDir->itemCountNet, level, filesAndLinks); + } + } + break; + } + } + return nullptr; +} + +//########################################################################################################## + +namespace +{ +//let's NOT create wxWidgets objects statically: +inline wxColor getColorPercentBorder () { return { 198, 198, 198 }; } +inline wxColor getColorPercentBackground() { return { 0xf8, 0xf8, 0xf8 }; } + +inline wxColor getColorTreeSelectionGradientFrom() { return Grid::getColorSelectionGradientFrom(); } +inline wxColor getColorTreeSelectionGradientTo () { return Grid::getColorSelectionGradientTo (); } + +const int iconSizeSmall = IconBuffer::getSize(IconBuffer::SIZE_SMALL); + + +wxColor getColorForLevel(size_t level) +{ + switch (level % 12) + { + case 0: + return { 0xcc, 0xcc, 0xff }; + case 1: + return { 0xcc, 0xff, 0xcc }; + case 2: + return { 0xff, 0xff, 0x99 }; + case 3: + return { 0xcc, 0xcc, 0xcc }; + case 4: + return { 0xff, 0xcc, 0xff }; + case 5: + return { 0x99, 0xff, 0xcc }; + case 6: + return { 0xcc, 0xcc, 0x99 }; + case 7: + return { 0xff, 0xcc, 0xcc }; + case 8: + return { 0xcc, 0xff, 0x99 }; + case 9: + return { 0xff, 0xff, 0xcc }; + case 10: + return { 0xcc, 0xff, 0xff }; + case 11: + return { 0xff, 0xcc, 0x99 }; + } + assert(false); + return *wxBLACK; +} + + +class GridDataTree : private wxEvtHandler, public GridData +{ +public: + GridDataTree(Grid& grid) : + rootBmp_(getResourceImage(L"rootFolder").ConvertToImage().Scale(iconSizeSmall, iconSizeSmall, wxIMAGE_QUALITY_HIGH)), + widthNodeIcon_(iconSizeSmall), + widthLevelStep_(widthNodeIcon_), + widthNodeStatus_(getResourceImage(L"node_expanded").GetWidth()), + grid_(grid) + { + grid.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridDataTree::onKeyDown), nullptr, this); + grid.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler (GridDataTree::onMouseLeft ), nullptr, this); + grid.Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler (GridDataTree::onMouseLeftDouble ), nullptr, this); + grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(GridDataTree::onGridLabelContext ), nullptr, this); + grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(GridDataTree::onGridLabelLeftClick), nullptr, this); + } + + void setShowPercentage(bool value) { showPercentBar_ = value; grid_.Refresh(); } + bool getShowPercentage() const { return showPercentBar_; } + + TreeView& getDataView() { return treeDataView_; } + +private: + size_t getRowCount() const override { return treeDataView_.linesTotal(); } + + std::wstring getToolTip(size_t row, ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeTree::FOLDER_NAME: + if (std::unique_ptr node = treeDataView_.getLine(row)) + if (const TreeView::RootNode* root = dynamic_cast(node.get())) + { + const std::wstring& dirLeft = AFS::getDisplayPath(root->baseFolder_.getAbstractPath< LEFT_SIDE>()); + const std::wstring& dirRight = AFS::getDisplayPath(root->baseFolder_.getAbstractPath()); + if (dirLeft.empty()) + return dirRight; + else if (dirRight.empty()) + return dirLeft; + return dirLeft + L" \u2013"/*en dash*/ + L"\n" + dirRight; + } + break; + + case ColumnTypeTree::ITEM_COUNT: + case ColumnTypeTree::BYTES: + break; + } + return std::wstring(); + } + + std::wstring getValue(size_t row, ColumnType colType) const override + { + if (std::unique_ptr node = treeDataView_.getLine(row)) + switch (static_cast(colType)) + { + case ColumnTypeTree::FOLDER_NAME: + if (const TreeView::RootNode* root = dynamic_cast(node.get())) + return utfTo(root->displayName_); + else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) + return utfTo(dir->folder_.getPairItemName()); + else if (dynamic_cast(node.get())) + return _("Files"); + break; + + case ColumnTypeTree::ITEM_COUNT: + return formatNumber(node->itemCount_); + + case ColumnTypeTree::BYTES: + return formatFilesizeShort(node->bytes_); + } + + return std::wstring(); + } + + void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); + + rectInside.x += COLUMN_GAP_LEFT; + rectInside.width -= COLUMN_GAP_LEFT; + drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); + + auto sortInfo = treeDataView_.getSortDirection(); + if (colType == static_cast(sortInfo.first)) + { + const wxBitmap& marker = getResourceImage(sortInfo.second ? L"sortAscending" : L"sortDescending"); + drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL); + } + } + + static const int GAP_SIZE = 2; + + enum class HoverAreaTree + { + NODE, + }; + + void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override + { + if (enabled) + { + if (selected) + dc.GradientFillLinear(rect, getColorTreeSelectionGradientFrom(), getColorTreeSelectionGradientTo(), wxEAST); + //ignore focus + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + } + + void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override + { + //wxRect rectTmp= drawCellBorder(dc, rect); + wxRect rectTmp = rect; + + // Partitioning: + // ________________________________________________________________________________ + // | space | gap | percentage bar | 2 x gap | node status | gap |icon | gap | rest | + // -------------------------------------------------------------------------------- + // -> synchronize renderCell() <-> getBestSize() <-> getRowMouseHover() + + if (static_cast(colType) == ColumnTypeTree::FOLDER_NAME) + { + if (std::unique_ptr node = treeDataView_.getLine(row)) + { + ////clear first secion: + //clearArea(dc, wxRect(rect.GetTopLeft(), wxSize( + // node->level_ * widthLevelStep_ + GAP_SIZE + //width + // (showPercentBar ? WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE : 0) + // + // widthNodeStatus_ + GAP_SIZE + widthNodeIcon + GAP_SIZE, // + // rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + //consume space + rectTmp.x += static_cast(node->level_) * widthLevelStep_; + rectTmp.width -= static_cast(node->level_) * widthLevelStep_; + + rectTmp.x += GAP_SIZE; + rectTmp.width -= GAP_SIZE; + + if (rectTmp.width > 0) + { + //percentage bar + if (showPercentBar_) + { + + const wxRect areaPerc(rectTmp.x, rectTmp.y + 2, WIDTH_PERCENTAGE_BAR, rectTmp.height - 4); + { + //clear background + wxDCPenChanger dummy (dc, getColorPercentBorder()); + wxDCBrushChanger dummy2(dc, getColorPercentBackground()); + dc.DrawRectangle(areaPerc); + + //inner area + const wxColor brushCol = getColorForLevel(node->level_); + dc.SetPen (brushCol); + dc.SetBrush(brushCol); + + wxRect areaPercTmp = areaPerc; + areaPercTmp.Deflate(1); //do not include border + areaPercTmp.width = numeric::round(areaPercTmp.width * node->percent_ / 100.0); + dc.DrawRectangle(areaPercTmp); + } + + wxDCTextColourChanger dummy3(dc, *wxBLACK); //accessibility: always set both foreground AND background colors! + drawCellText(dc, areaPerc, numberTo(node->percent_) + L"%", wxALIGN_CENTER); + + rectTmp.x += WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE; + rectTmp.width -= WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE; + } + if (rectTmp.width > 0) + { + //node status + auto drawStatus = [&](const wchar_t* image) + { + const wxBitmap& bmp = getResourceImage(image); + + wxRect rectStat(rectTmp.GetTopLeft(), wxSize(bmp.GetWidth(), bmp.GetHeight())); + rectStat.y += (rectTmp.height - rectStat.height) / 2; + + //clearArea(dc, rectStat, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + clearArea(dc, rectStat, *wxWHITE); //accessibility: always set both foreground AND background colors! + drawBitmapRtlMirror(dc, bmp, rectStat, wxALIGN_CENTER, renderBuf_); + }; + + const bool drawMouseHover = static_cast(rowHover) == HoverAreaTree::NODE; + switch (node->status_) + { + case TreeView::STATUS_EXPANDED: + drawStatus(drawMouseHover ? L"node_expanded_hover" : L"node_expanded"); + break; + case TreeView::STATUS_REDUCED: + drawStatus(drawMouseHover ? L"node_reduced_hover" : L"node_reduced"); + break; + case TreeView::STATUS_EMPTY: + break; + } + + rectTmp.x += widthNodeStatus_ + GAP_SIZE; + rectTmp.width -= widthNodeStatus_ + GAP_SIZE; + if (rectTmp.width > 0) + { + wxBitmap nodeIcon; + bool isActive = true; + //icon + if (dynamic_cast(node.get())) + nodeIcon = rootBmp_; + else if (auto dir = dynamic_cast(node.get())) + { + nodeIcon = dirIcon_; + isActive = dir->folder_.isActive(); + } + else if (dynamic_cast(node.get())) + nodeIcon = fileIcon_; + + if (!isActive) + nodeIcon = wxBitmap(nodeIcon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)); //treat all channels equally! + + drawBitmapRtlNoMirror(dc, nodeIcon, rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + rectTmp.x += widthNodeIcon_ + GAP_SIZE; + rectTmp.width -= widthNodeIcon_ + GAP_SIZE; + + if (rectTmp.width > 0) + { + wxDCTextColourChanger dummy(dc); + if (!isActive) + dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + + drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + } + } + } + } + } + else + { + int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL; + + //have file size and item count right-justified (but don't change for RTL languages) + if ((static_cast(colType) == ColumnTypeTree::BYTES || + static_cast(colType) == ColumnTypeTree::ITEM_COUNT) && grid_.GetLayoutDirection() != wxLayout_RightToLeft) + { + rectTmp.width -= 2 * GAP_SIZE; + alignment = wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL; + } + else //left-justified + { + rectTmp.x += 2 * GAP_SIZE; + rectTmp.width -= 2 * GAP_SIZE; + } + + drawCellText(dc, rectTmp, getValue(row, colType), alignment); + } + } + + int getBestSize(wxDC& dc, size_t row, ColumnType colType) override + { + // -> synchronize renderCell() <-> getBestSize() <-> getRowMouseHover() + + if (static_cast(colType) == ColumnTypeTree::FOLDER_NAME) + { + if (std::unique_ptr node = treeDataView_.getLine(row)) + return node->level_ * widthLevelStep_ + GAP_SIZE + (showPercentBar_ ? WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE : 0) + widthNodeStatus_ + GAP_SIZE + + widthNodeIcon_ + GAP_SIZE + dc.GetTextExtent(getValue(row, colType)).GetWidth() + + GAP_SIZE; //additional gap from right + else + return 0; + } + else + return 2 * GAP_SIZE + dc.GetTextExtent(getValue(row, colType)).GetWidth() + + 2 * GAP_SIZE; //include gap from right! + } + + HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override + { + switch (static_cast(colType)) + { + case ColumnTypeTree::FOLDER_NAME: + if (std::unique_ptr node = treeDataView_.getLine(row)) + { + const int tolerance = 2; + const int nodeStatusXFirst = -tolerance + static_cast(node->level_) * widthLevelStep_ + GAP_SIZE + (showPercentBar_ ? WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE : 0); + const int nodeStatusXLast = (nodeStatusXFirst + tolerance) + widthNodeStatus_ + tolerance; + // -> synchronize renderCell() <-> getBestSize() <-> getRowMouseHover() + + if (nodeStatusXFirst <= cellRelativePosX && cellRelativePosX < nodeStatusXLast) + return static_cast(HoverAreaTree::NODE); + } + break; + + case ColumnTypeTree::ITEM_COUNT: + case ColumnTypeTree::BYTES: + break; + } + return HoverArea::NONE; + } + + std::wstring getColumnLabel(ColumnType colType) const override + { + switch (static_cast(colType)) + { + case ColumnTypeTree::FOLDER_NAME: + return _("Name"); + case ColumnTypeTree::ITEM_COUNT: + return _("Items"); + case ColumnTypeTree::BYTES: + return _("Size"); + } + return std::wstring(); + } + + void onMouseLeft(GridClickEvent& event) + { + switch (static_cast(event.hoverArea_)) + { + case HoverAreaTree::NODE: + switch (treeDataView_.getStatus(event.row_)) + { + case TreeView::STATUS_EXPANDED: + return reduceNode(event.row_); + case TreeView::STATUS_REDUCED: + return expandNode(event.row_); + case TreeView::STATUS_EMPTY: + break; + } + break; + } + event.Skip(); + } + + void onMouseLeftDouble(GridClickEvent& event) + { + switch (treeDataView_.getStatus(event.row_)) + { + case TreeView::STATUS_EXPANDED: + return reduceNode(event.row_); + case TreeView::STATUS_REDUCED: + return expandNode(event.row_); + case TreeView::STATUS_EMPTY: + break; + } + event.Skip(); + } + + void onKeyDown(wxKeyEvent& event) + { + int keyCode = event.GetKeyCode(); + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + const size_t rowCount = grid_.getRowCount(); + if (rowCount == 0) return; + + const size_t row = grid_.getGridCursor(); + if (event.ShiftDown()) + ; + else if (event.ControlDown()) + ; + else + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + case WXK_NUMPAD_SUBTRACT: //https://msdn.microsoft.com/en-us/library/ms971323#atg_keyboardshortcuts_windows_shortcut_keys + switch (treeDataView_.getStatus(row)) + { + case TreeView::STATUS_EXPANDED: + return reduceNode(row); + case TreeView::STATUS_REDUCED: + case TreeView::STATUS_EMPTY: + + const int parentRow = treeDataView_.getParent(row); + if (parentRow >= 0) + grid_.setGridCursor(parentRow); + break; + } + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + case WXK_NUMPAD_ADD: + switch (treeDataView_.getStatus(row)) + { + case TreeView::STATUS_EXPANDED: + grid_.setGridCursor(std::min(rowCount - 1, row + 1)); + break; + case TreeView::STATUS_REDUCED: + return expandNode(row); + case TreeView::STATUS_EMPTY: + break; + } + return; //swallow event + } + + event.Skip(); + } + + void onGridLabelContext(GridLabelClickEvent& event) + { + ContextMenu menu; + //-------------------------------------------------------------------------------------------------------- + menu.addCheckBox(_("Percentage"), [this] { setShowPercentage(!getShowPercentage()); }, getShowPercentage()); + //-------------------------------------------------------------------------------------------------------- + auto toggleColumn = [&](ColumnType ct) + { + auto colAttr = grid_.getColumnConfig(); + + Grid::ColAttributes* caFolderName = nullptr; + Grid::ColAttributes* caToggle = nullptr; + + for (Grid::ColAttributes& ca : colAttr) + if (ca.type == static_cast(ColumnTypeTree::FOLDER_NAME)) + caFolderName = &ca; + else if (ca.type == ct) + caToggle = &ca; + + assert(caFolderName && caFolderName->stretch > 0 && caFolderName->visible); + assert(caToggle && caToggle->stretch == 0); + + if (caFolderName && caToggle) + { + caToggle->visible = !caToggle->visible; + + //take width of newly visible column from stretched folder name column + caFolderName->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset; + + grid_.setColumnConfig(colAttr); + } + }; + + for (const Grid::ColAttributes& ca : grid_.getColumnConfig()) + { + menu.addCheckBox(getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); }, + ca.visible, ca.type != static_cast(ColumnTypeTree::FOLDER_NAME)); //do not allow user to hide file name column! + } + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setDefaultColumns = [&] + { + setShowPercentage(treeGridShowPercentageDefault); + grid_.setColumnConfig(convertColAttributes(getTreeGridDefaultColAttribs(), getTreeGridDefaultColAttribs())); + }; + menu.addItem(_("&Default"), setDefaultColumns); //'&' -> reuse text from "default" buttons elsewhere + //-------------------------------------------------------------------------------------------------------- + + menu.popup(grid_); + //event.Skip(); + } + + void onGridLabelLeftClick(GridLabelClickEvent& event) + { + const auto colTypeTree = static_cast(event.colType_); + bool sortAscending = getDefaultSortDirection(colTypeTree); + + const auto sortInfo = treeDataView_.getSortDirection(); + if (sortInfo.first == colTypeTree) + sortAscending = !sortInfo.second; + + treeDataView_.setSortDirection(colTypeTree, sortAscending); + grid_.clearSelection(ALLOW_GRID_EVENT); + grid_.Refresh(); + } + + void expandNode(size_t row) + { + treeDataView_.expandNode(row); + grid_.Refresh(); //implicitly clears selection (changed row count after expand) + grid_.setGridCursor(row); + //grid_.autoSizeColumns(); -> doesn't look as good as expected + } + + void reduceNode(size_t row) + { + treeDataView_.reduceNode(row); + grid_.Refresh(); + grid_.setGridCursor(row); + } + + TreeView treeDataView_; + + const wxBitmap fileIcon_ = IconBuffer::genericFileIcon(IconBuffer::SIZE_SMALL); + const wxBitmap dirIcon_ = IconBuffer::genericDirIcon (IconBuffer::SIZE_SMALL); + + const wxBitmap rootBmp_; + Opt renderBuf_; //avoid costs of recreating this temporary variable + const int widthNodeIcon_; + const int widthLevelStep_; + const int widthNodeStatus_; + Grid& grid_; + bool showPercentBar_ = true; +}; +} + + +void zen::treegrid::init(Grid& grid) +{ + grid.setDataProvider(std::make_shared(grid)); + grid.showRowLabel(false); + + const int rowHeight = std::max(IconBuffer::getSize(IconBuffer::SIZE_SMALL), grid.getMainWin().GetCharHeight()) + 2; //allow 1 pixel space on top and bottom; dearly needed on OS X! + grid.setRowHeight(rowHeight); +} + + +TreeView& zen::treegrid::getDataView(Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getDataView(); + + throw std::runtime_error("treegrid was not initialized! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); +} + + +void zen::treegrid::setShowPercentage(Grid& grid, bool value) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + prov->setShowPercentage(value); + else + assert(false); +} + + +bool zen::treegrid::getShowPercentage(const Grid& grid) +{ + if (auto* prov = dynamic_cast(grid.getDataProvider())) + return prov->getShowPercentage(); + assert(false); + return true; +} diff --git a/FreeFileSync/Source/ui/tree_grid.h b/FreeFileSync/Source/ui/tree_grid.h new file mode 100755 index 00000000..0d69e820 --- /dev/null +++ b/FreeFileSync/Source/ui/tree_grid.h @@ -0,0 +1,185 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef TREE_VIEW_H_841703190201835280256673425 +#define TREE_VIEW_H_841703190201835280256673425 + +#include +#include +#include +#include "tree_grid_attr.h" +#include "../file_hierarchy.h" + + +namespace zen +{ +//tree view of FolderComparison +class TreeView +{ +public: + TreeView() {} + + void setData(FolderComparison& newData); //set data, taking (partial) ownership + + //apply view filter: comparison results + void updateCmpResult(bool showExcluded, + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive); + + //apply view filter: synchronization preview + void updateSyncPreview(bool showExcluded, + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive); + + enum NodeStatus + { + STATUS_EXPANDED, + STATUS_REDUCED, + STATUS_EMPTY + }; + + //--------------------------------------------------------------------- + struct Node + { + Node(int percent, uint64_t bytes, int itemCount, unsigned int level, NodeStatus status) : + percent_(percent), bytes_(bytes), itemCount_(itemCount), level_(level), status_(status) {} + virtual ~Node() {} + + const int percent_; //[0, 100] + const uint64_t bytes_; + const int itemCount_; + const unsigned int level_; + const NodeStatus status_; + }; + + struct FilesNode : public Node + { + FilesNode(int percent, uint64_t bytes, int itemCount, unsigned int level, const std::vector& filesAndLinks) : + Node(percent, bytes, itemCount, level, STATUS_EMPTY), filesAndLinks_(filesAndLinks) {} + + std::vector filesAndLinks_; //files and symlinks matching view filter; pointers are bound! + }; + + struct DirNode : public Node + { + DirNode(int percent, uint64_t bytes, int itemCount, unsigned int level, NodeStatus status, FolderPair& folder) : Node(percent, bytes, itemCount, level, status), folder_(folder) {} + FolderPair& folder_; + }; + + struct RootNode : public Node + { + RootNode(int percent, uint64_t bytes, int itemCount, NodeStatus status, BaseFolderPair& baseFolder, const Zstring& displayName) : + Node(percent, bytes, itemCount, 0, status), baseFolder_(baseFolder), displayName_(displayName) {} + + BaseFolderPair& baseFolder_; + const Zstring displayName_; + }; + + std::unique_ptr getLine(size_t row) const; //return nullptr on error + size_t linesTotal() const { return flatTree_.size(); } + + void expandNode(size_t row); + void reduceNode(size_t row); + NodeStatus getStatus(size_t row) const; + ptrdiff_t getParent(size_t row) const; //return < 0 if none + + void setSortDirection(ColumnTypeTree colType, bool ascending); //apply permanently! + std::pair getSortDirection() { return std::make_pair(sortColumn_, sortAscending_); } + +private: + struct DirNodeImpl; + + struct Container + { + uint64_t bytesGross = 0; + uint64_t bytesNet = 0; //bytes for files on view in this directory only + int itemCountGross = 0; + int itemCountNet = 0; //number of files on view for in this directory only + + std::vector subDirs; + FileSystemObject::ObjectId firstFileId = nullptr; //weak pointer to first FilePair or SymlinkPair + //- "compress" algorithm may hide file nodes for directories with a single included file, i.e. itemCountGross == itemCountNet == 1 + //- a ContainerObject* would be a better fit, but we need weak pointer semantics! + //- a std::vector would be a better design, but we don't want a second memory structure as large as custom grid! + }; + + struct DirNodeImpl : public Container + { + FileSystemObject::ObjectId objId = nullptr; //weak pointer to FolderPair + }; + + struct RootNodeImpl : public Container + { + std::shared_ptr baseFolder; + Zstring displayName; + }; + + enum NodeType + { + TYPE_ROOT, //-> RootNodeImpl + TYPE_DIRECTORY, //-> DirNodeImpl + TYPE_FILES //-> Container + }; + + struct TreeLine + { + unsigned int level = 0; + int percent = 0; //[0, 100] + const Container* node = nullptr; // + NodeType type = NodeType::TYPE_ROOT; //we increase size of "flatTree" using C-style types rather than have a polymorphic "folderCmpView" + }; + + static void compressNode(Container& cont); + template + static void extractVisibleSubtree(ContainerObject& hierObj, Container& cont, Function includeObject); + void getChildren(const Container& cont, unsigned int level, std::vector& output); + template void updateView(Predicate pred); + void applySubView(std::vector&& newView); + + template static void sortSingleLevel(std::vector& items, ColumnTypeTree columnType); + template struct LessShortName; + + std::vector flatTree_; //collapsable/expandable sub-tree of folderCmpView -> always sorted! + /* /|\ + | (update...) + | */ + std::vector folderCmpView_; //partial view on folderCmp -> unsorted (cannot be, because files are not a separate entity) + std::function lastViewFilterPred_; //buffer view filter predicate for lazy evaluation of files/symlinks corresponding to a TYPE_FILES node + /* /|\ + | (update...) + | */ + std::vector> folderCmp_; //full raw data + + ColumnTypeTree sortColumn_ = treeGridLastSortColumnDefault; + bool sortAscending_ = getDefaultSortDirection(treeGridLastSortColumnDefault); +}; + + +Zstring getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR); + +namespace treegrid +{ +void init(Grid& grid); +TreeView& getDataView(Grid& grid); + +void setShowPercentage(Grid& grid, bool value); +bool getShowPercentage(const Grid& grid); +} +} + +#endif //TREE_VIEW_H_841703190201835280256673425 diff --git a/FreeFileSync/Source/ui/tree_grid_attr.h b/FreeFileSync/Source/ui/tree_grid_attr.h new file mode 100755 index 00000000..0c5da30e --- /dev/null +++ b/FreeFileSync/Source/ui/tree_grid_attr.h @@ -0,0 +1,63 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef TREE_GRID_ATTR_H_83470918473021745 +#define TREE_GRID_ATTR_H_83470918473021745 + +#include +#include + + +namespace zen +{ +enum class ColumnTypeTree +{ + FOLDER_NAME, + ITEM_COUNT, + BYTES, +}; + +struct ColAttributesTree +{ + ColumnTypeTree type = ColumnTypeTree::FOLDER_NAME; + int offset = 0; + int stretch = 0; + bool visible = false; +}; + + +inline +std::vector getTreeGridDefaultColAttribs() +{ + return //harmonize with tree_view.cpp::onGridLabelContext() => expects stretched FOLDER_NAME and non-stretched other columns! + { + { ColumnTypeTree::FOLDER_NAME, -120, 1, true }, //stretch to full width and substract sum of fixed size widths + { ColumnTypeTree::ITEM_COUNT, 60, 0, true }, + { ColumnTypeTree::BYTES, 60, 0, true }, //GTK needs a few pixels more width + }; +} + +const bool treeGridShowPercentageDefault = true; +const ColumnTypeTree treeGridLastSortColumnDefault = ColumnTypeTree::BYTES; + +inline +bool getDefaultSortDirection(ColumnTypeTree colType) +{ + switch (colType) + { + case ColumnTypeTree::FOLDER_NAME: + return true; + case ColumnTypeTree::ITEM_COUNT: + return false; + case ColumnTypeTree::BYTES: + return false; + } + assert(false); + return true; +} +} + +#endif //TREE_GRID_ATTR_H_83470918473021745 diff --git a/FreeFileSync/Source/ui/tree_view.cpp b/FreeFileSync/Source/ui/tree_view.cpp deleted file mode 100755 index 19f80f4d..00000000 --- a/FreeFileSync/Source/ui/tree_view.cpp +++ /dev/null @@ -1,1338 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#include -#include "tree_view.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../lib/icon_buffer.h" - -using namespace zen; - - -namespace -{ -const int WIDTH_PERCENTAGE_BAR = 60; -} - - -inline -void TreeView::compressNode(Container& cont) //remove single-element sub-trees -> gain clarity + usability (call *after* inclusion check!!!) -{ - if (cont.subDirs.empty()) //single files node - cont.firstFileId = nullptr; - -#if 0 //let's not go overboard: empty folders should not be condensed => used for file exclusion filter; user expects to see them - if (cont.firstFileId == nullptr && //single dir node... - cont.subDirs.size() == 1 && // - cont.subDirs[0].firstFileId == nullptr && //...that is empty - cont.subDirs[0].subDirs.empty()) // - cont.subDirs.clear(); -#endif -} - - -template //(const FileSystemObject&) -> bool -void TreeView::extractVisibleSubtree(ContainerObject& hierObj, //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(); - // case SyncDirection::RIGHT: - // return file.getFileSize(); - // case SyncDirection::NONE: - // break; - // } - - //prefer file-browser semantics over sync preview (=> always show useful numbers, even for SyncDirection::NONE) - //discussion: https://www.freefilesync.org/forum/viewtopic.php?t=1595 - return std::max(file.getFileSize(), file.getFileSize()); - }; - - cont.firstFileId = nullptr; - for (FilePair& file : hierObj.refSubFiles()) - if (pred(file)) - { - cont.bytesNet += getBytes(file); - ++cont.itemCountNet; - - if (!cont.firstFileId) - cont.firstFileId = file.getId(); - } - - for (SymlinkPair& symlink : hierObj.refSubLinks()) - if (pred(symlink)) - { - ++cont.itemCountNet; - - if (!cont.firstFileId) - cont.firstFileId = symlink.getId(); - } - - cont.bytesGross += cont.bytesNet; - cont.itemCountGross += cont.itemCountNet; - - cont.subDirs.reserve(hierObj.refSubFolders().size()); //avoid expensive reallocations! - - for (FolderPair& folder : hierObj.refSubFolders()) - { - const bool included = pred(folder); - - cont.subDirs.emplace_back(); // - auto& subDirCont = cont.subDirs.back(); - TreeView::extractVisibleSubtree(folder, subDirCont, pred); - if (included) - ++subDirCont.itemCountGross; - - cont.bytesGross += subDirCont.bytesGross; - cont.itemCountGross += subDirCont.itemCountGross; - - if (!included && !subDirCont.firstFileId && subDirCont.subDirs.empty()) - cont.subDirs.pop_back(); - else - { - subDirCont.objId = folder.getId(); - compressNode(subDirCont); - } - } -} - - -namespace -{ -//generate nice percentage numbers which precisely sum up to 100 -void calcPercentage(std::vector>& workList) -{ - const uint64_t total = std::accumulate(workList.begin(), workList.end(), uint64_t(), - [](uint64_t sum, const std::pair& pair) { return sum + pair.first; }); - - if (total == 0U) //this case doesn't work with the error minimizing algorithm below - { - for (auto& pair : workList) - *pair.second = 0; - return; - } - - int remainingPercent = 100; - for (auto& pair : workList) - { - *pair.second = static_cast(pair.first * 100U / total); //round down - remainingPercent -= *pair.second; - } - assert(remainingPercent >= 0); - assert(remainingPercent < static_cast(workList.size())); - - //distribute remaining percent so that overall error is minimized as much as possible: - remainingPercent = std::min(remainingPercent, static_cast(workList.size())); - if (remainingPercent > 0) - { - std::nth_element(workList.begin(), workList.begin() + remainingPercent - 1, workList.end(), - [total](const std::pair& lhs, const std::pair& rhs) - { - return lhs.first * 100U % total > rhs.first * 100U % total; - }); - - std::for_each(workList.begin(), workList.begin() + remainingPercent, [&](std::pair& pair) { ++*pair.second; }); - } -} -} - - -Zstring zen::getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR) -{ - Zstring commonTrail; - AbstractPath tmpPathL = itemPathL; - AbstractPath tmpPathR = itemPathR; - for (;;) - { - Opt parentPathL = AFS::getParentFolderPath(tmpPathL); - Opt parentPathR = AFS::getParentFolderPath(tmpPathR); - if (!parentPathL || !parentPathR) - break; - - const Zstring itemNameL = AFS::getItemName(tmpPathL); - const Zstring itemNameR = AFS::getItemName(tmpPathR); - if (!strEqual(itemNameL, itemNameR, CmpNaturalSort())) //let's compare case-insensitively even on Linux! - break; - - tmpPathL = *parentPathL; - tmpPathR = *parentPathR; - - commonTrail = AFS::appendPaths(itemNameL, commonTrail, FILE_NAME_SEPARATOR); - } - if (!commonTrail.empty()) - return commonTrail; - - auto getLastComponent = [](const AbstractPath& itemPath) - { - if (!AFS::getParentFolderPath(itemPath)) //= device root - return utfTo(AFS::getDisplayPath(itemPath)); - return AFS::getItemName(itemPath); - }; - - if (AFS::isNullPath(itemPathL)) - return getLastComponent(itemPathR); - else if (AFS::isNullPath(itemPathR)) - return getLastComponent(itemPathL); - else - return getLastComponent(itemPathL) + utfTo(SPACED_DASH) + - getLastComponent(itemPathR); -} - - -template -struct TreeView::LessShortName -{ - bool operator()(const TreeLine& lhs, const TreeLine& rhs) const - { - //files last (irrespective of sort direction) - if (lhs.type_ == TreeView::TYPE_FILES) - return false; - else if (rhs.type_ == TreeView::TYPE_FILES) - return true; - - if (lhs.type_ != rhs.type_) // - return lhs.type_ < rhs.type_; //shouldn't happen! root nodes not mixed with files or directories - - switch (lhs.type_) - { - case TreeView::TYPE_ROOT: - return makeSortDirection(LessNaturalSort() /*even on Linux*/, Int2Type())(static_cast(lhs.node_)->displayName, - static_cast(rhs.node_)->displayName); - - case TreeView::TYPE_DIRECTORY: - { - const auto* folderL = dynamic_cast(FileSystemObject::retrieve(static_cast(lhs.node_)->objId)); - const auto* folderR = dynamic_cast(FileSystemObject::retrieve(static_cast(rhs.node_)->objId)); - - if (!folderL) //might be pathologic, but it's covered - return false; - else if (!folderR) - return true; - - return makeSortDirection(LessNaturalSort() /*even on Linux*/, Int2Type())(folderL->getPairItemName(), folderR->getPairItemName()); - } - - case TreeView::TYPE_FILES: - break; - } - assert(false); - return false; //:= all equal - } -}; - - -template -void TreeView::sortSingleLevel(std::vector& items, ColumnTypeNavi columnType) -{ - auto getBytes = [](const TreeLine& line) -> uint64_t - { - switch (line.type_) - { - case TreeView::TYPE_ROOT: - case TreeView::TYPE_DIRECTORY: - return line.node_->bytesGross; - case TreeView::TYPE_FILES: - return line.node_->bytesNet; - } - assert(false); - return 0U; - }; - - auto getCount = [](const TreeLine& line) -> int - { - switch (line.type_) - { - case TreeView::TYPE_ROOT: - case TreeView::TYPE_DIRECTORY: - return line.node_->itemCountGross; - - case TreeView::TYPE_FILES: - return line.node_->itemCountNet; - } - assert(false); - return 0; - }; - - const auto lessBytes = [&](const TreeLine& lhs, const TreeLine& rhs) { return getBytes(lhs) < getBytes(rhs); }; - const auto lessCount = [&](const TreeLine& lhs, const TreeLine& rhs) { return getCount(lhs) < getCount(rhs); }; - - switch (columnType) - { - case ColumnTypeNavi::FOLDER_NAME: - std::sort(items.begin(), items.end(), LessShortName()); - break; - case ColumnTypeNavi::ITEM_COUNT: - std::sort(items.begin(), items.end(), makeSortDirection(lessCount, Int2Type())); - break; - case ColumnTypeNavi::BYTES: - std::sort(items.begin(), items.end(), makeSortDirection(lessBytes, Int2Type())); - break; - } -} - - -void TreeView::getChildren(const Container& cont, unsigned int level, std::vector& output) -{ - output.clear(); - output.reserve(cont.subDirs.size() + 1); //keep pointers in "workList" valid - std::vector> workList; - - for (const DirNodeImpl& subDir : cont.subDirs) - { - output.emplace_back(level, 0, &subDir, TreeView::TYPE_DIRECTORY); - workList.emplace_back(subDir.bytesGross, &output.back().percent_); - } - - if (cont.firstFileId) - { - output.emplace_back(level, 0, &cont, TreeView::TYPE_FILES); - workList.emplace_back(cont.bytesNet, &output.back().percent_); - } - calcPercentage(workList); - - if (sortAscending) - sortSingleLevel(output, sortColumn); - else - sortSingleLevel(output, sortColumn); -} - - -void TreeView::applySubView(std::vector&& newView) -{ - //preserve current node expansion status - auto getHierAlias = [](const TreeView::TreeLine& tl) -> const ContainerObject* - { - switch (tl.type_) - { - case TreeView::TYPE_ROOT: - return static_cast(tl.node_)->baseFolder.get(); - - case TreeView::TYPE_DIRECTORY: - if (auto folder = dynamic_cast(FileSystemObject::retrieve(static_cast(tl.node_)->objId))) - return folder; - break; - - case TreeView::TYPE_FILES: - break; //none!!! - } - return nullptr; - }; - - std::unordered_set expandedNodes; - if (!flatTree.empty()) - { - auto it = flatTree.begin(); - for (auto iterNext = flatTree.begin() + 1; iterNext != flatTree.end(); ++iterNext, ++it) - if (it->level_ < iterNext->level_) - if (auto hierObj = getHierAlias(*it)) - expandedNodes.insert(hierObj); - } - - //update view on full data - folderCmpView.swap(newView); //newView may be an alias for folderCmpView! see sorting! - - //set default flat tree - flatTree.clear(); - - if (folderCmp.size() == 1) //single folder pair case (empty pairs were already removed!) do NOT use folderCmpView for this check! - { - if (!folderCmpView.empty()) //possibly empty! - getChildren(folderCmpView[0], 0, flatTree); //do not show root - } - else - { - //following is almost identical with TreeView::getChildren(): however we *cannot* reuse code here; - //this were only possible if we replaced "std::vector" with "Container"! - - flatTree.reserve(folderCmpView.size()); //keep pointers in "workList" valid - std::vector> workList; - - for (const RootNodeImpl& root : folderCmpView) - { - flatTree.emplace_back(0, 0, &root, TreeView::TYPE_ROOT); - workList.emplace_back(root.bytesGross, &flatTree.back().percent_); - } - - calcPercentage(workList); - - if (sortAscending) - sortSingleLevel(flatTree, sortColumn); - else - sortSingleLevel(flatTree, sortColumn); - } - - //restore node expansion status - for (size_t row = 0; row < flatTree.size(); ++row) //flatTree size changes during loop! - { - const TreeLine& line = flatTree[row]; - - if (auto hierObj = getHierAlias(line)) - if (expandedNodes.find(hierObj) != expandedNodes.end()) - { - std::vector newLines; - getChildren(*line.node_, line.level_ + 1, newLines); - - flatTree.insert(flatTree.begin() + row + 1, newLines.begin(), newLines.end()); - } - } -} - - -template -void TreeView::updateView(Predicate pred) -{ - //update view on full data - std::vector newView; - newView.reserve(folderCmp.size()); //avoid expensive reallocations! - - for (const std::shared_ptr& baseObj : folderCmp) - { - newView.emplace_back(); - RootNodeImpl& root = newView.back(); - this->extractVisibleSubtree(*baseObj, root, pred); //"this->" is bogus for a static method, but GCC screws this one up - - //warning: the following lines are almost 1:1 copy from extractVisibleSubtree: - //however we *cannot* reuse code here; this were only possible if we replaced "std::vector" with "Container"! - if (!root.firstFileId && root.subDirs.empty()) - newView.pop_back(); - else - { - root.baseFolder = baseObj; - root.displayName = getShortDisplayNameForFolderPair(baseObj->getAbstractPath< LEFT_SIDE>(), - baseObj->getAbstractPath()); - - this->compressNode(root); //"this->" required by two-pass lookup as enforced by GCC 4.7 - } - } - - lastViewFilterPred = pred; - applySubView(std::move(newView)); -} - - -void TreeView::setSortDirection(ColumnTypeNavi colType, bool ascending) //apply permanently! -{ - sortColumn = colType; - sortAscending = ascending; - - //reapply current view - applySubView(std::move(folderCmpView)); -} - - -bool TreeView::getDefaultSortDirection(ColumnTypeNavi colType) -{ - switch (colType) - { - case ColumnTypeNavi::FOLDER_NAME: - return true; - case ColumnTypeNavi::ITEM_COUNT: - return false; - case ColumnTypeNavi::BYTES: - return false; - } - assert(false); - return true; -} - - -TreeView::NodeStatus TreeView::getStatus(size_t row) const -{ - if (row < flatTree.size()) - { - if (row + 1 < flatTree.size() && flatTree[row + 1].level_ > flatTree[row].level_) - return TreeView::STATUS_EXPANDED; - - //it's either reduced or empty - switch (flatTree[row].type_) - { - case TreeView::TYPE_DIRECTORY: - case TreeView::TYPE_ROOT: - return flatTree[row].node_->firstFileId || !flatTree[row].node_->subDirs.empty() ? TreeView::STATUS_REDUCED : TreeView::STATUS_EMPTY; - - case TreeView::TYPE_FILES: - return TreeView::STATUS_EMPTY; - } - } - return TreeView::STATUS_EMPTY; -} - - -void TreeView::expandNode(size_t row) -{ - if (getStatus(row) != TreeView::STATUS_REDUCED) - { - assert(false); - return; - } - - if (row < flatTree.size()) - { - std::vector newLines; - - switch (flatTree[row].type_) - { - case TreeView::TYPE_ROOT: - case TreeView::TYPE_DIRECTORY: - getChildren(*flatTree[row].node_, flatTree[row].level_ + 1, newLines); - break; - case TreeView::TYPE_FILES: - break; - } - flatTree.insert(flatTree.begin() + row + 1, newLines.begin(), newLines.end()); - } -} - - -void TreeView::reduceNode(size_t row) -{ - if (row < flatTree.size()) - { - const unsigned int parentLevel = flatTree[row].level_; - - bool done = false; - flatTree.erase(std::remove_if(flatTree.begin() + row + 1, flatTree.end(), - [&](const TreeLine& line) -> bool - { - if (done) - return false; - if (line.level_ > parentLevel) - return true; - else - { - done = true; - return false; - } - }), flatTree.end()); - } -} - - -ptrdiff_t TreeView::getParent(size_t row) const -{ - if (row < flatTree.size()) - { - const auto level = flatTree[row].level_; - - while (row-- > 0) - if (flatTree[row].level_ < level) - return row; - } - return -1; -} - - -void TreeView::updateCmpResult(bool showExcluded, - bool leftOnlyFilesActive, - bool rightOnlyFilesActive, - bool leftNewerFilesActive, - bool rightNewerFilesActive, - bool differentFilesActive, - bool equalFilesActive, - bool conflictFilesActive) -{ - updateView([showExcluded, //make sure the predicate can be stored safely! - leftOnlyFilesActive, - rightOnlyFilesActive, - leftNewerFilesActive, - rightNewerFilesActive, - differentFilesActive, - equalFilesActive, - conflictFilesActive](const FileSystemObject& fsObj) -> bool - { - if (!fsObj.isActive() && !showExcluded) - return false; - - switch (fsObj.getCategory()) - { - case FILE_LEFT_SIDE_ONLY: - return leftOnlyFilesActive; - case FILE_RIGHT_SIDE_ONLY: - return rightOnlyFilesActive; - case FILE_LEFT_NEWER: - return leftNewerFilesActive; - case FILE_RIGHT_NEWER: - return rightNewerFilesActive; - case FILE_DIFFERENT_CONTENT: - return differentFilesActive; - case FILE_EQUAL: - case FILE_DIFFERENT_METADATA: //= sub-category of equal - return equalFilesActive; - case FILE_CONFLICT: - return conflictFilesActive; - } - assert(false); - return true; - }); -} - - -void TreeView::updateSyncPreview(bool showExcluded, - bool syncCreateLeftActive, - bool syncCreateRightActive, - bool syncDeleteLeftActive, - bool syncDeleteRightActive, - bool syncDirOverwLeftActive, - bool syncDirOverwRightActive, - bool syncDirNoneActive, - bool syncEqualActive, - bool conflictFilesActive) -{ - updateView([showExcluded, //make sure the predicate can be stored safely! - syncCreateLeftActive, - syncCreateRightActive, - syncDeleteLeftActive, - syncDeleteRightActive, - syncDirOverwLeftActive, - syncDirOverwRightActive, - syncDirNoneActive, - syncEqualActive, - conflictFilesActive](const FileSystemObject& fsObj) -> bool - { - if (!fsObj.isActive() && !showExcluded) - return false; - - switch (fsObj.getSyncOperation()) - { - case SO_CREATE_NEW_LEFT: - return syncCreateLeftActive; - case SO_CREATE_NEW_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_MOVE_RIGHT_FROM: - case SO_MOVE_RIGHT_TO: - return syncDirOverwRightActive; - case SO_OVERWRITE_LEFT: - case SO_COPY_METADATA_TO_LEFT: - case SO_MOVE_LEFT_FROM: - case SO_MOVE_LEFT_TO: - return syncDirOverwLeftActive; - case SO_DO_NOTHING: - return syncDirNoneActive; - case SO_EQUAL: - return syncEqualActive; - case SO_UNRESOLVED_CONFLICT: - return conflictFilesActive; - } - assert(false); - return true; - }); -} - - -void TreeView::setData(FolderComparison& newData) -{ - std::vector().swap(flatTree); //free mem - std::vector().swap(folderCmpView); // - folderCmp = newData; - - //remove truly empty folder pairs as early as this: we want to distinguish single/multiple folder pair cases by looking at "folderCmp" - erase_if(folderCmp, [](const std::shared_ptr& baseObj) - { - return AFS::isNullPath(baseObj->getAbstractPath< LEFT_SIDE>()) && - AFS::isNullPath(baseObj->getAbstractPath()); - }); -} - - -std::unique_ptr TreeView::getLine(size_t row) const -{ - if (row < flatTree.size()) - { - const auto level = flatTree[row].level_; - const int percent = flatTree[row].percent_; - - switch (flatTree[row].type_) - { - case TreeView::TYPE_ROOT: - { - const auto& root = *static_cast(flatTree[row].node_); - return std::make_unique(percent, root.bytesGross, root.itemCountGross, getStatus(row), *root.baseFolder, root.displayName); - } - break; - - case TreeView::TYPE_DIRECTORY: - { - const auto* dir = static_cast(flatTree[row].node_); - if (auto folder = dynamic_cast(FileSystemObject::retrieve(dir->objId))) - return std::make_unique(percent, dir->bytesGross, dir->itemCountGross, level, getStatus(row), *folder); - } - break; - - case TreeView::TYPE_FILES: - { - const auto* parentDir = flatTree[row].node_; - if (auto firstFile = FileSystemObject::retrieve(parentDir->firstFileId)) - { - std::vector filesAndLinks; - ContainerObject& parent = firstFile->parent(); - - //lazy evaluation: recheck "lastViewFilterPred" again rather than buffer and bloat "lastViewFilterPred" - for (FileSystemObject& fsObj : parent.refSubFiles()) - if (lastViewFilterPred(fsObj)) - filesAndLinks.push_back(&fsObj); - - for (FileSystemObject& fsObj : parent.refSubLinks()) - if (lastViewFilterPred(fsObj)) - filesAndLinks.push_back(&fsObj); - - return std::make_unique(percent, parentDir->bytesNet, parentDir->itemCountNet, level, filesAndLinks); - } - } - break; - } - } - return nullptr; -} - -//########################################################################################################## - -namespace -{ -//let's NOT create wxWidgets objects statically: -inline wxColor getColorPercentBorder () { return { 198, 198, 198 }; } -inline wxColor getColorPercentBackground() { return { 0xf8, 0xf8, 0xf8 }; } - -inline wxColor getColorTreeSelectionGradientFrom() { return Grid::getColorSelectionGradientFrom(); } -inline wxColor getColorTreeSelectionGradientTo () { return Grid::getColorSelectionGradientTo (); } - -const int iconSizeSmall = IconBuffer::getSize(IconBuffer::SIZE_SMALL); - - -wxColor getColorForLevel(size_t level) -{ - switch (level % 12) - { - case 0: - return { 0xcc, 0xcc, 0xff }; - case 1: - return { 0xcc, 0xff, 0xcc }; - case 2: - return { 0xff, 0xff, 0x99 }; - case 3: - return { 0xcc, 0xcc, 0xcc }; - case 4: - return { 0xff, 0xcc, 0xff }; - case 5: - return { 0x99, 0xff, 0xcc }; - case 6: - return { 0xcc, 0xcc, 0x99 }; - case 7: - return { 0xff, 0xcc, 0xcc }; - case 8: - return { 0xcc, 0xff, 0x99 }; - case 9: - return { 0xff, 0xff, 0xcc }; - case 10: - return { 0xcc, 0xff, 0xff }; - case 11: - return { 0xff, 0xcc, 0x99 }; - } - assert(false); - return *wxBLACK; -} - - -class GridDataNavi : private wxEvtHandler, public GridData -{ -public: - GridDataNavi(Grid& grid, const std::shared_ptr& treeDataView) : treeDataView_(treeDataView), - rootBmp(getResourceImage(L"rootFolder").ConvertToImage().Scale(iconSizeSmall, iconSizeSmall, wxIMAGE_QUALITY_HIGH)), - widthNodeIcon(iconSizeSmall), - widthLevelStep(widthNodeIcon), - widthNodeStatus(getResourceImage(L"node_expanded").GetWidth()), - grid_(grid) - { - grid.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridDataNavi::onKeyDown), nullptr, this); - grid.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler (GridDataNavi::onMouseLeft ), nullptr, this); - grid.Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler (GridDataNavi::onMouseLeftDouble ), nullptr, this); - grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEventHandler(GridDataNavi::onGridLabelContext ), nullptr, this); - grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(GridDataNavi::onGridLabelLeftClick), nullptr, this); - } - - void setShowPercentage(bool value) { showPercentBar = value; grid_.Refresh(); } - bool getShowPercentage() const { return showPercentBar; } - -private: - size_t getRowCount() const override { return treeDataView_ ? treeDataView_->linesTotal() : 0; } - - std::wstring getToolTip(size_t row, ColumnType colType) const override - { - switch (static_cast(colType)) - { - case ColumnTypeNavi::FOLDER_NAME: - if (treeDataView_) - if (std::unique_ptr node = treeDataView_->getLine(row)) - if (const TreeView::RootNode* root = dynamic_cast(node.get())) - { - const std::wstring& dirLeft = AFS::getDisplayPath(root->baseFolder_.getAbstractPath< LEFT_SIDE>()); - const std::wstring& dirRight = AFS::getDisplayPath(root->baseFolder_.getAbstractPath()); - if (dirLeft.empty()) - return dirRight; - else if (dirRight.empty()) - return dirLeft; - return dirLeft + L" \u2013"/*en dash*/ + L"\n" + dirRight; - } - break; - - case ColumnTypeNavi::ITEM_COUNT: - case ColumnTypeNavi::BYTES: - break; - } - return std::wstring(); - } - - std::wstring getValue(size_t row, ColumnType colType) const override - { - if (treeDataView_) - { - if (std::unique_ptr node = treeDataView_->getLine(row)) - switch (static_cast(colType)) - { - case ColumnTypeNavi::FOLDER_NAME: - if (const TreeView::RootNode* root = dynamic_cast(node.get())) - return utfTo(root->displayName_); - else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) - return utfTo(dir->folder_.getPairItemName()); - else if (dynamic_cast(node.get())) - return _("Files"); - break; - - case ColumnTypeNavi::ITEM_COUNT: - return formatNumber(node->itemCount_); - - case ColumnTypeNavi::BYTES: - return formatFilesizeShort(node->bytes_); - } - } - return std::wstring(); - } - - void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override - { - wxRect rectInside = drawColumnLabelBorder(dc, rect); - drawColumnLabelBackground(dc, rectInside, highlighted); - - rectInside.x += COLUMN_GAP_LEFT; - rectInside.width -= COLUMN_GAP_LEFT; - drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); - - if (treeDataView_) //draw sort marker - { - auto sortInfo = treeDataView_->getSortDirection(); - if (colType == static_cast(sortInfo.first)) - { - const wxBitmap& marker = getResourceImage(sortInfo.second ? L"sortAscending" : L"sortDescending"); - drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL); - } - } - } - - static const int GAP_SIZE = 2; - - enum class HoverAreaNavi - { - NODE, - }; - - void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override - { - if (enabled) - { - if (selected) - dc.GradientFillLinear(rect, getColorTreeSelectionGradientFrom(), getColorTreeSelectionGradientTo(), wxEAST); - //ignore focus - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - } - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - } - - void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override - { - //wxRect rectTmp= drawCellBorder(dc, rect); - wxRect rectTmp = rect; - - // Partitioning: - // ________________________________________________________________________________ - // | space | gap | percentage bar | 2 x gap | node status | gap |icon | gap | rest | - // -------------------------------------------------------------------------------- - // -> synchronize renderCell() <-> getBestSize() <-> getRowMouseHover() - - if (static_cast(colType) == ColumnTypeNavi::FOLDER_NAME && treeDataView_) - { - if (std::unique_ptr node = treeDataView_->getLine(row)) - { - ////clear first secion: - //clearArea(dc, wxRect(rect.GetTopLeft(), wxSize( - // node->level_ * widthLevelStep + GAP_SIZE + //width - // (showPercentBar ? WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE : 0) + // - // widthNodeStatus + GAP_SIZE + widthNodeIcon + GAP_SIZE, // - // rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - - //consume space - rectTmp.x += static_cast(node->level_) * widthLevelStep; - rectTmp.width -= static_cast(node->level_) * widthLevelStep; - - rectTmp.x += GAP_SIZE; - rectTmp.width -= GAP_SIZE; - - if (rectTmp.width > 0) - { - //percentage bar - if (showPercentBar) - { - - const wxRect areaPerc(rectTmp.x, rectTmp.y + 2, WIDTH_PERCENTAGE_BAR, rectTmp.height - 4); - { - //clear background - wxDCPenChanger dummy (dc, getColorPercentBorder()); - wxDCBrushChanger dummy2(dc, getColorPercentBackground()); - dc.DrawRectangle(areaPerc); - - //inner area - const wxColor brushCol = getColorForLevel(node->level_); - dc.SetPen (brushCol); - dc.SetBrush(brushCol); - - wxRect areaPercTmp = areaPerc; - areaPercTmp.Deflate(1); //do not include border - areaPercTmp.width = numeric::round(areaPercTmp.width * node->percent_ / 100.0); - dc.DrawRectangle(areaPercTmp); - } - - wxDCTextColourChanger dummy3(dc, *wxBLACK); //accessibility: always set both foreground AND background colors! - drawCellText(dc, areaPerc, numberTo(node->percent_) + L"%", wxALIGN_CENTER); - - rectTmp.x += WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE; - rectTmp.width -= WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE; - } - if (rectTmp.width > 0) - { - //node status - auto drawStatus = [&](const wchar_t* image) - { - const wxBitmap& bmp = getResourceImage(image); - - wxRect rectStat(rectTmp.GetTopLeft(), wxSize(bmp.GetWidth(), bmp.GetHeight())); - rectStat.y += (rectTmp.height - rectStat.height) / 2; - - //clearArea(dc, rectStat, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - clearArea(dc, rectStat, *wxWHITE); //accessibility: always set both foreground AND background colors! - drawBitmapRtlMirror(dc, bmp, rectStat, wxALIGN_CENTER, renderBuf); - }; - - const bool drawMouseHover = static_cast(rowHover) == HoverAreaNavi::NODE; - switch (node->status_) - { - case TreeView::STATUS_EXPANDED: - drawStatus(drawMouseHover ? L"node_expanded_hover" : L"node_expanded"); - break; - case TreeView::STATUS_REDUCED: - drawStatus(drawMouseHover ? L"node_reduced_hover" : L"node_reduced"); - break; - case TreeView::STATUS_EMPTY: - break; - } - - rectTmp.x += widthNodeStatus + GAP_SIZE; - rectTmp.width -= widthNodeStatus + GAP_SIZE; - if (rectTmp.width > 0) - { - wxBitmap nodeIcon; - bool isActive = true; - //icon - if (dynamic_cast(node.get())) - nodeIcon = rootBmp; - else if (auto dir = dynamic_cast(node.get())) - { - nodeIcon = dirIcon; - isActive = dir->folder_.isActive(); - } - else if (dynamic_cast(node.get())) - nodeIcon = fileIcon; - - if (!isActive) - nodeIcon = wxBitmap(nodeIcon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)); //treat all channels equally! - - drawBitmapRtlNoMirror(dc, nodeIcon, rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - - rectTmp.x += widthNodeIcon + GAP_SIZE; - rectTmp.width -= widthNodeIcon + GAP_SIZE; - - if (rectTmp.width > 0) - { - wxDCTextColourChanger dummy(dc); - if (!isActive) - dummy.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); - - drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); - } - } - } - } - } - } - else - { - int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL; - - //have file size and item count right-justified (but don't change for RTL languages) - if ((static_cast(colType) == ColumnTypeNavi::BYTES || - static_cast(colType) == ColumnTypeNavi::ITEM_COUNT) && grid_.GetLayoutDirection() != wxLayout_RightToLeft) - { - rectTmp.width -= 2 * GAP_SIZE; - alignment = wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL; - } - else //left-justified - { - rectTmp.x += 2 * GAP_SIZE; - rectTmp.width -= 2 * GAP_SIZE; - } - - drawCellText(dc, rectTmp, getValue(row, colType), alignment); - } - } - - int getBestSize(wxDC& dc, size_t row, ColumnType colType) override - { - // -> synchronize renderCell() <-> getBestSize() <-> getRowMouseHover() - - if (static_cast(colType) == ColumnTypeNavi::FOLDER_NAME && treeDataView_) - { - if (std::unique_ptr node = treeDataView_->getLine(row)) - return node->level_ * widthLevelStep + GAP_SIZE + (showPercentBar ? WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE : 0) + widthNodeStatus + GAP_SIZE - + widthNodeIcon + GAP_SIZE + dc.GetTextExtent(getValue(row, colType)).GetWidth() + - GAP_SIZE; //additional gap from right - else - return 0; - } - else - return 2 * GAP_SIZE + dc.GetTextExtent(getValue(row, colType)).GetWidth() + - 2 * GAP_SIZE; //include gap from right! - } - - HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override - { - switch (static_cast(colType)) - { - case ColumnTypeNavi::FOLDER_NAME: - if (treeDataView_) - if (std::unique_ptr node = treeDataView_->getLine(row)) - { - const int tolerance = 2; - const int nodeStatusXFirst = -tolerance + static_cast(node->level_) * widthLevelStep + GAP_SIZE + (showPercentBar ? WIDTH_PERCENTAGE_BAR + 2 * GAP_SIZE : 0); - const int nodeStatusXLast = (nodeStatusXFirst + tolerance) + widthNodeStatus + tolerance; - // -> synchronize renderCell() <-> getBestSize() <-> getRowMouseHover() - - if (nodeStatusXFirst <= cellRelativePosX && cellRelativePosX < nodeStatusXLast) - return static_cast(HoverAreaNavi::NODE); - } - break; - - case ColumnTypeNavi::ITEM_COUNT: - case ColumnTypeNavi::BYTES: - break; - } - return HoverArea::NONE; - } - - std::wstring getColumnLabel(ColumnType colType) const override - { - switch (static_cast(colType)) - { - case ColumnTypeNavi::FOLDER_NAME: - return _("Name"); - case ColumnTypeNavi::ITEM_COUNT: - return _("Items"); - case ColumnTypeNavi::BYTES: - return _("Size"); - } - return std::wstring(); - } - - void onMouseLeft(GridClickEvent& event) - { - switch (static_cast(event.hoverArea_)) - { - case HoverAreaNavi::NODE: - if (treeDataView_) - switch (treeDataView_->getStatus(event.row_)) - { - case TreeView::STATUS_EXPANDED: - return reduceNode(event.row_); - case TreeView::STATUS_REDUCED: - return expandNode(event.row_); - case TreeView::STATUS_EMPTY: - break; - } - break; - } - event.Skip(); - } - - void onMouseLeftDouble(GridClickEvent& event) - { - if (treeDataView_) - switch (treeDataView_->getStatus(event.row_)) - { - case TreeView::STATUS_EXPANDED: - return reduceNode(event.row_); - case TreeView::STATUS_REDUCED: - return expandNode(event.row_); - case TreeView::STATUS_EMPTY: - break; - } - event.Skip(); - } - - void onKeyDown(wxKeyEvent& event) - { - int keyCode = event.GetKeyCode(); - if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) - { - if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) - keyCode = WXK_RIGHT; - else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) - keyCode = WXK_LEFT; - } - - const size_t rowCount = grid_.getRowCount(); - if (rowCount == 0) return; - - const size_t row = grid_.getGridCursor(); - if (event.ShiftDown()) - ; - else if (event.ControlDown()) - ; - else - switch (keyCode) - { - case WXK_LEFT: - case WXK_NUMPAD_LEFT: - case WXK_NUMPAD_SUBTRACT: //https://msdn.microsoft.com/en-us/library/ms971323#atg_keyboardshortcuts_windows_shortcut_keys - if (treeDataView_) - switch (treeDataView_->getStatus(row)) - { - case TreeView::STATUS_EXPANDED: - return reduceNode(row); - case TreeView::STATUS_REDUCED: - case TreeView::STATUS_EMPTY: - - const int parentRow = treeDataView_->getParent(row); - if (parentRow >= 0) - grid_.setGridCursor(parentRow); - break; - } - return; //swallow event - - case WXK_RIGHT: - case WXK_NUMPAD_RIGHT: - case WXK_NUMPAD_ADD: - if (treeDataView_) - switch (treeDataView_->getStatus(row)) - { - case TreeView::STATUS_EXPANDED: - grid_.setGridCursor(std::min(rowCount - 1, row + 1)); - break; - case TreeView::STATUS_REDUCED: - return expandNode(row); - case TreeView::STATUS_EMPTY: - break; - } - return; //swallow event - } - - event.Skip(); - } - - void onGridLabelContext(GridLabelClickEvent& event) - { - ContextMenu menu; - - //-------------------------------------------------------------------------------------------------------- - menu.addCheckBox(_("Percentage"), [this] { setShowPercentage(!getShowPercentage()); }, getShowPercentage()); - //-------------------------------------------------------------------------------------------------------- - auto toggleColumn = [&](ColumnType ct) - { - auto colAttr = grid_.getColumnConfig(); - - Grid::ColumnAttribute* caFolderName = nullptr; - Grid::ColumnAttribute* caToggle = nullptr; - - for (Grid::ColumnAttribute& ca : colAttr) - if (ca.type_ == static_cast(ColumnTypeNavi::FOLDER_NAME)) - caFolderName = &ca; - else if (ca.type_ == ct) - caToggle = &ca; - - assert(caFolderName && caFolderName->stretch_ > 0 && caFolderName->visible_); - assert(caToggle && caToggle->stretch_ == 0); - - if (caFolderName && caToggle) - { - caToggle->visible_ = !caToggle->visible_; - - //take width of newly visible column from stretched folder name column - caFolderName->offset_ -= caToggle->visible_ ? caToggle->offset_ : -caToggle->offset_; - - grid_.setColumnConfig(colAttr); - } - }; - - for (const Grid::ColumnAttribute& ca : grid_.getColumnConfig()) - { - menu.addCheckBox(getColumnLabel(ca.type_), [ct = ca.type_, toggleColumn] { toggleColumn(ct); }, - ca.visible_, ca.type_ != static_cast(ColumnTypeNavi::FOLDER_NAME)); //do not allow user to hide file name column! - } - //-------------------------------------------------------------------------------------------------------- - menu.addSeparator(); - - auto setDefaultColumns = [&] - { - setShowPercentage(naviGridShowPercentageDefault); - grid_.setColumnConfig(treeview::convertConfig(getDefaultColumnAttributesNavi())); - }; - menu.addItem(_("&Default"), setDefaultColumns); //'&' -> reuse text from "default" buttons elsewhere - - menu.popup(grid_); - - //event.Skip(); - } - - void onGridLabelLeftClick(GridLabelClickEvent& event) - { - if (treeDataView_) - { - const auto colTypeNavi = static_cast(event.colType_); - bool sortAscending = TreeView::getDefaultSortDirection(colTypeNavi); - - const auto sortInfo = treeDataView_->getSortDirection(); - if (sortInfo.first == colTypeNavi) - sortAscending = !sortInfo.second; - - treeDataView_->setSortDirection(colTypeNavi, sortAscending); - grid_.clearSelection(ALLOW_GRID_EVENT); - grid_.Refresh(); - } - } - - void expandNode(size_t row) - { - treeDataView_->expandNode(row); - grid_.Refresh(); //implicitly clears selection (changed row count after expand) - grid_.setGridCursor(row); - //grid_.autoSizeColumns(); -> doesn't look as good as expected - } - - void reduceNode(size_t row) - { - treeDataView_->reduceNode(row); - grid_.Refresh(); - grid_.setGridCursor(row); - } - - std::shared_ptr treeDataView_; - const wxBitmap fileIcon = IconBuffer::genericFileIcon(IconBuffer::SIZE_SMALL); - const wxBitmap dirIcon = IconBuffer::genericDirIcon (IconBuffer::SIZE_SMALL); - - const wxBitmap rootBmp; - Opt renderBuf; //avoid costs of recreating this temporal variable - const int widthNodeIcon; - const int widthLevelStep; - const int widthNodeStatus; - Grid& grid_; - bool showPercentBar = true; -}; -} - - -void treeview::init(Grid& grid, const std::shared_ptr& treeDataView) -{ - grid.setDataProvider(std::make_shared(grid, treeDataView)); - grid.showRowLabel(false); - - const int rowHeight = std::max(IconBuffer::getSize(IconBuffer::SIZE_SMALL), grid.getMainWin().GetCharHeight()) + 2; //allow 1 pixel space on top and bottom; dearly needed on OS X! - grid.setRowHeight(rowHeight); -} - - -void treeview::setShowPercentage(Grid& grid, bool value) -{ - if (auto* prov = dynamic_cast(grid.getDataProvider())) - prov->setShowPercentage(value); - else - assert(false); -} - - -bool treeview::getShowPercentage(const Grid& grid) -{ - if (auto* prov = dynamic_cast(grid.getDataProvider())) - return prov->getShowPercentage(); - assert(false); - return true; -} - - -namespace -{ -std::vector makeConsistent(const std::vector& attribs) -{ - std::set usedTypes; - - std::vector output; - //remove duplicates - std::copy_if(attribs.begin(), attribs.end(), std::back_inserter(output), - [&](const ColumnAttributeNavi& a) { return usedTypes.insert(a.type_).second; }); - - //make sure each type is existing! - const auto& defAttr = getDefaultColumnAttributesNavi(); - std::copy_if(defAttr.begin(), defAttr.end(), std::back_inserter(output), - [&](const ColumnAttributeNavi& a) { return usedTypes.insert(a.type_).second; }); - - return output; -} -} - -std::vector treeview::convertConfig(const std::vector& attribs) -{ - std::vector output; - for (const ColumnAttributeNavi& ca : makeConsistent(attribs)) - output.emplace_back(static_cast(ca.type_), ca.offset_, ca.stretch_, ca.visible_); - return output; -} - - -std::vector treeview::convertConfig(const std::vector& attribs) -{ - std::vector output; - for (const Grid::ColumnAttribute& ca : attribs) - output.emplace_back(static_cast(ca.type_), ca.offset_, ca.stretch_, ca.visible_); - return makeConsistent(output); -} diff --git a/FreeFileSync/Source/ui/tree_view.h b/FreeFileSync/Source/ui/tree_view.h deleted file mode 100755 index 6ec1a2d8..00000000 --- a/FreeFileSync/Source/ui/tree_view.h +++ /dev/null @@ -1,191 +0,0 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#ifndef TREE_VIEW_H_841703190201835280256673425 -#define TREE_VIEW_H_841703190201835280256673425 - -#include -#include -#include -#include "column_attr.h" -#include "../file_hierarchy.h" - - -namespace zen -{ -//tree view of FolderComparison -class TreeView -{ -public: - TreeView() {} - - void setData(FolderComparison& newData); //set data, taking (partial) ownership - - //apply view filter: comparison results - void updateCmpResult(bool showExcluded, - bool leftOnlyFilesActive, - bool rightOnlyFilesActive, - bool leftNewerFilesActive, - bool rightNewerFilesActive, - bool differentFilesActive, - bool equalFilesActive, - bool conflictFilesActive); - - //apply view filter: synchronization preview - void updateSyncPreview(bool showExcluded, - bool syncCreateLeftActive, - bool syncCreateRightActive, - bool syncDeleteLeftActive, - bool syncDeleteRightActive, - bool syncDirOverwLeftActive, - bool syncDirOverwRightActive, - bool syncDirNoneActive, - bool syncEqualActive, - bool conflictFilesActive); - - enum NodeStatus - { - STATUS_EXPANDED, - STATUS_REDUCED, - STATUS_EMPTY - }; - - //--------------------------------------------------------------------- - struct Node - { - Node(int percent, uint64_t bytes, int itemCount, unsigned int level, NodeStatus status) : - percent_(percent), level_(level), status_(status), bytes_(bytes), itemCount_(itemCount) {} - virtual ~Node() {} - - const int percent_; //[0, 100] - const unsigned int level_; - const NodeStatus status_; - const uint64_t bytes_; - const int itemCount_; - }; - - struct FilesNode : public Node - { - FilesNode(int percent, uint64_t bytes, int itemCount, unsigned int level, const std::vector& filesAndLinks) : - Node(percent, bytes, itemCount, level, STATUS_EMPTY), filesAndLinks_(filesAndLinks) {} - - std::vector filesAndLinks_; //files and symlinks matching view filter; pointers are bound! - }; - - struct DirNode : public Node - { - DirNode(int percent, uint64_t bytes, int itemCount, unsigned int level, NodeStatus status, FolderPair& folder) : Node(percent, bytes, itemCount, level, status), folder_(folder) {} - FolderPair& folder_; - }; - - struct RootNode : public Node - { - RootNode(int percent, uint64_t bytes, int itemCount, NodeStatus status, BaseFolderPair& baseFolder, const Zstring& displayName) : - Node(percent, bytes, itemCount, 0, status), baseFolder_(baseFolder), displayName_(displayName) {} - - BaseFolderPair& baseFolder_; - const Zstring displayName_; - }; - - std::unique_ptr getLine(size_t row) const; //return nullptr on error - size_t linesTotal() const { return flatTree.size(); } - - void expandNode(size_t row); - void reduceNode(size_t row); - NodeStatus getStatus(size_t row) const; - ptrdiff_t getParent(size_t row) const; //return < 0 if none - - void setSortDirection(ColumnTypeNavi colType, bool ascending); //apply permanently! - std::pair getSortDirection() { return std::make_pair(sortColumn, sortAscending); } - static bool getDefaultSortDirection(ColumnTypeNavi colType); //ascending? - -private: - struct DirNodeImpl; - - struct Container - { - uint64_t bytesGross = 0; - uint64_t bytesNet = 0; //bytes for files on view in this directory only - int itemCountGross = 0; - int itemCountNet = 0; //number of files on view for in this directory only - - std::vector subDirs; - FileSystemObject::ObjectId firstFileId = nullptr; //weak pointer to first FilePair or SymlinkPair - //- "compress" algorithm may hide file nodes for directories with a single included file, i.e. itemCountGross == itemCountNet == 1 - //- a ContainerObject* would be a better fit, but we need weak pointer semantics! - //- a std::vector would be a better design, but we don't want a second memory structure as large as custom grid! - }; - - struct DirNodeImpl : public Container - { - FileSystemObject::ObjectId objId = nullptr; //weak pointer to FolderPair - }; - - struct RootNodeImpl : public Container - { - std::shared_ptr baseFolder; - Zstring displayName; - }; - - enum NodeType - { - TYPE_ROOT, //-> RootNodeImpl - TYPE_DIRECTORY, //-> DirNodeImpl - TYPE_FILES //-> Container - }; - - struct TreeLine - { - TreeLine(unsigned int level, int percent, const Container* node, enum NodeType type) : level_(level), percent_(percent), node_(node), type_(type) {} - - unsigned int level_; - int percent_; //[0, 100] - const Container* node_; // - NodeType type_; //we increase size of "flatTree" using C-style types rather than have a polymorphic "folderCmpView" - }; - - static void compressNode(Container& cont); - template - static void extractVisibleSubtree(ContainerObject& hierObj, Container& cont, Function includeObject); - void getChildren(const Container& cont, unsigned int level, std::vector& output); - template void updateView(Predicate pred); - void applySubView(std::vector&& newView); - - template static void sortSingleLevel(std::vector& items, ColumnTypeNavi columnType); - template struct LessShortName; - - std::vector flatTree; //collapsable/expandable sub-tree of folderCmpView -> always sorted! - /* /|\ - | (update...) - | */ - std::vector folderCmpView; //partial view on folderCmp -> unsorted (cannot be, because files are not a separate entity) - std::function lastViewFilterPred; //buffer view filter predicate for lazy evaluation of files/symlinks corresponding to a TYPE_FILES node - /* /|\ - | (update...) - | */ - std::vector> folderCmp; //full raw data - - ColumnTypeNavi sortColumn = naviGridLastSortColumnDefault; - bool sortAscending = naviGridLastSortAscendingDefault; -}; - - -Zstring getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, const AbstractPath& itemPathR); - - -namespace treeview -{ -void init(Grid& grid, const std::shared_ptr& treeDataView); - -void setShowPercentage(Grid& grid, bool value); -bool getShowPercentage(const Grid& grid); - -std::vector convertConfig(const std::vector& attribs); //+ make consistent -std::vector convertConfig(const std::vector& attribs); // -} -} - -#endif //TREE_VIEW_H_841703190201835280256673425 diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 06eb503f..5fc0f704 100755 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace zen { -const char ffsVersion[] = "9.6"; //internal linkage! +const char ffsVersion[] = "9.7"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/wx+/file_drop.cpp b/wx+/file_drop.cpp index 2c0b471e..65d5d861 100755 --- a/wx+/file_drop.cpp +++ b/wx+/file_drop.cpp @@ -7,6 +7,7 @@ #include "file_drop.h" #include #include +#include using namespace zen; diff --git a/wx+/file_drop.h b/wx+/file_drop.h index 9826bf27..ee5393b7 100755 --- a/wx+/file_drop.h +++ b/wx+/file_drop.h @@ -60,6 +60,8 @@ using FileDropEventFunction = void (wxEvtHandler::*)(FileDropEvent&); void setupFileDrop(wxWindow& wnd); + + } #endif //FILE_DROP_H_09457802957842560325626 diff --git a/wx+/focus.h b/wx+/focus.h new file mode 100755 index 00000000..cd99d010 --- /dev/null +++ b/wx+/focus.h @@ -0,0 +1,66 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef FOCUS_1084731021985757843 +#define FOCUS_1084731021985757843 + +#include + + +namespace zen +{ +//pretty much the same like "bool wxWindowBase::IsDescendant(wxWindowBase* child) const" but without the obvious misnomer +inline +bool isComponentOf(const wxWindow* child, const wxWindow* top) +{ + for (const wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) + if (wnd == top) + return true; + return false; +} + + +inline +wxTopLevelWindow* getTopLevelWindow(wxWindow* child) +{ + for (wxWindow* wnd = child; wnd != nullptr; wnd = wnd->GetParent()) + if (auto tlw = dynamic_cast(wnd)) //why does wxWidgets use wxWindows::IsTopLevel() ?? + return tlw; + return nullptr; +} + + +/* +Preserving input focus has to be more clever than: + wxWindow* oldFocus = wxWindow::FindFocus(); + ZEN_ON_SCOPE_EXIT(if (oldFocus) oldFocus->SetFocus()); + +=> wxWindow::SetFocus() internally calls Win32 ::SetFocus, which calls ::SetActiveWindow, which - lord knows why - changes the foreground window to the focus window + even if the user is currently busy using a different app! More curiosity: this foreground focus stealing happens only during the *first* SetFocus() after app start! + It also can be avoided by changing focus back and forth with some other app after start => wxWidgets bug or Win32 feature??? +*/ +struct FocusPreserver +{ + ~FocusPreserver() + { + //wxTopLevelWindow::IsActive() does NOT call Win32 ::GetActiveWindow()! + //Instead it checks if ::GetFocus() is set somewhere inside the top level + //Note: Both Win32 active and focus windows are *thread-local* values, while foreground window is global! https://blogs.msdn.microsoft.com/oldnewthing/20131016-00/?p=2913 + if (oldFocus_) + if (wxTopLevelWindow* topWin = getTopLevelWindow(oldFocus_)) + if (topWin->IsActive()) //Linux/macOS: already behaves just like ::GetForegroundWindow() on Windows! + oldFocus_->SetFocus(); + } + + wxWindow* getFocus() const { return oldFocus_; } + void setFocus(wxWindow* win) { oldFocus_ = win; } + +private: + wxWindow* oldFocus_ = wxWindow::FindFocus(); +}; +} + +#endif //FOCUS_1084731021985757843 diff --git a/wx+/grid.cpp b/wx+/grid.cpp index f048d059..b301bf6b 100755 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -631,12 +631,12 @@ private: for (auto it = absWidths.begin(); it != absWidths.end(); ++it) { const size_t col = it - absWidths.begin(); - const int width = it->width_; //don't use unsigned for calculations! + const int width = it->width; //don't use unsigned for calculations! if (labelAreaTL.x > rect.GetRight()) return; //done, rect is fully covered if (labelAreaTL.x + width > rect.x) - drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_); + drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type); labelAreaTL.x += width; } if (labelAreaTL.x > rect.GetRight()) @@ -647,7 +647,7 @@ private: { int totalWidth = 0; for (const ColumnWidth& cw : absWidths) - totalWidth += cw.width_; + totalWidth += cw.width; const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width if (totalWidth < clientWidth) @@ -894,7 +894,7 @@ private: { int totalRowWidth = 0; for (const ColumnWidth& cw : absWidths) - totalRowWidth += cw.width_; + totalRowWidth += cw.width; //fill gap after columns and cover full width if (fillGapAfterColumns) @@ -922,14 +922,14 @@ private: if (cellAreaTL.x > rect.GetRight()) return; //done - if (cellAreaTL.x + cw.width_ > rect.x) + if (cellAreaTL.x + cw.width > rect.x) for (auto row = rowRange.first; row < rowRange.second; ++row) { - const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight); + const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width, rowHeight); RecursiveDcClipper dummy3(dc, cellRect); - prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row)); + prov->renderCell(dc, cellRect, row, cw.type, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row)); } - cellAreaTL.x += cw.width_; + cellAreaTL.x += cw.width; } } } @@ -993,28 +993,29 @@ private: //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff! GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover); + const MouseSelect mouseSelectBegin{ mouseEvent, false /*complete*/ }; if (row >= 0) if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area! { if (event.ControlDown()) - activeSelection_ = std::make_unique(*this, row, !refParent().isSelected(row), mouseEvent); + activeSelection_ = std::make_unique(*this, row, !refParent().isSelected(row) /*positive*/, mouseEvent); else if (event.ShiftDown()) { - activeSelection_ = std::make_unique(*this, selectionAnchor_, true, mouseEvent); - refParent().clearSelection(ALLOW_GRID_EVENT); + activeSelection_ = std::make_unique(*this, selectionAnchor_, true /*positive*/, mouseEvent); + refParent().clearSelectionImpl(&mouseSelectBegin, ALLOW_GRID_EVENT); } else { - activeSelection_ = std::make_unique(*this, row, true, mouseEvent); - refParent().clearSelection(ALLOW_GRID_EVENT); + activeSelection_ = std::make_unique(*this, row, true /*positive*/, mouseEvent); + refParent().clearSelectionImpl(&mouseSelectBegin, ALLOW_GRID_EVENT); } } - //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary, - //then GridClickEvent and the associated GridRangeSelectEvent one after the other - sendEventNow(mouseEvent); - Refresh(); + + //notify event *after* potential "clearSelection()" above: a client should first receive a GridSelectEvent for clearing the grid, if necessary, + //then GridClickEvent and the associated GridSelectEvent one after the other + sendEventNow(mouseEvent); } event.Skip(); //allow changing focus } @@ -1041,11 +1042,15 @@ private: } //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys - refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from - activeSelection_->getCurrentRow(), //to - activeSelection_->isPositiveSelect(), - &activeSelection_->getFirstClick()); - activeSelection_.reset(); + const ptrdiff_t rowFrom = activeSelection_->getStartRow(); + const ptrdiff_t rowTo = activeSelection_->getCurrentRow(); + const bool positive = activeSelection_->isPositiveSelect(); + const MouseSelect mouseSelect{ activeSelection_->getFirstClick(), true /*complete*/ }; + + activeSelection_.reset(); //release mouse capture *before* sending the event (which might show a modal popup dialog requiring the mouse!!!) + + + refParent().selectRangeAndNotify(rowFrom, rowTo, positive, &mouseSelect); } if (auto prov = refParent().getDataProvider()) @@ -1123,8 +1128,8 @@ private: class MouseSelection : private wxEvtHandler { public: - MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) : - wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick) + MouseSelection(MainWin& wnd, size_t rowStart, bool positive, const GridClickEvent& firstClick) : + wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positive), firstClick_(firstClick) { wnd_.CaptureMouse(); timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this); @@ -1618,28 +1623,42 @@ void Grid::showRowLabel(bool show) } +void Grid::selectRow(size_t row, GridEventPolicy rangeEventPolicy) +{ + selection_.selectRow(row); + mainWin_->Refresh(); + + if (rangeEventPolicy == ALLOW_GRID_EVENT) + { + GridSelectEvent selEvent(row, row + 1, true, nullptr); + if (wxEvtHandler* evtHandler = GetEventHandler()) + evtHandler->ProcessEvent(selEvent); + } +} + + void Grid::selectAllRows(GridEventPolicy rangeEventPolicy) { selection_.selectAll(); mainWin_->Refresh(); - if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction + if (rangeEventPolicy == ALLOW_GRID_EVENT) { - GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr); + GridSelectEvent selEvent(0, getRowCount(), true /*positive*/, nullptr); if (wxEvtHandler* evtHandler = GetEventHandler()) evtHandler->ProcessEvent(selEvent); } } -void Grid::clearSelection(GridEventPolicy rangeEventPolicy) +void Grid::clearSelectionImpl(const MouseSelect* mouseSelect, GridEventPolicy rangeEventPolicy) { selection_.clear(); mainWin_->Refresh(); - if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction + if (rangeEventPolicy == ALLOW_GRID_EVENT) { - GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr); + GridSelectEvent unselectionEvent(0, getRowCount(), false /*positive*/, mouseSelect); if (wxEvtHandler* evtHandler = GetEventHandler()) evtHandler->ProcessEvent(unselectionEvent); } @@ -1648,18 +1667,16 @@ void Grid::clearSelection(GridEventPolicy rangeEventPolicy) void Grid::scrollDelta(int deltaX, int deltaY) { - int scrollPosX = 0; - int scrollPosY = 0; - GetViewStart(&scrollPosX, &scrollPosY); + wxPoint scrollPos = GetViewStart(); - scrollPosX += deltaX; - scrollPosY += deltaY; + scrollPos.x += deltaX; + scrollPos.y += deltaY; - scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"! - scrollPosY = std::max(0, scrollPosY); // + scrollPos.x = std::max(0, scrollPos.x); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"! + scrollPos.y = std::max(0, scrollPos.y); // - Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()! - updateWindowSizes(); //may show horizontal scroll bar + Scroll(scrollPos); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider } @@ -1704,17 +1721,19 @@ void Grid::setRowHeight(int height) } -void Grid::setColumnConfig(const std::vector& attr) +void Grid::setColumnConfig(const std::vector& attr) { //hold ownership of non-visible columns oldColAttributes_ = attr; std::vector visCols; - for (const ColumnAttribute& ca : attr) + for (const ColAttributes& ca : attr) { - assert(ca.type_ != ColumnType::NONE); - if (ca.visible_) - visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_); + assert(ca.stretch >= 0); + assert(ca.type != ColumnType::NONE); + + if (ca.visible) + visCols.push_back({ ca.type, ca.offset, std::max(ca.stretch, 0) }); } //"ownership" of visible columns is now within Grid @@ -1725,23 +1744,23 @@ void Grid::setColumnConfig(const std::vector& attr) } -std::vector Grid::getColumnConfig() const +std::vector Grid::getColumnConfig() const { //get non-visible columns (+ outdated visible ones) - std::vector output = oldColAttributes_; + std::vector output = oldColAttributes_; auto iterVcols = visibleCols_.begin(); auto iterVcolsend = visibleCols_.end(); //update visible columns but keep order of non-visible ones! - for (ColumnAttribute& ca : output) - if (ca.visible_) + for (ColAttributes& ca : output) + if (ca.visible) { if (iterVcols != iterVcolsend) { - ca.type_ = iterVcols->type_; - ca.stretch_ = iterVcols->stretch_; - ca.offset_ = iterVcols->offset_; + ca.type = iterVcols->type; + ca.stretch = iterVcols->stretch; + ca.offset = iterVcols->offset; ++iterVcols; } else @@ -1807,7 +1826,7 @@ Opt Grid::clientPosToColumnAction(const wxPoint& pos) const int accuWidth = 0; for (size_t col = 0; col < absWidths.size(); ++col) { - accuWidth += absWidths[col].width_; + accuWidth += absWidths[col].width; if (std::abs(absPosX - accuWidth) < resizeTolerance) { ColAction out; @@ -1850,7 +1869,7 @@ ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const std::vector absWidths = getColWidths(); //resolve negative/stretched widths for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol) { - const int width = itCol->width_; //beware dreaded unsigned conversions! + const int width = itCol->width; //beware dreaded unsigned conversions! accWidth += width; if (absPosX < accWidth - width / 2) @@ -1863,7 +1882,7 @@ ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const ColumnType Grid::colToType(size_t col) const { if (col < visibleCols_.size()) - return visibleCols_[col].type_; + return visibleCols_[col].type; return ColumnType::NONE; } @@ -1878,9 +1897,9 @@ Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const int accWidth = 0; for (const ColumnWidth& cw : getColWidths()) { - accWidth += cw.width_; + accWidth += cw.width; if (posX < accWidth) - return { cw.type_, posX + cw.width_ - accWidth, cw.width_ }; + return { cw.type, posX + cw.width - accWidth, cw.width }; } } return { ColumnType::NONE, 0, 0 }; @@ -1892,16 +1911,16 @@ wxRect Grid::getColumnLabelArea(ColumnType colType) const std::vector absWidths = getColWidths(); //resolve negative/stretched widths //colType is not unique in general, but *this* function expects it! - assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1); + assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type == colType; }) <= 1); - auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }); + auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type == colType; }); if (itCol != absWidths.end()) { ptrdiff_t posX = 0; for (auto it = absWidths.begin(); it != itCol; ++it) - posX += it->width_; + posX += it->width; - return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_)); + return wxRect(wxPoint(posX, 0), wxSize(itCol->width, colLabelHeight_)); } return wxRect(); } @@ -1928,9 +1947,6 @@ void Grid::setGridCursor(size_t row) selection_.clear(); //clear selection, do NOT fire event selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event - - mainWin_->Refresh(); - rowLabelWin_->Refresh(); //row labels! (Kubuntu) } @@ -1943,9 +1959,6 @@ void Grid::selectWithCursor(ptrdiff_t row) selection_.clear(); //clear selection, do NOT fire event selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event - - mainWin_->Refresh(); - rowLabelWin_->Refresh(); } @@ -1954,43 +1967,45 @@ void Grid::makeRowVisible(size_t row) const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found if (labelRect.height > 0) { - int scrollPosX = 0; - GetViewStart(&scrollPosX, nullptr); - int pixelsPerUnitY = 0; GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); - if (pixelsPerUnitY <= 0) return; - - const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y; - if (clientPosY < 0) - { - const int scrollPosY = labelRect.y / pixelsPerUnitY; - Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()! - updateWindowSizes(); //may show horizontal scroll bar - } - else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight()) + if (pixelsPerUnitY > 0) { - auto execScroll = [&](int clientHeight) - { - const int scrollPosY = std::ceil((labelRect.y - clientHeight + - labelRect.height) / static_cast(pixelsPerUnitY)); - Scroll(scrollPosX, scrollPosY); - updateWindowSizes(); //may show horizontal scroll bar - }; - - const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight(); - execScroll(clientHeightBefore); + const wxPoint scrollPosOld = GetViewStart(); - //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row - const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight(); - if (clientHeightAfter < clientHeightBefore) - execScroll(clientHeightAfter); + const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y; + if (clientPosY < 0) + { + const int scrollPosNewY = labelRect.y / pixelsPerUnitY; + Scroll(scrollPosOld.x, scrollPosNewY); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider + Refresh(); + } + else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight()) + { + auto execScroll = [&](int clientHeight) + { + const int scrollPosNewY = std::ceil((labelRect.y - clientHeight + + labelRect.height) / static_cast(pixelsPerUnitY)); + Scroll(scrollPosOld.x, scrollPosNewY); + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider + Refresh(); + }; + + const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight(); + execScroll(clientHeightBefore); + + //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row + const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight(); + if (clientHeightAfter < clientHeightBefore) + execScroll(clientHeightAfter); + } } } } -void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated) +void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const MouseSelect* mouseSelect) { //sort + convert to half-open range auto rowFirst = std::min(rowFrom, rowTo); @@ -2001,13 +2016,12 @@ void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positiv numeric::clamp(rowLast, 0, rowCount); selection_.selectRange(rowFirst, rowLast, positive); + mainWin_->Refresh(); //notify event - GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated); + GridSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseSelect); if (wxEvtHandler* evtHandler = GetEventHandler()) evtHandler->ProcessEvent(selectionEvent); - - mainWin_->Refresh(); } @@ -2020,15 +2034,13 @@ void Grid::scrollTo(size_t row) GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); if (pixelsPerUnitY > 0) { - const int scrollPosYNew = labelRect.y / pixelsPerUnitY; - int scrollPosXOld = 0; - int scrollPosYOld = 0; - GetViewStart(&scrollPosXOld, &scrollPosYOld); + const int scrollPosNewY = labelRect.y / pixelsPerUnitY; + const wxPoint scrollPosOld = GetViewStart(); - if (scrollPosYOld != scrollPosYNew) //support polling + if (scrollPosOld.y != scrollPosNewY) //support polling { - Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()! - updateWindowSizes(); //may show horizontal scroll bar + Scroll(scrollPosOld.x, scrollPosNewY); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar if row column gets wider Refresh(); } } @@ -2036,6 +2048,15 @@ void Grid::scrollTo(size_t row) } +size_t Grid::getTopRow() const +{ + const wxPoint absPos = CalcUnscrolledPosition(wxPoint(0, 0)); + const ptrdiff_t row = rowLabelWin_->getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range + assert((getRowCount() == 0 && row == 0) || (0 <= row && row < static_cast(getRowCount()))); + return row; +} + + bool Grid::Enable(bool enable) { Refresh(); @@ -2053,7 +2074,7 @@ int Grid::getBestColumnSize(size_t col) const { if (dataView_ && col < visibleCols_.size()) { - const ColumnType type = visibleCols_[col].type_; + const ColumnType type = visibleCols_[col].type; wxClientDC dc(mainWin_); dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render() @@ -2088,7 +2109,7 @@ void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEve //unusual delay when enlarging the column again later width = std::max(width, COLUMN_MIN_WIDTH); - vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset + vcRs.offset = width - stretchedWidths[col]; //width := stretchedWidth + offset //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth! // test case: @@ -2097,12 +2118,12 @@ void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEve //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again //4. now verify that the stretched column is resizing immediately if main window is enlarged again for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) - if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only - visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]); + if (visibleCols_[col2].stretch > 0) //normalize stretched columns only + visibleCols_[col2].offset = std::max(visibleCols_[col2].offset, COLUMN_MIN_WIDTH - stretchedWidths[col2]); if (columnResizeEventPolicy == ALLOW_GRID_EVENT) { - GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_); + GridColumnResizeEvent sizeEvent(vcRs.offset, vcRs.type); if (wxEvtHandler* evtHandler = GetEventHandler()) { if (notifyAsync) @@ -2125,7 +2146,7 @@ void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy) { const int bestWidth = getBestColumnSize(col); //return -1 on error if (bestWidth >= 0) - setColumnWidth(bestWidth, col, columnResizeEventPolicy, true); + setColumnWidth(bestWidth, col, columnResizeEventPolicy, true /*notifyAsync*/); } updateWindowSizes(); Refresh(); @@ -2140,8 +2161,8 @@ std::vector Grid::getColStretchedWidths(int clientWidth) const //final widt int stretchTotal = 0; for (const VisibleColumn& vc : visibleCols_) { - assert(vc.stretch_ >= 0); - stretchTotal += vc.stretch_; + assert(vc.stretch >= 0); + stretchTotal += vc.stretch; } int remainingWidth = clientWidth; @@ -2154,7 +2175,7 @@ std::vector Grid::getColStretchedWidths(int clientWidth) const //final widt { for (const VisibleColumn& vc : visibleCols_) { - const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down! + const int width = clientWidth * vc.stretch / stretchTotal; //rounds down! output.push_back(width); remainingWidth -= width; } @@ -2162,7 +2183,7 @@ std::vector Grid::getColStretchedWidths(int clientWidth) const //final widt //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution if (remainingWidth > 0) for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) - if (visibleCols_[col2].stretch_ > 0) + if (visibleCols_[col2].stretch > 0) { ++output[col2]; if (--remainingWidth == 0) @@ -2189,14 +2210,14 @@ std::vector Grid::getColWidths(int mainWinWidth) const //eval for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) { const auto& vc = visibleCols_[col2]; - int width = stretchedWidths[col2] + vc.offset_; + int width = stretchedWidths[col2] + vc.offset; - if (vc.stretch_ > 0) + if (vc.stretch > 0) width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width else width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration - output.emplace_back(vc.type_, width); + output.push_back({ vc.type, width }); } return output; } @@ -2206,6 +2227,6 @@ int Grid::getColWidthsSum(int mainWinWidth) const { int sum = 0; for (const ColumnWidth& cw : getColWidths(mainWinWidth)) - sum += cw.width_; + sum += cw.width; return sum; } diff --git a/wx+/grid.h b/wx+/grid.h index 14006205..8b916f2f 100755 --- a/wx+/grid.h +++ b/wx+/grid.h @@ -28,7 +28,7 @@ extern const wxEventType EVENT_GRID_MOUSE_LEFT_UP; //generates: GridClickEve extern const wxEventType EVENT_GRID_MOUSE_RIGHT_DOWN; // extern const wxEventType EVENT_GRID_MOUSE_RIGHT_UP; // -extern const wxEventType EVENT_GRID_SELECT_RANGE; //generates: GridRangeSelectEvent +extern const wxEventType EVENT_GRID_SELECT_RANGE; //generates: GridSelectEvent //NOTE: neither first nor second row need to match EVENT_GRID_MOUSE_LEFT_DOWN/EVENT_GRID_MOUSE_LEFT_UP: user holding SHIFT; moving out of window... extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_LEFT; //generates: GridLabelClickEvent @@ -41,54 +41,60 @@ struct GridClickEvent : public wxMouseEvent { GridClickEvent(wxEventType et, const wxMouseEvent& me, ptrdiff_t row, HoverArea hoverArea) : wxMouseEvent(me), row_(row), hoverArea_(hoverArea) { SetEventType(et); } - wxEvent* Clone() const override { return new GridClickEvent(*this); } + GridClickEvent* Clone() const override { return new GridClickEvent(*this); } const ptrdiff_t row_; //-1 for invalid position, >= rowCount if out of range const HoverArea hoverArea_; //may be HoverArea::NONE }; -struct GridRangeSelectEvent : public wxCommandEvent +struct MouseSelect { - GridRangeSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseInitiated) : + GridClickEvent click; + bool complete = false; //false if this is a preliminary "clear range" event for mouse-down, before the actual selection has happened during mouse-up +}; + +struct GridSelectEvent : public wxCommandEvent +{ + GridSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const MouseSelect* mouseSelect) : wxCommandEvent(EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive), - mouseInitiated_(mouseInitiated ? *mouseInitiated : Opt()) { assert(rowFirst <= rowLast); } - wxEvent* Clone() const override { return new GridRangeSelectEvent(*this); } + mouseSelect_(mouseSelect ? *mouseSelect : Opt()) { assert(rowFirst <= rowLast); } + GridSelectEvent* Clone() const override { return new GridSelectEvent(*this); } const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) const size_t rowLast_; const bool positive_; //"false" when clearing selection! - Opt mouseInitiated_; //filled unless selection was performed via keyboard shortcuts or is result of Grid::clearSelection() + const Opt mouseSelect_; //filled unless selection was performed via keyboard shortcuts }; struct GridLabelClickEvent : public wxMouseEvent { GridLabelClickEvent(wxEventType et, const wxMouseEvent& me, ColumnType colType) : wxMouseEvent(me), colType_(colType) { SetEventType(et); } - wxEvent* Clone() const override { return new GridLabelClickEvent(*this); } + GridLabelClickEvent* Clone() const override { return new GridLabelClickEvent(*this); } const ColumnType colType_; //may be ColumnType::NONE }; - struct GridColumnResizeEvent : public wxCommandEvent { GridColumnResizeEvent(int offset, ColumnType colType) : wxCommandEvent(EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {} - wxEvent* Clone() const override { return new GridColumnResizeEvent(*this); } + GridColumnResizeEvent* Clone() const override { return new GridColumnResizeEvent(*this); } const ColumnType colType_; const int offset_; }; using GridClickEventFunction = void (wxEvtHandler::*)(GridClickEvent&); -using GridRangeSelectEventFunction = void (wxEvtHandler::*)(GridRangeSelectEvent&); +using GridSelectEventFunction = void (wxEvtHandler::*)(GridSelectEvent&); using GridLabelClickEventFunction = void (wxEvtHandler::*)(GridLabelClickEvent&); using GridColumnResizeEventFunction = void (wxEvtHandler::*)(GridColumnResizeEvent&); -#define GridClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridClickEventFunction, &func) -#define GridRangeSelectEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridRangeSelectEventFunction, &func) -#define GridLabelClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridLabelClickEventFunction, &func) +#define GridClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridClickEventFunction, &func) +#define GridSelectEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridSelectEventFunction, &func) +#define GridLabelClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridLabelClickEventFunction, &func) #define GridColumnResizeEventHandler(func)(wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridColumnResizeEventFunction, &func) //------------------------------------------------------------------------------------------------------------ + class Grid; @@ -146,19 +152,18 @@ public: void setRowHeight(int height); - struct ColumnAttribute + struct ColAttributes { - ColumnAttribute(ColumnType type, int offset, int stretch, bool visible = true) : type_(type), visible_(visible), stretch_(std::max(stretch, 0)), offset_(offset) { assert(stretch >=0 ); } - ColumnType type_; - bool visible_; + ColumnType type = ColumnType::NONE; //first, client width is partitioned according to all available stretch factors, then "offset_" is added //universal model: a non-stretched column has stretch factor 0 with the "offset" becoming identical to final width! - int stretch_; //>= 0 - int offset_; + int offset = 0; + int stretch = 0; //>= 0 + bool visible = false; }; - void setColumnConfig(const std::vector& attr); //set column count + widths - std::vector getColumnConfig() const; + void setColumnConfig(const std::vector& attr); //set column count + widths + std::vector getColumnConfig() const; void setDataProvider(const std::shared_ptr& dataView) { dataView_ = dataView; } /**/ GridData* getDataProvider() { return dataView_.get(); } @@ -178,8 +183,9 @@ public: void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical); std::vector getSelectedRows() const { return selection_.get(); } - void selectAllRows (GridEventPolicy rangeEventPolicy); - void clearSelection(GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion! + void selectRow(size_t row, GridEventPolicy rangeEventPolicy); + void selectAllRows (GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion! + void clearSelection(GridEventPolicy rangeEventPolicy) { clearSelectionImpl(nullptr /*mouseSelect*/, rangeEventPolicy); } // void scrollDelta(int deltaX, int deltaY); //in scroll units @@ -193,9 +199,9 @@ public: struct ColumnPosInfo { - ColumnType colType; //ColumnType::NONE no column at x position! - int cellRelativePosX; - int colWidth; + ColumnType colType = ColumnType::NONE; //ColumnType::NONE no column at x position! + int cellRelativePosX = 0; + int colWidth = 0; }; ColumnPosInfo getColumnAtPos(int posX) const; //absolute position! @@ -208,6 +214,9 @@ public: size_t getGridCursor() const; //returns row void scrollTo(size_t row); + size_t getTopRow() const; + + void makeRowVisible(size_t row); void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override; bool Enable(bool enable = true) override; @@ -226,7 +235,6 @@ private: void updateWindowSizes(bool updateScrollbar = true); void selectWithCursor(ptrdiff_t row); - void makeRowVisible(size_t row); void redirectRowLabelEvent(wxMouseEvent& event); @@ -247,53 +255,52 @@ private: class Selection { public: - void init(size_t rowCount) { rowSelectionValue.resize(rowCount); clear(); } + void init(size_t rowCount) { selected_.resize(rowCount); clear(); } - size_t maxSize() const { return rowSelectionValue.size(); } + size_t maxSize() const { return selected_.size(); } std::vector get() const { std::vector result; - for (size_t row = 0; row < rowSelectionValue.size(); ++row) - if (rowSelectionValue[row] != 0) + for (size_t row = 0; row < selected_.size(); ++row) + if (selected_[row] != 0) result.push_back(row); return result; } - void selectAll() { selectRange(0, rowSelectionValue.size(), true); } - void clear () { selectRange(0, rowSelectionValue.size(), false); } + void selectRow(size_t row) { selectRange(row, row + 1, true); } + void selectAll () { selectRange(0, selected_.size(), true); } + void clear () { selectRange(0, selected_.size(), false); } - bool isSelected(size_t row) const { return row < rowSelectionValue.size() ? rowSelectionValue[row] != 0 : false; } + bool isSelected(size_t row) const { return row < selected_.size() ? selected_[row] != 0 : false; } void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required! { if (rowFirst <= rowLast) { - numeric::clamp(rowFirst, 0, rowSelectionValue.size()); - numeric::clamp(rowLast, 0, rowSelectionValue.size()); + numeric::clamp(rowFirst, 0, selected_.size()); + numeric::clamp(rowLast, 0, selected_.size()); - std::fill(rowSelectionValue.begin() + rowFirst, rowSelectionValue.begin() + rowLast, positive); + std::fill(selected_.begin() + rowFirst, selected_.begin() + rowLast, positive); } else assert(false); } private: - std::vector rowSelectionValue; //effectively a vector of size "number of rows" + std::vector selected_; //effectively a vector of size "number of rows" }; struct VisibleColumn { - VisibleColumn(ColumnType type, int offset, int stretch) : type_(type), stretch_(stretch), offset_(offset) {} - ColumnType type_; - int stretch_; //>= 0 - int offset_; + ColumnType type = ColumnType::NONE; + int offset = 0; + int stretch = 0; //>= 0 }; struct ColumnWidth { - ColumnWidth(ColumnType type, int width) : type_(type), width_(width) {} - ColumnType type_; - int width_; + ColumnType type = ColumnType::NONE; + int width = 0; }; std::vector getColWidths() const; // std::vector getColWidths(int mainWinWidth) const; //evaluate stretched columns @@ -304,7 +311,7 @@ private: { const auto& widths = getColWidths(); if (col < widths.size()) - return widths[col].width_; + return widths[col].width; return NoValue(); } @@ -312,7 +319,9 @@ private: wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found - void selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated); //select inclusive range [rowFrom, rowTo] + notify event! + void selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const MouseSelect* mouseSelect); //select inclusive range [rowFrom, rowTo] + notify event! + + void clearSelectionImpl(const MouseSelect* mouseSelect, GridEventPolicy rangeEventPolicy); bool isSelected(size_t row) const { return selection_.isSelected(row); } @@ -352,11 +361,53 @@ private: bool allowColumnMove_ = true; bool allowColumnResize_ = true; - std::vector visibleCols_; //individual widths, type and total column count - std::vector oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*! + std::vector visibleCols_; //individual widths, type and total column count + std::vector oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*! size_t rowCountOld_ = 0; //at the time of last Grid::Refresh() }; + +//------------------------------------------------------------------------------------------------------------ + +template +std::vector makeConsistent(const std::vector& attribs, const std::vector& defaults) +{ + using ColTypeReal = decltype(ColAttrReal().type); + std::vector output; + + std::set usedTypes; //remove duplicates + auto appendUnique = [&](const std::vector& attr) + { + std::copy_if(attr.begin(), attr.end(), std::back_inserter(output), + [&](const ColAttrReal& a) { return usedTypes.insert(a.type).second; }); + }; + appendUnique(attribs); + appendUnique(defaults); //make sure each type is existing! + + return output; +} + + +template +std::vector convertColAttributes(const std::vector& attribs, const std::vector& defaults) +{ + std::vector output; + for (const ColAttrReal& ca : makeConsistent(attribs, defaults)) + output.push_back({ static_cast(ca.type), ca.offset, ca.stretch, ca.visible }); + return output; +} + + +template +std::vector convertColAttributes(const std::vector& attribs) +{ + using ColTypeReal = decltype(ColAttrReal().type); + + std::vector output; + for (const Grid::ColAttributes& ca : attribs) + output.push_back({ static_cast(ca.type), ca.offset, ca.stretch, ca.visible }); + return output; +} } #endif //GRID_H_834702134831734869987 diff --git a/wx+/http.cpp b/wx+/http.cpp index dd3cb3bc..fa88bb1d 100755 --- a/wx+/http.cpp +++ b/wx+/http.cpp @@ -77,22 +77,24 @@ public: size_t read(void* buffer, size_t bytesToRead) //throw SysError, X; return "bytesToRead" bytes unless end of stream! { const size_t blockSize = getBlockSize(); - assert(memBuf_.size() <= blockSize); + assert(memBuf_.size() >= blockSize); + assert(bufPos_ <= bufPosEnd_ && bufPosEnd_ <= memBuf_.size()); + char* it = static_cast(buffer); char* const itEnd = it + bytesToRead; for (;;) { - const size_t junkSize = std::min(static_cast(itEnd - it), memBuf_.size()); - std::copy (memBuf_.begin(), memBuf_.begin() + junkSize, it); - memBuf_.erase(memBuf_.begin(), memBuf_.begin() + junkSize); - it += junkSize; + const size_t junkSize = std::min(static_cast(itEnd - it), bufPosEnd_ - bufPos_); + std::memcpy(it, &memBuf_[0] + bufPos_, junkSize); + bufPos_ += junkSize; + it += junkSize; if (it == itEnd) break; //-------------------------------------------------------------------- - memBuf_.resize(blockSize); const size_t bytesRead = tryRead(&memBuf_[0], blockSize); //throw SysError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0 - memBuf_.resize(bytesRead); + bufPos_ = 0; + bufPosEnd_ = bytesRead; if (notifyUnbufferedIO_) notifyUnbufferedIO_(bytesRead); //throw X @@ -137,8 +139,11 @@ private: wxHTTP webAccess_; std::unique_ptr httpStream_; //must be deleted BEFORE webAccess is closed - std::vector memBuf_; const IOCallback notifyUnbufferedIO_; //throw X + + std::vector memBuf_ = std::vector(getBlockSize()); + size_t bufPos_ = 0; //buffered I/O; see file_io.cpp + size_t bufPosEnd_ = 0; // }; diff --git a/zen/file_io.cpp b/zen/file_io.cpp index bb4848c6..1cbd970b 100755 --- a/zen/file_io.cpp +++ b/zen/file_io.cpp @@ -131,8 +131,6 @@ size_t FileInput::tryRead(void* buffer, size_t bytesToRead) //throw FileError, E size_t FileInput::read(void* buffer, size_t bytesToRead) //throw FileError, ErrorFileLocked, X; return "bytesToRead" bytes unless end of stream! { - warn_static("implement PERF_AWESOME_BUFFER program wide for all buffers!?") - /* FFS 8.9-9.5 perf issues on macOS: https://www.freefilesync.org/forum/viewtopic.php?t=4808 app-level buffering is essential to optimize random data sizes; e.g. "export file list": @@ -155,13 +153,12 @@ size_t FileInput::read(void* buffer, size_t bytesToRead) //throw FileError, Erro for (;;) { const size_t junkSize = std::min(static_cast(itEnd - it), bufPosEnd_ - bufPos_); - std::memcpy(it, &memBuf_[bufPos_], junkSize); + std::memcpy(it, &memBuf_[0] + bufPos_ /*caveat: vector debug checks*/, junkSize); bufPos_ += junkSize; it += junkSize; if (it == itEnd) break; - //-------------------------------------------------------------------- const size_t bytesRead = tryRead(&memBuf_[0], blockSize); //throw FileError, ErrorFileLocked; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0 bufPos_ = 0; @@ -263,19 +260,18 @@ void FileOutput::write(const void* buffer, size_t bytesToWrite) //throw FileErro if (memBuf_.size() - bufPos_ < blockSize) //support memBuf_.size() > blockSize to reduce memmove()s, but perf test shows: not really needed! // || bufPos_ == bufPosEnd_) -> not needed while memBuf_.size() == blockSize { - std::memmove(&memBuf_[0], &memBuf_[bufPos_], bufPosEnd_ - bufPos_); + std::memmove(&memBuf_[0], &memBuf_[0] + bufPos_, bufPosEnd_ - bufPos_); bufPosEnd_ -= bufPos_; bufPos_ = 0; } const size_t junkSize = std::min(static_cast(itEnd - it), blockSize - (bufPosEnd_ - bufPos_)); - std::memcpy(&memBuf_[bufPosEnd_], it, junkSize); + std::memcpy(&memBuf_[0] + bufPosEnd_ /*caveat: vector debug checks*/, it, junkSize); bufPosEnd_ += junkSize; it += junkSize; if (it == itEnd) return; - //-------------------------------------------------------------------- const size_t bytesWritten = tryWrite(&memBuf_[bufPos_], blockSize); //throw FileError; may return short! CONTRACT: bytesToWrite > 0 bufPos_ += bytesWritten; diff --git a/zen/zstring.h b/zen/zstring.h index 2a4a549e..b96842b5 100755 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -74,6 +74,7 @@ S ciReplaceCpy(const S& str, const T& oldTerm, const U& newTerm); //common unicode sequences const wchar_t EM_DASH = L'\u2014'; +const wchar_t EN_DASH = L'\u2013'; const wchar_t* const SPACED_DASH = L" \u2013 "; //using 'EN DASH' const wchar_t LTR_MARK = L'\u200E'; //UTF-8: E2 80 8E const wchar_t RTL_MARK = L'\u200F'; //UTF-8: E2 80 8F diff --git a/zenXml/zenxml/cvrt_struc.h b/zenXml/zenxml/cvrt_struc.h index 3a724376..87687929 100755 --- a/zenXml/zenxml/cvrt_struc.h +++ b/zenXml/zenxml/cvrt_struc.h @@ -140,6 +140,7 @@ struct ConvertElement value.insert(value.end(), childVal); else success = false; + //should we support insertion of partially-loaded struct?? } return success; } -- cgit