summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xChangelog.txt18
-rwxr-xr-xFreeFileSync/Build/Help/html/expert-settings.html23
-rwxr-xr-xFreeFileSync/Build/Help/html/external-applications.html7
-rwxr-xr-xFreeFileSync/Build/Help/html/schedule-batch-jobs.html8
-rwxr-xr-xFreeFileSync/Build/Help/html/versioning.html76
-rwxr-xr-xFreeFileSync/Build/Help/images/performance.pngbin3454 -> 5380 bytes
-rwxr-xr-xFreeFileSync/Build/Help/images/setup-batch-job.pngbin14074 -> 14211 bytes
-rwxr-xr-xFreeFileSync/Build/Help/images/versioning.pngbin6105 -> 10599 bytes
-rwxr-xr-xFreeFileSync/Build/Languages/german.lng71
-rwxr-xr-xFreeFileSync/Build/Languages/korean.lng44
-rwxr-xr-xFreeFileSync/Build/Resources.zipbin316800 -> 319245 bytes
-rwxr-xr-xFreeFileSync/Source/Makefile1
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/gui_generated.cpp2
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/gui_generated.h2
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/main_dlg.cpp9
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/monitor.cpp247
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/monitor.h23
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/tray_menu.cpp218
-rwxr-xr-xFreeFileSync/Source/RealTimeSync/tray_menu.h8
-rwxr-xr-xFreeFileSync/Source/base/algorithm.cpp20
-rwxr-xr-xFreeFileSync/Source/base/application.cpp115
-rwxr-xr-xFreeFileSync/Source/base/comparison.cpp4
-rwxr-xr-xFreeFileSync/Source/base/dir_exist_async.h4
-rwxr-xr-xFreeFileSync/Source/base/dir_lock.cpp2
-rwxr-xr-xFreeFileSync/Source/base/generate_logfile.cpp362
-rwxr-xr-xFreeFileSync/Source/base/generate_logfile.h35
-rwxr-xr-xFreeFileSync/Source/base/parallel_scan.cpp6
-rwxr-xr-xFreeFileSync/Source/base/perf_check.h6
-rwxr-xr-xFreeFileSync/Source/base/process_callback.h8
-rwxr-xr-xFreeFileSync/Source/base/process_xml.cpp182
-rwxr-xr-xFreeFileSync/Source/base/process_xml.h41
-rwxr-xr-xFreeFileSync/Source/base/return_codes.h52
-rwxr-xr-xFreeFileSync/Source/base/status_handler.cpp2
-rwxr-xr-xFreeFileSync/Source/base/status_handler.h76
-rwxr-xr-xFreeFileSync/Source/base/synchronization.cpp148
-rwxr-xr-xFreeFileSync/Source/base/versioning.cpp24
-rwxr-xr-xFreeFileSync/Source/base/versioning.h4
-rwxr-xr-xFreeFileSync/Source/fs/abstract.cpp2
-rwxr-xr-xFreeFileSync/Source/fs/abstract.h2
-rwxr-xr-xFreeFileSync/Source/fs/native.cpp5
-rwxr-xr-xFreeFileSync/Source/ui/batch_config.cpp59
-rwxr-xr-xFreeFileSync/Source/ui/batch_status_handler.cpp325
-rwxr-xr-xFreeFileSync/Source/ui/batch_status_handler.h51
-rwxr-xr-xFreeFileSync/Source/ui/cfg_grid.cpp350
-rwxr-xr-xFreeFileSync/Source/ui/cfg_grid.h45
-rwxr-xr-xFreeFileSync/Source/ui/file_grid.cpp41
-rwxr-xr-xFreeFileSync/Source/ui/file_view.cpp4
-rwxr-xr-xFreeFileSync/Source/ui/gui_generated.cpp268
-rwxr-xr-xFreeFileSync/Source/ui/gui_generated.h29
-rwxr-xr-xFreeFileSync/Source/ui/gui_status_handler.cpp386
-rwxr-xr-xFreeFileSync/Source/ui/gui_status_handler.h51
-rwxr-xr-xFreeFileSync/Source/ui/log_panel.cpp566
-rwxr-xr-xFreeFileSync/Source/ui/log_panel.h43
-rwxr-xr-xFreeFileSync/Source/ui/main_dlg.cpp674
-rwxr-xr-xFreeFileSync/Source/ui/main_dlg.h14
-rwxr-xr-xFreeFileSync/Source/ui/progress_indicator.cpp1134
-rwxr-xr-xFreeFileSync/Source/ui/progress_indicator.h18
-rwxr-xr-xFreeFileSync/Source/ui/small_dlgs.cpp46
-rwxr-xr-xFreeFileSync/Source/ui/tree_grid.cpp24
-rwxr-xr-xFreeFileSync/Source/ui/version_check_impl.h2
-rwxr-xr-xFreeFileSync/Source/version/version.h2
-rwxr-xr-xLicense.txt1622
-rwxr-xr-xwx+/async_task.h2
-rwxr-xr-xwx+/graph.h2
-rwxr-xr-xwx+/grid.cpp226
-rwxr-xr-xwx+/grid.h31
-rwxr-xr-xwx+/image_tools.cpp36
-rwxr-xr-xwx+/image_tools.h21
-rwxr-xr-xwx+/popup_dlg_generated.cpp2
-rwxr-xr-xwx+/popup_dlg_generated.h2
-rwxr-xr-xxBRZ/src/xbrz.cpp10
-rwxr-xr-xzen/dir_watcher.h4
-rwxr-xr-xzen/error_log.h38
-rwxr-xr-xzen/process_priority.cpp2
-rwxr-xr-xzen/shell_execute.h7
-rwxr-xr-xzen/string_tools.h61
-rwxr-xr-xzen/zstring.h2
77 files changed, 4561 insertions, 3496 deletions
diff --git a/Changelog.txt b/Changelog.txt
index 3386a0a4..81106189 100755
--- a/Changelog.txt
+++ b/Changelog.txt
@@ -1,3 +1,21 @@
+FreeFileSync 10.3 [2018-08-07]
+------------------------------
+New log panel showing details about the last operation
+Show status of last syncs in configuration panel
+Access log files via the configuration panel
+Allow auto-retry and ignore errors during comparison
+Show folder RealTimeSync is waiting on
+New %logfile_path% macro for "on completion" command
+Show errors and warnings count in log file header
+Fixed crash when resizing panel during comparison
+Fixed folders created hidden when source is a volume root path
+Use steady clock while waiting in RealTimeSync
+Fixed folder access error with Google Drive File Stream
+Open global log folder path via options dialog
+Limit global logs by age instead of size
+Deprecated batch-level log files and LastSyncs.log
+
+
FreeFileSync 10.2 [2018-07-06]
------------------------------
Limit number of file versions by age and count
diff --git a/FreeFileSync/Build/Help/html/expert-settings.html b/FreeFileSync/Build/Help/html/expert-settings.html
index 4620f863..2791c5fb 100755
--- a/FreeFileSync/Build/Help/html/expert-settings.html
+++ b/FreeFileSync/Build/Help/html/expert-settings.html
@@ -13,13 +13,17 @@
FreeFileSync has a number of special-purpose settings that can only be accessed
by manually opening the global configuration file <span class="file-path">GlobalSettings.xml</span>.
Note that this file is read once when FreeFileSync starts and saved again on exit.
- Therefore, you should <b>apply manual changes only while FreeFileSync is not running.</b><br>
- <br>
- To locate this file on Windows, enter <b><span class="command-line">%appdata%\FreeFileSync</span></b> in the Windows Explorer address bar or go to the FreeFileSync
- installation folder if you are using the portable installation.
- On Linux, you can find the file in <b><span class="command-line">~/.FreeFileSync</span></b> for the Launchpad release and in the installation folder for the portable version.
- On macOS, go to <b><span class="command-line">~/Library/Application Support/FreeFileSync</span></b>.
+ Therefore, you should <b>apply manual changes only while FreeFileSync is not running.</b>
+ For the portable FreeFileSync variant the file is found in the installation folder,
+ for local installations go to:
</p>
+
+ <table style="border-spacing:0;">
+ <tr><td>Windows:</span></td> <td><b><span class="command-line">%AppData%\FreeFileSync</span></b></td></tr>
+ <tr><td>Linux:</td> <td>&lt;installation folder&gt;</td></tr>
+ <tr><td>macOS:</td> <td><b><span class="command-line">~/Library/Application Support/FreeFileSync</span></b></td></tr>
+ </table>
+ <br>
<div class="greybox">
<div class="command-line">
@@ -31,7 +35,6 @@
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;<b>RunWithBackgroundPriority</b> Enabled=&quot;false&quot;/&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;<b>LockDirectoriesDuringSync</b> Enabled=&quot;true&quot;/&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;<b>VerifyCopiedFiles</b> Enabled=&quot;false&quot;/&gt;<br>
- &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;<b>LastSyncsLogSizeMax</b> Bytes=&quot;100000&quot;/&gt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;<b>NotificationSound</b> CompareFinished=&quot;ding.wav&quot; SyncFinished=&quot;harp.wav&quot;/&gt;
</div>
</div>
@@ -84,12 +87,6 @@
</p>
<p>
- <b>LastSyncsLogSizeMax:</b><br>
- The progress logs of the most recent synchronizations (for both GUI and batch jobs) are collected automatically in the file <span class="file-path">LastSyncs.log</span>.
- The maximum size of this log file can be set here.
- </p>
-
- <p>
<b>NotificationSound:</b><br>
Select sound files from the FreeFileSync installation directory to be played after comparison or synchronization. Set empty names if no sound should be played.
</p>
diff --git a/FreeFileSync/Build/Help/html/external-applications.html b/FreeFileSync/Build/Help/html/external-applications.html
index 5655ff7c..cfe79d64 100755
--- a/FreeFileSync/Build/Help/html/external-applications.html
+++ b/FreeFileSync/Build/Help/html/external-applications.html
@@ -57,8 +57,11 @@
<h2>Examples:</h2>
<ul>
- <li>Start file content comparison (Diff) tool:<br>
- <div class="command-line">&quot;C:\Program Files (x86)\WinMerge\WinMergeU.exe&quot; &quot;%local_path%&quot; &quot;%local_path2%&quot;</div><br>
+ <li>Start file content comparison tool (WinMerge):<br>
+ <div class="command-line">&quot;C:\Program Files (x86)\WinMerge\WinMergeU.exe&quot; &quot;%local_path%&quot; &quot;%local_path2%&quot;</div>
+ <br>
+ opendiff on macOS (requires Xcode):<br>
+ <div class="command-line">opendiff &quot;%local_path%&quot; &quot;%local_path2%&quot;</div><br>
<li>Show file in Windows Explorer:<br>
<div class="command-line">explorer /select, &quot;%local_path%&quot;</div><br>
diff --git a/FreeFileSync/Build/Help/html/schedule-batch-jobs.html b/FreeFileSync/Build/Help/html/schedule-batch-jobs.html
index c15dcf5a..4a836801 100755
--- a/FreeFileSync/Build/Help/html/schedule-batch-jobs.html
+++ b/FreeFileSync/Build/Help/html/schedule-batch-jobs.html
@@ -31,13 +31,7 @@
<li>If you don't want error or warning messages to stall synchronization when no user is available to respond,
either check <b>Ignore errors</b> or set <b>Cancel</b> to stop the synchronization at the first error.<br>
&nbsp;
-
- <li>If log files are required, enable <b>Save log</b> and enter a folder path.
- If the path is left empty, the logs will be saved under the current user's roaming profile,
- <span class="file-path">%appdata%\FreeFileSync\Logs</span>.<br>
- Additionally, FreeFileSync always stores the result of the last
- synchronization in file <span class="file-path">LastSyncs.log</span> (up to a user-defined size, see <a href="expert-settings.html">Expert Settings</a>).<br>
- &nbsp;
+
<li>Set up the FreeFileSync batch job in your operating system's scheduler:<br>
</ol>
diff --git a/FreeFileSync/Build/Help/html/versioning.html b/FreeFileSync/Build/Help/html/versioning.html
index 4a1d1f99..71ecff49 100755
--- a/FreeFileSync/Build/Help/html/versioning.html
+++ b/FreeFileSync/Build/Help/html/versioning.html
@@ -27,51 +27,57 @@
<br><br>
</p>
- <h2>2. Keep all versions of old files</h2>
- <p>
- Set deletion handling to <b>Versioning</b>
- and naming convention to <b>Time stamp</b>. FreeFileSync will move
- deleted files into the provided folder and add a time stamp to each
- file name. The structure of the synchronized folders is preserved so
- that old versions of a file can be conveniently accessed via a file
- browser.
- </p>
- <p>
- <b>Example:</b>
- A file <span class="file-path">Folder\File.txt</span> was updated three times and old versions were moved to folder <span class="file-path">C:\Revisions</span>
- </p>
-
- <div class="greybox">
- <div class="file-path">
- C:\Revisions\Folder\File.txt <b>2012-12-12 111111</b>.txt<br>
- C:\Revisions\Folder\File.txt <b>2012-12-12 122222</b>.txt<br>
- C:\Revisions\Folder\File.txt <b>2012-12-12 133333</b>.txt
- </div>
- </div>
+ <h2>2. Keep multiple versions of old files</h2>
+ <ol type="A">
+ <li><p>
+ Set deletion handling to <b>Versioning</b>
+ and naming convention to <b>Time stamp [File]</b>. FreeFileSync will move
+ deleted files into the provided folder and add a time stamp to each
+ file name. The structure of the synchronized folders is preserved so
+ that old versions of a file can be conveniently accessed via a file
+ browser.
+ </p>
+ <p><b>Example:</b> Last versions of the file <span class="file-path">Folder\File.txt</span> inside folder <span class="file-path">D:\Revisions</span> </p>
+ <div class="greybox">
+ <div class="file-path">
+ D:\Revisions\Folder\File.txt <b>2012-12-12 111111</b>.txt<br>
+ D:\Revisions\Folder\File.txt <b>2012-12-12 122222</b>.txt<br>
+ D:\Revisions\Folder\File.txt <b>2012-12-12 133333</b>.txt
+ </div>
+ </div>
+ <br>
+
+ <li><p>
+ With naming convention <b>Time stamp [Folder]</b> files are moved into a time-stamped subfolder
+ of the versioning folder while their names remain unchanged.
+ This makes it easy to manually undo a synchronization by moving the deleted files from the
+ versioning folder back to their original folders.
+ </p>
+ <p><b>Example:</b> Last versions of the file <span class="file-path">Folder\File.txt</span> inside folder <span class="file-path">D:\Revisions</span> </p>
+ <div class="greybox">
+ <div class="file-path">
+ D:\Revisions\<b>2012-12-12 111111</b>\Folder\File.txt<br>
+ D:\Revisions\<b>2012-12-12 122222</b>\Folder\File.txt<br>
+ D:\Revisions\<b>2012-12-12 133333</b>\Folder\File.txt
+ </div>
+ </div>
+ </ol>
<br>
-
+
<h2>3. Save versions at certain intervals</h2>
<p>
With naming convention <b>Replace</b>
it is possible to refine the granularity of versions to keep by adding <a href="macros.html">Macros</a>
to the versioning folder path. For example, you can save deleted files
- on a per-sync session basis by adding the <b><span class="command-line">%timestamp%</span></b> macro:
+ on a daily basis by adding the <b><span class="command-line">%date%</span></b> macro:
</p>
-
- <p><b>Example:</b> Using the dynamically generated folder name <span class="file-path">C:\Revisions\%timestamp%</span></p>
-
+ <p><b>Example:</b> Last versions of the file <span class="file-path">Folder\File.txt</span> inside folder <span class="file-path">D:\Revisions\%date%</span> </p>
<div class="greybox">
<div class="file-path">
- C:\Revisions\<b>2012-12-12 111111</b>\Folder\File.txt<br>
- C:\Revisions\<b>2012-12-12 122222</b>\Folder\File.txt<br>
- C:\Revisions\<b>2012-12-12 133333</b>\Folder\File.txt
+ D:\Revisions\<b>2012-12-11</b>\Folder\File.txt<br>
+ D:\Revisions\<b>2012-12-12</b>\Folder\File.txt<br>
+ D:\Revisions\<b>2012-12-13</b>\Folder\File.txt
</div>
</div>
- <p>
- This allows for a simple manual undo by moving the deleted files from the
- last synchronization session back to their original folders. Other
- macros like <b><span class="command-line">%date%</span></b> or <b><span class="command-line">%weekday%</span></b> can be used to reduce the granularity down
- to days and weeks.
- </p>
</body>
</html>
diff --git a/FreeFileSync/Build/Help/images/performance.png b/FreeFileSync/Build/Help/images/performance.png
index 0c189c5c..435762b4 100755
--- a/FreeFileSync/Build/Help/images/performance.png
+++ b/FreeFileSync/Build/Help/images/performance.png
Binary files differ
diff --git a/FreeFileSync/Build/Help/images/setup-batch-job.png b/FreeFileSync/Build/Help/images/setup-batch-job.png
index cc38e85c..4eb8556b 100755
--- a/FreeFileSync/Build/Help/images/setup-batch-job.png
+++ b/FreeFileSync/Build/Help/images/setup-batch-job.png
Binary files differ
diff --git a/FreeFileSync/Build/Help/images/versioning.png b/FreeFileSync/Build/Help/images/versioning.png
index f9fd5b56..9d4260ca 100755
--- a/FreeFileSync/Build/Help/images/versioning.png
+++ b/FreeFileSync/Build/Help/images/versioning.png
Binary files differ
diff --git a/FreeFileSync/Build/Languages/german.lng b/FreeFileSync/Build/Languages/german.lng
index 2cd75c20..9739cf40 100755
--- a/FreeFileSync/Build/Languages/german.lng
+++ b/FreeFileSync/Build/Languages/german.lng
@@ -7,8 +7,14 @@
<plural_definition>n == 1 ? 0 : 1</plural_definition>
</header>
-<source>Defined by context of use</source>
-<target>Durch Nutzungskontext festgelegt</target>
+<source>No log entries</source>
+<target>Keine Protokolleinträge</target>
+
+<source>Remove old log files after x days:</source>
+<target>Alte Protokolldateien nach x Tagen entfernen:</target>
+
+<source>Show &log</source>
+<target>&Protokoll zeigen</target>
<source>Both sides have changed since last synchronization.</source>
<target>Beide Seiten wurden seit der letzten Synchronisation verändert.</target>
@@ -130,6 +136,9 @@
<source>The folders are created automatically when needed.</source>
<target>Die Ordner werden bei Bedarf automatisch erstellt.</target>
+<source>Scanning:</source>
+<target>Suche Dateien:</target>
+
<source>Comparison finished:</source>
<target>Vergleich abgeschlossen:</target>
@@ -334,6 +343,9 @@
<source>Update attributes on right</source>
<target>Aktualisiere Attribute des rechten Elements</target>
+<source>Warning</source>
+<target>Warnung</target>
+
<source>Items processed:</source>
<target>Verarbeitete Elemente:</target>
@@ -343,6 +355,9 @@
<source>Total time:</source>
<target>Gesamtzeit:</target>
+<source>Stopped</source>
+<target>Gestoppt</target>
+
<source>Cleaning up log files:</source>
<target>Bereinige Protokolldateien:</target>
@@ -361,9 +376,6 @@
<pluralform>%x Threads</pluralform>
</target>
-<source>Scanning:</source>
-<target>Suche Dateien:</target>
-
<source>Cannot read directory %x.</source>
<target>Das Verzeichnis %x kann nicht gelesen werden.</target>
@@ -385,6 +397,15 @@
<source>Unable to connect to %x.</source>
<target>Es kann keine Verbindung zu %x aufgebaut werden.</target>
+<source>Completed successfully</source>
+<target>Erfolgreich abgeschlossen</target>
+
+<source>Completed with warnings</source>
+<target>Mit Warnungen abgeschlossen</target>
+
+<source>Completed with errors</source>
+<target>Mit Fehlern abgeschlossen</target>
+
<source>Cannot access the Volume Shadow Copy Service.</source>
<target>Auf den Volumenschattenkopiedienst kann nicht zugegriffen werden.</target>
@@ -764,24 +785,9 @@ Die Befehlszeile wird ausgelöst, wenn:
<source>System: Shut down</source>
<target>System: Herunterfahren</target>
-<source>Stopped</source>
-<target>Gestoppt</target>
-
-<source>Completed with errors</source>
-<target>Mit Fehlern abgeschlossen</target>
-
-<source>Completed with warnings</source>
-<target>Mit Warnungen abgeschlossen</target>
-
-<source>Warning</source>
-<target>Warnung</target>
-
<source>Nothing to synchronize</source>
<target>Es gibt nichts zu synchronisieren</target>
-<source>Completed successfully</source>
-<target>Erfolgreich abgeschlossen</target>
-
<source>Executing command %x</source>
<target>Führe Befehl aus: %x</target>
@@ -831,7 +837,10 @@ Die Befehlszeile wird ausgelöst, wenn:
<target>Name</target>
<source>Last sync</source>
-<target>Letzte Ausführung</target>
+<target>Letzte Synchronisation</target>
+
+<source>Log</source>
+<target>Protokoll</target>
<source>Folder</source>
<target>Ordner</target>
@@ -896,6 +905,9 @@ Die Befehlszeile wird ausgelöst, wenn:
<source>Please select a folder on a local file system, network or an MTP device.</source>
<target>Bitte wählen Sie einen Ordner auf einem lokalen Dateisystem, Netzwerk oder MTP Gerät.</target>
+<source>Defined by context of use</source>
+<target>Durch Nutzungskontext festgelegt</target>
+
<source>Requires FreeFileSync Donation Edition</source>
<target>Benötigt FreeFileSync Spendenversion</target>
@@ -1226,7 +1238,7 @@ Die Befehlszeile wird ausgelöst, wenn:
<target>In das Benachrichtigungsfeld minimieren</target>
<source>When finished:</source>
-<target>Am Ende:</target>
+<target>Wenn fertig:</target>
<source>Auto-close</source>
<target>Automatisch schließen</target>
@@ -1393,6 +1405,12 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert.
<source>Highlight Configurations</source>
<target>Konfigurationen hervorheben</target>
+<source>Info</source>
+<target>Info</target>
+
+<source>Select all</source>
+<target>Alle auswählen</target>
+
<source>&Options</source>
<target>&Optionen</target>
@@ -1630,21 +1648,12 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert.
<source>Comparing content...</source>
<target>Vergleiche Dateiinhalt...</target>
-<source>Info</source>
-<target>Info</target>
-
-<source>Select all</source>
-<target>Alle auswählen</target>
-
<source>&Continue</source>
<target>&Fortfahren</target>
<source>Progress</source>
<target>Fortschritt</target>
-<source>Log</source>
-<target>Protokoll</target>
-
<source>Thank you, %x, for your donation and support!</source>
<target>Danke %x für die Spende und Unterstützung!</target>
diff --git a/FreeFileSync/Build/Languages/korean.lng b/FreeFileSync/Build/Languages/korean.lng
index 0a46b70d..8529fa42 100755
--- a/FreeFileSync/Build/Languages/korean.lng
+++ b/FreeFileSync/Build/Languages/korean.lng
@@ -122,10 +122,13 @@
<target>다음 폴더를 찾을 수 없습니다:</target>
<source>The following folders do not yet exist:</source>
-<target></target>
+<target>다음 폴더는 아직 존재하지 않습니다:</target>
<source>The folders are created automatically when needed.</source>
-<target></target>
+<target>폴더는 필요 시 자동으로 생성됩니다.</target>
+
+<source>Scanning:</source>
+<target>스캔 중:</target>
<source>Comparison finished:</source>
<target>비교 완료:</target>
@@ -339,7 +342,7 @@
<target>전체 시간:</target>
<source>Cleaning up log files:</source>
-<target></target>
+<target>로그 파일 정리 중:</target>
<source>Error parsing file %x, row %y, column %z.</source>
<target>분석 오류 - 파일: %x; 행: %y; 열: %z.</target>
@@ -355,9 +358,6 @@
<pluralform>%x 스레드</pluralform>
</target>
-<source>Scanning:</source>
-<target>스캔 중:</target>
-
<source>Cannot read directory %x.</source>
<target>디렉터리 %x을(를) 읽을 수 없습니다.</target>
@@ -506,10 +506,10 @@
<target>데이터베이스 생성 중...</target>
<source>Searching for excess file versions:</source>
-<target></target>
+<target>초과 파일 버전 검색 중:</target>
<source>Removing excess file versions:</source>
-<target></target>
+<target>초과 파일 버전 제거 중:</target>
<source>Unable to create time stamp for versioning:</source>
<target>버전 관리를 위한 타임 스탬프 생성 불가:</target>
@@ -729,7 +729,7 @@ The command is triggered if:
<target>디렉터리 모니터링 활성화</target>
<source>Waiting until directory is available:</source>
-<target></target>
+<target>디렉토리를 사용할 수 있을 때까지 대기 중:</target>
<source>&Restore</source>
<target>복원(&R)</target>
@@ -887,7 +887,7 @@ The command is triggered if:
<target>로컬 파일 시스템, 네트워크 또는 MTP 장치에서의 폴더 하나를 선택하십시오.</target>
<source>Defined by context of use</source>
-<target></target>
+<target>사용 환경에 따라 정의 됨</target>
<source>Requires FreeFileSync Donation Edition</source>
<target>FreeFileSync 기부자 에디션이 필요합니다.</target>
@@ -1111,10 +1111,10 @@ The command is triggered if:
<target>이름 지정:</target>
<source>Limit file versions:</source>
-<target></target>
+<target>파일 버전 제한:</target>
<source>Last x days:</source>
-<target></target>
+<target>최근 x일:</target>
<source>Ignore errors</source>
<target>오류 무시</target>
@@ -1195,7 +1195,7 @@ The command is triggered if:
<target>베어리언트:</target>
<source>&Don't show this dialog again</source>
-<target>이 대화 창을 다시 표시 안 함(&D)</target>
+<target>이 대화 상자를 다시 표시 안 함(&D)</target>
<source>Items found:</source>
<target>발견된 항목:</target>
@@ -1237,7 +1237,7 @@ The command is triggered if:
<target>직접 지켜보지 않는 자동 동기화의 경우, 배치 파일을 만듭니다. 시작하려면 파일을 더블 클릭하거나 작업 플래너에서 일정을 만드십시오: %x</target>
<source>Progress dialog:</source>
-<target>진행 대화 상자:</target>
+<target>진행률 대화 상자:</target>
<source>Run minimized</source>
<target>최소화 실행</target>
@@ -1258,7 +1258,7 @@ The command is triggered if:
<target>로그 저장:</target>
<source>Limit number of log files:</source>
-<target></target>
+<target>로그 파일 개수 제한:</target>
<source>How can I schedule a batch job?</source>
<target>일괄 작업 예약 방법은?</target>
@@ -1294,7 +1294,7 @@ This guarantees a consistent state even in case of a serious error.
<target>파일 및 폴더 권한 전송.</target>
<source>Show all permanently hidden dialogs and warning messages again</source>
-<target>영구적으로 숨겨진 모든 대화창 및 경고 메세지 다시 보이기</target>
+<target>영구적으로 숨겨진 모든 대화 상자 및 경고 메세지 다시 보이기</target>
<source>Customize context menu:</source>
<target>컨텍스트 메뉴 커스터마이즈 (사용자 정의):</target>
@@ -1503,7 +1503,7 @@ This guarantees a consistent state even in case of a serious error.
<target>시간간격(타임스팬) 선택...</target>
<source>Donation Edition</source>
-<target></target>
+<target>기부자 에디션</target>
<source>Folder Comparison and Synchronization</source>
<target>폴더 비교 및 동기화</target>
@@ -1701,10 +1701,10 @@ This guarantees a consistent state even in case of a serious error.
<target>반대 측에 대한 매개 변수</target>
<source>Show hidden dialogs again</source>
-<target>숨겨진 대화창 다시 보이기</target>
+<target>숨겨진 대화 상자 다시 보이기</target>
<source>All dialogs shown</source>
-<target></target>
+<target>모든 대화 상자 표시</target>
<source>Downloading update...</source>
<target>업데이트 다운로드 중...</target>
@@ -1770,7 +1770,7 @@ This guarantees a consistent state even in case of a serious error.
<target>타임 스탬프</target>
<source>Move files into a time-stamped subfolder</source>
-<target></target>
+<target>파일을 타임 스탬프가 지정된 하위 폴더로 이동</target>
<source>File</source>
<target>파일</target>
@@ -1800,7 +1800,7 @@ This guarantees a consistent state even in case of a serious error.
<target>YYYY-MM-DD hhmmss</target>
<source>Minimum version count must be smaller than maximum count.</source>
-<target></target>
+<target>최소 버전 카운트는 최대 카운트보다 더 작아야 합니다.</target>
<source>Files</source>
<target>파일</target>
@@ -1948,7 +1948,7 @@ This guarantees a consistent state even in case of a serious error.
<target>포터블</target>
<source>Save settings in %x</source>
-<target></target>
+<target>%x에 설정 저장</target>
<source>Register FreeFileSync file extensions</source>
<target>FreeFileSync 파일 확장자 등록</target>
diff --git a/FreeFileSync/Build/Resources.zip b/FreeFileSync/Build/Resources.zip
index fda17b61..48cee7b5 100755
--- a/FreeFileSync/Build/Resources.zip
+++ b/FreeFileSync/Build/Resources.zip
Binary files differ
diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile
index 9693f756..efc73825 100755
--- a/FreeFileSync/Source/Makefile
+++ b/FreeFileSync/Source/Makefile
@@ -62,6 +62,7 @@ CPP_FILES+=ui/folder_history_box.cpp
CPP_FILES+=ui/folder_selector.cpp
CPP_FILES+=ui/file_grid.cpp
CPP_FILES+=ui/file_view.cpp
+CPP_FILES+=ui/log_panel.cpp
CPP_FILES+=ui/tree_grid.cpp
CPP_FILES+=ui/gui_generated.cpp
CPP_FILES+=ui/gui_status_handler.cpp
diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp
index 85b74527..d0f1a137 100755
--- a/FreeFileSync/Source/RealTimeSync/gui_generated.cpp
+++ b/FreeFileSync/Source/RealTimeSync/gui_generated.cpp
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
-// C++ code generated with wxFormBuilder (version Jan 23 2018)
+// C++ code generated with wxFormBuilder (version May 29 2018)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
diff --git a/FreeFileSync/Source/RealTimeSync/gui_generated.h b/FreeFileSync/Source/RealTimeSync/gui_generated.h
index eaead163..5bfb621b 100755
--- a/FreeFileSync/Source/RealTimeSync/gui_generated.h
+++ b/FreeFileSync/Source/RealTimeSync/gui_generated.h
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
-// C++ code generated with wxFormBuilder (version Jan 23 2018)
+// C++ code generated with wxFormBuilder (version May 29 2018)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp
index 89678ba2..3ee8956b 100755
--- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp
+++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp
@@ -215,17 +215,17 @@ void MainDialog::OnStart(wxCommandEvent& event)
XmlRealConfig currentCfg = getConfiguration();
const Zstring activeCfgFilePath = !equalFilePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring();
- switch (rts::startDirectoryMonitor(currentCfg, ::extractJobName(activeCfgFilePath)))
+ switch (runFolderMonitor(currentCfg, ::extractJobName(activeCfgFilePath)))
{
- case rts::EXIT_APP:
+ case AbortReason::REQUEST_EXIT:
Close();
return;
- case rts::SHOW_GUI:
+ case AbortReason::REQUEST_GUI:
break;
}
- Show(); //don't show for EXIT_APP
+ Show(); //don't show for AbortReason::REQUEST_EXIT
Raise();
}
@@ -460,6 +460,7 @@ void MainDialog::removeAddFolder(size_t pos)
const size_t visibleRows = std::min(additionalFolderPanels_.size(), MAX_ADD_FOLDERS); //up to MAX_ADD_FOLDERS additional folders shall be shown
m_scrolledWinFolders->SetMinSize(wxSize(-1, folderHeight * static_cast<int>(visibleRows)));
+ m_scrolledWinFolders->Layout(); //[!] needed when scrollbars are shown
//adapt delete top folder pair button
m_bpButtonRemoveTopFolder->Show(!additionalFolderPanels_.empty());
diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp
index 3b5a4321..a586ce4d 100755
--- a/FreeFileSync/Source/RealTimeSync/monitor.cpp
+++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp
@@ -5,13 +5,9 @@
// *****************************************************************************
#include "monitor.h"
-#include <ctime>
-#include <set>
#include <zen/file_access.h>
#include <zen/dir_watcher.h>
#include <zen/thread.h>
-#include <zen/basic_math.h>
-#include <wx/utils.h>
#include "../base/resolve_path.h"
//#include "../library/db_file.h" //SYNC_DB_FILE_ENDING -> complete file too much of a dependency; file ending too little to decouple into single header
//#include "../library/lock_holder.h" //LOCK_FILE_ENDING
@@ -25,10 +21,11 @@ namespace
const std::chrono::seconds FOLDER_EXISTENCE_CHECK_INTERVAL(1);
-std::vector<Zstring> getFormattedDirs(const std::vector<Zstring>& folderPathPhrases) //throw FileError
+std::set<Zstring, LessFilePath> getFormattedDirs(const std::vector<Zstring>& folderPathPhrases) //throw FileError
{
std::set<Zstring, LessFilePath> folderPaths; //make unique
- for (const Zstring& phrase : std::set<Zstring, LessFilePath>(folderPathPhrases.begin(), folderPathPhrases.end()))
+
+ for (const Zstring& phrase : folderPathPhrases)
{
//hopefully clear enough now: https://freefilesync.org/forum/viewtopic.php?t=4302
auto checkProtocol = [&](const Zstring& protoName)
@@ -44,7 +41,60 @@ std::vector<Zstring> getFormattedDirs(const std::vector<Zstring>& folderPathPhra
folderPaths.insert(fff::getResolvedFilePath(phrase));
}
- return { folderPaths.begin(), folderPaths.end() };
+ return folderPaths;
+}
+
+
+//wait until all directories become available (again) + logs in network share
+std::set<Zstring, LessFilePath> waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError
+ const std::function<void(const Zstring& folderPath)>& requestUiRefresh, std::chrono::milliseconds cbInterval)
+{
+ for (;;)
+ {
+ //support specifying volume by name => call getResolvedFilePath() repeatedly
+ std::set<Zstring, LessFilePath> folderPaths = getFormattedDirs(folderPathPhrases); //throw FileError
+
+ std::vector<std::pair<Zstring, std::future<bool>>> futureInfo;
+ //start all folder checks asynchronously (non-existent network path may block)
+ for (const Zstring& folderPath : folderPaths)
+ futureInfo.emplace_back(folderPath, runAsync([folderPath]
+ {
+ //2. check dir availability
+ return dirAvailable(folderPath);
+ }));
+
+ bool allAvailable = true;
+
+ for (auto& item : futureInfo)
+ {
+ const Zstring& folderPath = item.first;
+ std::future<bool>& ftDirAvailable = item.second;
+
+ for (;;)
+ {
+ while (ftDirAvailable.wait_for(cbInterval) != std::future_status::ready)
+ requestUiRefresh(folderPath); //throw X
+
+ if (ftDirAvailable.get())
+ break;
+
+ //wait until folder is available: do not needlessly poll all others again!
+ allAvailable = false;
+
+ //wait some time...
+ const auto delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL;
+ for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now())
+ {
+ requestUiRefresh(folderPath); //throw X
+ std::this_thread::sleep_for(cbInterval);
+ }
+
+ ftDirAvailable = runAsync([folderPath] { return dirAvailable(folderPath); });
+ }
+ }
+ if (allAvailable) //only return when all folders were found on *first* try!
+ return folderPaths;
+ }
}
@@ -53,43 +103,32 @@ struct WaitResult
{
enum ChangeType
{
- CHANGE_DETECTED,
- CHANGE_DIR_UNAVAILABLE //not existing or can't access
+ ITEM_CHANGED,
+ FOLDER_UNAVAILABLE //1. not existing or 2. can't access
};
- WaitResult(const zen::DirWatcher::Entry& changedItem) : type(CHANGE_DETECTED), changedItem_(changedItem) {}
- WaitResult(const Zstring& folderPath) : type(CHANGE_DIR_UNAVAILABLE), folderPath_(folderPath) {}
+ explicit WaitResult(const DirWatcher::Entry& changeEntry) : type(ITEM_CHANGED), changedItem(changeEntry) {}
+ explicit WaitResult(const Zstring& folderPath) : type(FOLDER_UNAVAILABLE), missingFolderPath(folderPath) {}
ChangeType type;
- zen::DirWatcher::Entry changedItem_; //for type == CHANGE_DETECTED: file or directory
- Zstring folderPath_; //for type == CHANGE_DIR_UNAVAILABLE
+ DirWatcher::Entry changedItem; //for type == ITEM_CHANGED: file or directory
+ Zstring missingFolderPath; //for type == FOLDER_UNAVAILABLE
};
-WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw FileError
+WaitResult waitForChanges(const std::set<Zstring, LessFilePath>& folderPaths, //throw FileError
const std::function<void(bool readyForSync)>& requestUiRefresh, std::chrono::milliseconds cbInterval)
{
- const std::vector<Zstring> folderPaths = getFormattedDirs(folderPathPhrases); //throw FileError
+ assert(std::all_of(folderPaths.begin(), folderPaths.end(), [](const Zstring& folderPath) { return dirAvailable(folderPath); }));
if (folderPaths.empty()) //pathological case, but we have to check else this function will wait endlessly
throw FileError(_("A folder input field is empty.")); //should have been checked by caller!
- //detect when volumes are removed/are not available anymore
- std::vector<std::pair<Zstring, std::shared_ptr<DirWatcher>>> watches;
+ std::vector<std::pair<Zstring, std::unique_ptr<DirWatcher>>> watches;
for (const Zstring& folderPath : folderPaths)
- {
try
{
- //a non-existent network path may block, so check existence asynchronously!
- auto ftDirAvailable = runAsync([=] { return dirAvailable(folderPath); });
-
- while (ftDirAvailable.wait_for(cbInterval) != std::future_status::ready)
- if (requestUiRefresh) requestUiRefresh(false /*readyForSync*/); //throw X
-
- if (!ftDirAvailable.get()) //folder not existing or can't access
- return WaitResult(folderPath);
-
- watches.emplace_back(folderPath, std::make_shared<DirWatcher>(folderPath)); //throw FileError
+ watches.emplace_back(folderPath, std::make_unique<DirWatcher>(folderPath)); //throw FileError
}
catch (FileError&)
{
@@ -97,16 +136,14 @@ WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw
return WaitResult(folderPath);
throw;
}
- }
auto lastCheckTime = std::chrono::steady_clock::now();
for (;;)
{
- const bool checkDirNow = [&]() -> bool //checking once per sec should suffice
+ const bool checkDirNow = [&] //checking once per sec should suffice
{
const auto now = std::chrono::steady_clock::now();
-
- if (numeric::dist(now, lastCheckTime) > FOLDER_EXISTENCE_CHECK_INTERVAL) //handle potential chrono wrap-around!
+ if (now > lastCheckTime + FOLDER_EXISTENCE_CHECK_INTERVAL)
{
lastCheckTime = now;
return true;
@@ -114,13 +151,12 @@ WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw
return false;
}();
-
- for (auto it = watches.begin(); it != watches.end(); ++it)
+ for (const auto& item : watches)
{
- const Zstring& folderPath = it->first;
- DirWatcher& watcher = *(it->second);
+ const Zstring& folderPath = item.first;
+ DirWatcher& watcher = *item.second;
- //IMPORTANT CHECK: dirwatcher has problems detecting removal of top watched directories!
+ //IMPORTANT CHECK: DirWatcher has problems detecting removal of top watched directories!
if (checkDirNow)
if (!dirAvailable(folderPath)) //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED
return WaitResult(folderPath);
@@ -128,14 +164,12 @@ WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw
{
std::vector<DirWatcher::Entry> changedItems = watcher.getChanges([&] { requestUiRefresh(false /*readyForSync*/); /*throw X*/ },
cbInterval); //throw FileError
-
- //remove to be ignored changes
erase_if(changedItems, [](const DirWatcher::Entry& e)
{
return
- endsWith(e.filePath, Zstr(".ffs_tmp")) || //sync.8ea2.ffs_tmp
- endsWith(e.filePath, Zstr(".ffs_lock")) || //sync.ffs_lock, sync.Del.ffs_lock
- endsWith(e.filePath, Zstr(".ffs_db")); //sync.ffs_db
+ endsWith(e.itemPath, Zstr(".ffs_tmp")) || //sync.8ea2.ffs_tmp
+ endsWith(e.itemPath, Zstr(".ffs_lock")) || //sync.ffs_lock, sync.Del.ffs_lock
+ endsWith(e.itemPath, Zstr(".ffs_db")); //sync.ffs_db
//no need to ignore temporary recycle bin directory: this must be caused by a file deletion anyway
});
@@ -156,45 +190,8 @@ WaitResult waitForChanges(const std::vector<Zstring>& folderPathPhrases, //throw
}
-//wait until all directories become available (again) + logs in network share
-void waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError
- const std::function<void(const Zstring& folderPath)>& requestUiRefresh, std::chrono::milliseconds cbInterval)
-{
- for (;;)
- {
- bool allAvailable = true;
- //support specifying volume by name => call getResolvedFilePath() repeatedly
- for (const Zstring& folderPath : getFormattedDirs(folderPathPhrases)) //throw FileError
- {
- auto ftDirAvailable = runAsync([=]
- {
- //2. check dir availability
- return dirAvailable(folderPath);
- });
- while (ftDirAvailable.wait_for(cbInterval) != std::future_status::ready)
- if (requestUiRefresh) requestUiRefresh(folderPath); //throw X
-
- if (!ftDirAvailable.get())
- {
- allAvailable = false;
- //wait some time...
- const auto delayUntil = std::chrono::steady_clock::now() + FOLDER_EXISTENCE_CHECK_INTERVAL;
- for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now())
- {
- if (requestUiRefresh) requestUiRefresh(folderPath); //throw X
- std::this_thread::sleep_for(cbInterval);
- }
- break;
- }
- }
- if (allAvailable)
- return;
- }
-}
-
-
inline
-wxString toString(DirWatcher::ActionType type)
+std::wstring getActionName(DirWatcher::ActionType type)
{
switch (type)
{
@@ -205,6 +202,7 @@ wxString toString(DirWatcher::ActionType type)
case DirWatcher::ACTION_DELETE:
return L"DELETE";
}
+ assert(false);
return L"ERROR";
}
@@ -212,71 +210,60 @@ struct ExecCommandNowException {};
}
-void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, size_t delay, MonitorCallback& cb, std::chrono::milliseconds cbInterval)
+void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, std::chrono::seconds delay,
+ const std::function<void(const Zstring& itemPath, const std::wstring& actionName)>& executeExternalCommand,
+ const std::function<void(const Zstring* missingFolderPath)>& requestUiRefresh,
+ const std::function<void(const std::wstring& msg )>& reportError,
+ std::chrono::milliseconds cbInterval)
{
+ assert(!folderPathPhrases.empty());
if (folderPathPhrases.empty())
- {
- assert(false);
return;
- }
- auto execMonitoring = [&] //throw FileError
- {
- cb.setPhase(MonitorCallback::MONITOR_PHASE_WAITING);
- waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { cb.requestUiRefresh(); }, cbInterval); //throw FileError
- cb.setPhase(MonitorCallback::MONITOR_PHASE_ACTIVE);
+ for (;;)
+ try
+ {
+ std::set<Zstring, LessFilePath> folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiRefresh(&folderPath); }, cbInterval); //throw FileError
- //schedule initial execution (*after* all directories have arrived, which could take some time which we don't want to include)
- time_t nextExecTime = std::time(nullptr) + delay;
+ //schedule initial execution (*after* all directories have arrived)
+ auto nextExecTime = std::chrono::steady_clock::now() + delay;
- for (;;) //loop over command invocations
- {
- DirWatcher::Entry lastChangeDetected;
- try
+ for (;;) //command executions
{
- for (;;) //loop over detected changes
+ DirWatcher::Entry lastChangeDetected;
+ try
{
- //wait for changes (and for all directories to become available)
- WaitResult res = waitForChanges(folderPathPhrases, [&](bool readyForSync) //throw FileError, ExecCommandNowException
+ for (;;) //detected changes
{
- if (readyForSync)
- if (nextExecTime <= std::time(nullptr))
+ const WaitResult res = waitForChanges(folderPaths, [&](bool readyForSync) //throw FileError, ExecCommandNowException
+ {
+ requestUiRefresh(nullptr);
+
+ if (readyForSync && std::chrono::steady_clock::now() >= nextExecTime)
throw ExecCommandNowException(); //abort wait and start sync
- cb.requestUiRefresh();
- }, cbInterval);
- switch (res.type)
- {
- case WaitResult::CHANGE_DIR_UNAVAILABLE: //don't execute the command before all directories are available!
- cb.setPhase(MonitorCallback::MONITOR_PHASE_WAITING);
- waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { cb.requestUiRefresh(); }, cbInterval); //throw FileError
- cb.setPhase(MonitorCallback::MONITOR_PHASE_ACTIVE);
- break;
-
- case WaitResult::CHANGE_DETECTED:
- lastChangeDetected = res.changedItem_;
- break;
+ }, cbInterval);
+ switch (res.type)
+ {
+ case WaitResult::ITEM_CHANGED:
+ lastChangeDetected = res.changedItem;
+ break;
+
+ case WaitResult::FOLDER_UNAVAILABLE: //don't execute the command before all directories are available!
+ lastChangeDetected = DirWatcher::Entry{ DirWatcher::ACTION_UPDATE, res.missingFolderPath};
+ folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiRefresh(&folderPath); }, cbInterval); //throw FileError
+ break;
+ }
+ nextExecTime = std::chrono::steady_clock::now() + delay;
}
- nextExecTime = std::time(nullptr) + delay;
}
- }
- catch (ExecCommandNowException&) {}
-
- ::wxSetEnv(L"change_path", utfTo<wxString>(lastChangeDetected.filePath)); //some way to output what file changed to the user
- ::wxSetEnv(L"change_action", toString(lastChangeDetected.action)); //
-
- //execute command
- cb.executeExternalCommand();
- nextExecTime = std::numeric_limits<time_t>::max();
- }
- };
+ catch (ExecCommandNowException&) {}
- for (;;)
- try
- {
- execMonitoring(); //throw FileError
+ executeExternalCommand(lastChangeDetected.itemPath, getActionName(lastChangeDetected.action));
+ nextExecTime = std::chrono::steady_clock::time_point::max();
+ }
}
catch (const FileError& e)
{
- cb.reportError(e.toString());
+ reportError(e.toString());
}
}
diff --git a/FreeFileSync/Source/RealTimeSync/monitor.h b/FreeFileSync/Source/RealTimeSync/monitor.h
index 70b6ff84..06d01161 100755
--- a/FreeFileSync/Source/RealTimeSync/monitor.h
+++ b/FreeFileSync/Source/RealTimeSync/monitor.h
@@ -14,24 +14,13 @@
namespace rts
{
-struct MonitorCallback
-{
- virtual ~MonitorCallback() {}
-
- enum WatchPhase
- {
- MONITOR_PHASE_ACTIVE,
- MONITOR_PHASE_WAITING,
- };
- virtual void setPhase(WatchPhase mode) = 0;
- virtual void executeExternalCommand () = 0;
- virtual void requestUiRefresh () = 0;
- virtual void reportError(const std::wstring& msg) = 0; //automatically retries after return!
-};
void monitorDirectories(const std::vector<Zstring>& folderPathPhrases,
- //non-formatted dirnames that yet require call to getFormattedDirectoryName(); empty directories must be checked by caller!
- size_t delay,
- MonitorCallback& cb, std::chrono::milliseconds cbInterval);
+ //non-formatted paths that yet require call to getFormattedDirectoryName(); empty directories must be checked by caller!
+ std::chrono::seconds delay,
+ const std::function<void(const Zstring& changedItemPath, const std::wstring& actionName)>& executeExternalCommand,
+ const std::function<void(const Zstring* missingFolderPath)>& requestUiRefresh, //either waiting for change notifications or at least one folder is missing
+ const std::function<void(const std::wstring& msg )>& reportError, //automatically retries after return!
+ std::chrono::milliseconds cbInterval);
}
#endif //MONITOR_H_345087425834253425
diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp
index 4e01f2ea..ddc5ea1c 100755
--- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp
+++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp
@@ -36,7 +36,7 @@ bool updateUiIsAllowed()
{
const auto now = std::chrono::steady_clock::now();
- if (numeric::dist(now, lastExec) > UI_UPDATE_INTERVAL) //handle potential chrono wrap-around!
+ if (now > lastExec + UI_UPDATE_INTERVAL)
{
lastExec = now;
return true;
@@ -57,45 +57,46 @@ class TrayIconObject : public wxTaskBarIcon
{
public:
TrayIconObject(const wxString& jobname) :
- resumeRequested(false),
- abortRequested(false),
- showErrorMsgRequested(false),
- mode(TRAY_MODE_ACTIVE),
- iconFlashStatusLast(false),
- jobName_(jobname),
- trayBmp(getResourceImage(L"RTS_tray_24x24")) //use a 24x24 bitmap for perfect fit
+ jobName_(jobname)
{
Connect(wxEVT_TASKBAR_LEFT_DCLICK, wxEventHandler(TrayIconObject::OnDoubleClick), nullptr, this);
- setMode(mode);
+
+ assert(mode_ != TRAY_MODE_ACTIVE); //setMode() supports polling!
+ setMode(TRAY_MODE_ACTIVE, Zstring());
}
//require polling:
- bool resumeIsRequested() const { return resumeRequested; }
- bool abortIsRequested () const { return abortRequested; }
+ bool resumeIsRequested() const { return resumeRequested_; }
+ bool abortIsRequested () const { return abortRequested_; }
//during TRAY_MODE_ERROR those two functions are available:
- void clearShowErrorRequested() { assert(mode == TRAY_MODE_ERROR); showErrorMsgRequested = false; }
- bool getShowErrorRequested() const { assert(mode == TRAY_MODE_ERROR); return showErrorMsgRequested; }
+ void clearShowErrorRequested() { assert(mode_ == TRAY_MODE_ERROR); showErrorMsgRequested_ = false; }
+ bool getShowErrorRequested() const { assert(mode_ == TRAY_MODE_ERROR); return showErrorMsgRequested_; }
- void setMode(TrayMode m)
+ void setMode(TrayMode m, const Zstring& missingFolderPath)
{
- mode = m;
- timer.Stop();
- timer.Disconnect(wxEVT_TIMER, wxEventHandler(TrayIconObject::OnErrorFlashIcon), nullptr, this);
+ if (mode_ == m && missingFolderPath_ == missingFolderPath)
+ return; //support polling
+
+ mode_ = m;
+ missingFolderPath_ = missingFolderPath;
+
+ timer_.Stop();
+ timer_.Disconnect(wxEVT_TIMER, wxEventHandler(TrayIconObject::OnErrorFlashIcon), nullptr, this);
switch (m)
{
case TRAY_MODE_ACTIVE:
- setTrayIcon(trayBmp, _("Directory monitoring active"));
+ setTrayIcon(trayBmp_, _("Directory monitoring active"));
break;
case TRAY_MODE_WAITING:
- setTrayIcon(greyScale(trayBmp), replaceCpy(_("Waiting until directory is available:"), L":", L""));
- warn_static("TODO: which one? => show on UI!")
+ assert(!missingFolderPath.empty());
+ setTrayIcon(greyScale(trayBmp_), _("Waiting until directory is available:") + L" " + fmtPath(missingFolderPath));
break;
case TRAY_MODE_ERROR:
- timer.Connect(wxEVT_TIMER, wxEventHandler(TrayIconObject::OnErrorFlashIcon), nullptr, this);
- timer.Start(500); //timer interval in [ms]
+ timer_.Connect(wxEVT_TIMER, wxEventHandler(TrayIconObject::OnErrorFlashIcon), nullptr, this);
+ timer_.Start(500); //timer interval in [ms]
break;
}
}
@@ -103,8 +104,8 @@ public:
private:
void OnErrorFlashIcon(wxEvent& event)
{
- iconFlashStatusLast = !iconFlashStatusLast;
- setTrayIcon(iconFlashStatusLast ? trayBmp : greyScale(trayBmp), _("Error"));
+ iconFlashStatusLast_ = !iconFlashStatusLast_;
+ setTrayIcon(iconFlashStatusLast_ ? trayBmp_ : greyScale(trayBmp_), _("Error"));
}
void setTrayIcon(const wxBitmap& bmp, const wxString& statusTxt)
@@ -129,7 +130,7 @@ private:
wxMenu* contextMenu = new wxMenu;
wxMenuItem* defaultItem = nullptr;
- switch (mode)
+ switch (mode_)
{
case TRAY_MODE_ACTIVE:
case TRAY_MODE_WAITING:
@@ -143,9 +144,8 @@ private:
contextMenu->AppendSeparator();
contextMenu->Append(CONTEXT_ABORT, _("&Quit"));
- //event handling
- contextMenu->Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TrayIconObject::OnContextMenuSelection), nullptr, this);
+ contextMenu->Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TrayIconObject::OnContextMenuSelection), nullptr, this);
return contextMenu; //ownership transferred to caller
}
@@ -154,44 +154,46 @@ private:
switch (static_cast<Selection>(event.GetId()))
{
case CONTEXT_ABORT:
- abortRequested = true;
+ abortRequested_ = true;
break;
case CONTEXT_RESTORE:
- resumeRequested = true;
+ resumeRequested_ = true;
break;
case CONTEXT_SHOW_ERROR:
- showErrorMsgRequested = true;
+ showErrorMsgRequested_ = true;
break;
}
}
void OnDoubleClick(wxEvent& event)
{
- switch (mode)
+ switch (mode_)
{
case TRAY_MODE_ACTIVE:
case TRAY_MODE_WAITING:
- resumeRequested = true; //never throw exceptions through a C-Layer call stack (GUI)!
+ resumeRequested_ = true; //never throw exceptions through a C-Layer call stack (GUI)!
break;
case TRAY_MODE_ERROR:
- showErrorMsgRequested = true;
+ showErrorMsgRequested_ = true;
break;
}
}
- bool resumeRequested;
- bool abortRequested;
- bool showErrorMsgRequested;
+ bool resumeRequested_ = false;
+ bool abortRequested_ = false;
+ bool showErrorMsgRequested_ = false;
- TrayMode mode;
+ TrayMode mode_ = TRAY_MODE_WAITING;
+ Zstring missingFolderPath_;
- bool iconFlashStatusLast; //flash try icon for TRAY_MODE_ERROR
- wxTimer timer; //
+ bool iconFlashStatusLast_ = false; //flash try icon for TRAY_MODE_ERROR
+ wxTimer timer_; //
const wxString jobName_; //RTS job name, may be empty
- const wxBitmap trayBmp;
+
+ const wxBitmap trayBmp_ = getResourceImage(L"RTS_tray_24x24"); //use a 24x24 bitmap for perfect fit
};
@@ -207,14 +209,14 @@ class TrayIconHolder
{
public:
TrayIconHolder(const wxString& jobname) :
- trayObj(new TrayIconObject(jobname)) {}
+ trayObj_(new TrayIconObject(jobname)) {}
~TrayIconHolder()
{
//harmonize with tray_icon.cpp!!!
- trayObj->RemoveIcon();
+ trayObj_->RemoveIcon();
//use wxWidgets delayed destruction: delete during next idle loop iteration (handle late window messages, e.g. when double-clicking)
- wxPendingDelete.Append(trayObj);
+ wxPendingDelete.Append(trayObj_);
}
void doUiRefreshNow() //throw AbortMonitoring
@@ -222,27 +224,27 @@ public:
wxTheApp->Yield(); //yield is UI-layer which is represented by this tray icon
//advantage of polling vs callbacks: we can throw exceptions!
- if (trayObj->resumeIsRequested())
- throw AbortMonitoring(SHOW_GUI);
+ if (trayObj_->resumeIsRequested())
+ throw AbortMonitoring(AbortReason::REQUEST_GUI);
- if (trayObj->abortIsRequested())
- throw AbortMonitoring(EXIT_APP);
+ if (trayObj_->abortIsRequested())
+ throw AbortMonitoring(AbortReason::REQUEST_EXIT);
}
- void setMode(TrayMode m) { trayObj->setMode(m); }
+ void setMode(TrayMode m, const Zstring& missingFolderPath) { trayObj_->setMode(m, missingFolderPath); }
- bool getShowErrorRequested() const { return trayObj->getShowErrorRequested(); }
- void clearShowErrorRequested() { trayObj->clearShowErrorRequested(); }
+ bool getShowErrorRequested() const { return trayObj_->getShowErrorRequested(); }
+ void clearShowErrorRequested() { trayObj_->clearShowErrorRequested(); }
private:
- TrayIconObject* trayObj;
+ TrayIconObject* const trayObj_;
};
//##############################################################################################################
}
-rts::AbortReason rts::startDirectoryMonitor(const XmlRealConfig& config, const wxString& jobname)
+rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxString& jobname)
{
std::vector<Zstring> dirNamesNonFmt = config.directories;
erase_if(dirNamesNonFmt, [](const Zstring& str) { return trimCpy(str).empty(); }); //remove empty entries WITHOUT formatting paths yet!
@@ -250,7 +252,7 @@ rts::AbortReason rts::startDirectoryMonitor(const XmlRealConfig& config, const w
if (dirNamesNonFmt.empty())
{
showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setMainInstructions(_("A folder input field is empty.")));
- return SHOW_GUI;
+ return AbortReason::REQUEST_GUI;
}
const Zstring cmdLine = trimCpy(config.commandline);
@@ -258,80 +260,74 @@ rts::AbortReason rts::startDirectoryMonitor(const XmlRealConfig& config, const w
if (cmdLine.empty())
{
showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setMainInstructions(_("Incorrect command line:") + L" \"\""));
- return SHOW_GUI;
+ return AbortReason::REQUEST_GUI;
}
- struct MonitorCallbackImpl : public MonitorCallback
+
+ TrayIconHolder trayIcon(jobname);
+
+ auto executeExternalCommand = [&](const Zstring& changedItemPath, const std::wstring& actionName)
{
- MonitorCallbackImpl(const wxString& jobname,
- const Zstring& cmdLine) : trayIcon(jobname), cmdLine_(cmdLine) {}
+ ::wxSetEnv(L"change_path", utfTo<wxString>(changedItemPath)); //some way to output what file changed to the user
+ ::wxSetEnv(L"change_action", actionName); //
- void setPhase(WatchPhase mode) override
+ auto cmdLineExp = fff::expandMacros(cmdLine);
+ try
{
- switch (mode)
- {
- case MONITOR_PHASE_ACTIVE:
- trayIcon.setMode(TRAY_MODE_ACTIVE);
- break;
- case MONITOR_PHASE_WAITING:
- trayIcon.setMode(TRAY_MODE_WAITING);
- break;
- }
+ shellExecute(cmdLineExp, ExecutionType::SYNC); //throw FileError
}
-
- void executeExternalCommand() override
+ catch (const FileError& e)
{
- auto cmdLineExp = fff::expandMacros(cmdLine_);
- try
- {
- shellExecute(cmdLineExp, ExecutionType::SYNC); //throw FileError
- }
- catch (const FileError& e)
- {
- showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString()));
- }
+ //blocks! however, we *expect* this to be a persistent error condition...
+ showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString()));
}
+ };
- void requestUiRefresh() override
- {
- if (updateUiIsAllowed())
- trayIcon.doUiRefreshNow(); //throw AbortMonitoring
- }
+ auto requestUiRefresh = [&](const Zstring* missingFolderPath)
+ {
+ if (missingFolderPath)
+ trayIcon.setMode(TRAY_MODE_WAITING, *missingFolderPath);
+ else
+ trayIcon.setMode(TRAY_MODE_ACTIVE, Zstring());
+
+ if (updateUiIsAllowed())
+ trayIcon.doUiRefreshNow(); //throw AbortMonitoring
+ };
- void reportError(const std::wstring& msg) override
+ auto reportError = [&](const std::wstring& msg)
+ {
+ trayIcon.setMode(TRAY_MODE_ERROR, Zstring());
+ trayIcon.clearShowErrorRequested();
+
+ //wait for some time, then return to retry
+ const auto delayUntil = std::chrono::steady_clock::now() + RETRY_AFTER_ERROR_INTERVAL;
+ for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now())
{
- trayIcon.setMode(TRAY_MODE_ERROR);
- trayIcon.clearShowErrorRequested();
-
- //wait for some time, then return to retry
- const auto delayUntil = std::chrono::steady_clock::now() + RETRY_AFTER_ERROR_INTERVAL;
- for (auto now = std::chrono::steady_clock::now(); now < delayUntil; now = std::chrono::steady_clock::now())
- {
- trayIcon.doUiRefreshNow(); //throw AbortMonitoring
-
- if (trayIcon.getShowErrorRequested())
- switch (showConfirmationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().
- setDetailInstructions(msg), _("&Retry")))
- {
- case ConfirmationButton::ACCEPT: //retry
- return;
-
- case ConfirmationButton::CANCEL:
- throw AbortMonitoring(SHOW_GUI);
- }
- std::this_thread::sleep_for(UI_UPDATE_INTERVAL);
- }
+ trayIcon.doUiRefreshNow(); //throw AbortMonitoring
+
+ if (trayIcon.getShowErrorRequested())
+ switch (showConfirmationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().
+ setDetailInstructions(msg), _("&Retry")))
+ {
+ case ConfirmationButton::ACCEPT: //retry
+ return;
+
+ case ConfirmationButton::CANCEL:
+ throw AbortMonitoring(AbortReason::REQUEST_GUI);
+ }
+ std::this_thread::sleep_for(UI_UPDATE_INTERVAL);
}
-
- TrayIconHolder trayIcon;
- const Zstring cmdLine_;
- } cb(jobname, cmdLine);
+ };
try
{
- monitorDirectories(dirNamesNonFmt, config.delay, cb, UI_UPDATE_INTERVAL / 2); //cb: throw AbortMonitoring
+ monitorDirectories(dirNamesNonFmt, std::chrono::seconds(config.delay),
+ executeExternalCommand,
+ requestUiRefresh, //throw AbortMonitoring
+ reportError, //
+ UI_UPDATE_INTERVAL / 2);
assert(false);
- return SHOW_GUI;
+ return AbortReason::REQUEST_GUI;
}
catch (const AbortMonitoring& ab)
{
diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.h b/FreeFileSync/Source/RealTimeSync/tray_menu.h
index ad2ee456..79c63dc2 100755
--- a/FreeFileSync/Source/RealTimeSync/tray_menu.h
+++ b/FreeFileSync/Source/RealTimeSync/tray_menu.h
@@ -13,12 +13,12 @@
namespace rts
{
-enum AbortReason
+enum class AbortReason
{
- SHOW_GUI,
- EXIT_APP
+ REQUEST_GUI,
+ REQUEST_EXIT
};
-AbortReason startDirectoryMonitor(const XmlRealConfig& config, const wxString& jobname); //jobname may be empty
+AbortReason runFolderMonitor(const XmlRealConfig& config, const wxString& jobname); //jobname may be empty
}
#endif //TRAY_MENU_H_3967857420987534253245
diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp
index 7821f2c2..a91d1130 100755
--- a/FreeFileSync/Source/base/algorithm.cpp
+++ b/FreeFileSync/Source/base/algorithm.cpp
@@ -169,16 +169,16 @@ private:
bool allItemsCategoryEqual(const ContainerObject& hierObj)
{
return std::all_of(hierObj.refSubFiles().begin(), hierObj.refSubFiles().end(),
- [](const FilePair& file) { return file.getCategory() == FILE_EQUAL; })&& //files
+ [](const FilePair& file) { return file.getCategory() == FILE_EQUAL; })&&
std::all_of(hierObj.refSubLinks().begin(), hierObj.refSubLinks().end(),
- [](const SymlinkPair& link) { return link.getLinkCategory() == SYMLINK_EQUAL; })&& //symlinks
+ [](const SymlinkPair& link) { return link.getLinkCategory() == SYMLINK_EQUAL; })&&
- std::all_of(hierObj.refSubFolders(). begin(), hierObj.refSubFolders().end(),
+ std::all_of(hierObj.refSubFolders().begin(), hierObj.refSubFolders().end(),
[](const FolderPair& folder)
{
- return folder.getDirCategory() == DIR_EQUAL && allItemsCategoryEqual(folder); //short circuit-behavior!
- }); //directories
+ return folder.getDirCategory() == DIR_EQUAL && allItemsCategoryEqual(folder); //short-circuit behavior!
+ });
}
}
@@ -1206,8 +1206,8 @@ void copyToAlternateFolderFrom(const std::vector<const FileSystemObject*>& rowsT
const std::wstring txtCreatingFolder(_("Creating folder %x" ));
const std::wstring txtCreatingLink (_("Creating symbolic link %x"));
- auto copyItem = [&callback, overwriteIfExists](const AbstractPath& targetPath, ItemStatReporter<>& statReporter, //throw FileError
- const std::function<void(const std::function<void()>& deleteTargetItem)>& copyItemPlain) //throw FileError
+ auto copyItem = [&](const AbstractPath& targetPath, ItemStatReporter<>& statReporter, //throw FileError
+ const std::function<void(const std::function<void()>& deleteTargetItem)>& copyItemPlain) //throw FileError
{
//start deleting existing target as required by copyFileTransactional():
//best amortized performance if "target existing" is the most common case
@@ -1235,13 +1235,15 @@ void copyToAlternateFolderFrom(const std::vector<const FileSystemObject*>& rowsT
}
else if (ps.relPath.size() > 1) //parent folder missing
{
+ //notifyItemCopy(txtCreatingFolder, AFS::getDisplayPath(*AFS::getParentFolderPath(targetPath))); -> useful?
+
AbstractPath intermediatePath = ps.existingPath;
- for (const Zstring& itemName : std::vector<Zstring>(ps.relPath.begin(), ps.relPath.end() - 1))
+ std::for_each(ps.relPath.begin(), ps.relPath.end() - 1, [&](const Zstring& itemName)
{
AFS::createFolderPlain(intermediatePath = AFS::appendRelPath(intermediatePath, itemName)); //throw FileError
statReporter.reportDelta(1, 0);
callback.requestUiRefresh(); //throw X
- }
+ });
//potential future issue when adding multithreading support: intermediate folders might already exist
//potential future issue 2: folder created by parallel thread just after failure => ps->relPath.size() == 1, but need retry!
//see abstract.cpp; AFS::createFolderIfMissingRecursion()
diff --git a/FreeFileSync/Source/base/application.cpp b/FreeFileSync/Source/base/application.cpp
index 96aac50d..9ccb0b05 100755
--- a/FreeFileSync/Source/base/application.cpp
+++ b/FreeFileSync/Source/base/application.cpp
@@ -20,11 +20,13 @@
#include "process_xml.h"
#include "error_log.h"
#include "resolve_path.h"
+#include "generate_logfile.h"
#include "../ui/batch_status_handler.h"
#include "../ui/main_dlg.h"
#include <gtk/gtk.h>
+
using namespace zen;
using namespace fff;
@@ -500,7 +502,7 @@ void showSyntaxHelp()
void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& batchCfg, const Zstring& cfgFilePath, FfsReturnCode& returnCode)
{
- const bool showPopupAllowed = !batchCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorDialog == BatchErrorDialog::SHOW;
+ const bool showPopupAllowed = !batchCfg.mainCfg.ignoreErrors && batchCfg.batchExCfg.batchErrorHandling == BatchErrorHandling::SHOW_POPUP;
auto notifyError = [&](const std::wstring& msg, FfsReturnCode rc)
{
@@ -543,37 +545,49 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat
// checkForUpdatePeriodically(globalCfg.lastUpdateCheck);
//WinInet not working when FFS is running as a service!!! https://support.microsoft.com/en-us/kb/238425
- try //begin of synchronization process (all in one try-catch block)
+ const std::map<AbstractPath, size_t>& deviceParallelOps = batchCfg.mainCfg.deviceParallelOps;
+
+ std::set<Zstring, LessFilePath> logFilePathsToKeep;
+ for (const ConfigFileItem& item : globalCfg.gui.mainDlg.cfgFileHistory)
+ logFilePathsToKeep.insert(item.logFilePath);
+
+ const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
+
+ //class handling status updates and error messages
+ BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized,
+ batchCfg.batchExCfg.autoCloseSummary,
+ extractJobName(cfgFilePath),
+ globalCfg.soundFileSyncFinished,
+ syncStartTime,
+ batchCfg.batchExCfg.altLogfileCountMax,
+ batchCfg.batchExCfg.altLogFolderPathPhrase,
+ batchCfg.mainCfg.ignoreErrors,
+ batchCfg.batchExCfg.batchErrorHandling,
+ batchCfg.mainCfg.automaticRetryCount,
+ batchCfg.mainCfg.automaticRetryDelay,
+ batchCfg.mainCfg.postSyncCommand,
+ batchCfg.mainCfg.postSyncCondition,
+ batchCfg.batchExCfg.postSyncAction);
+ try
{
- const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
-
- //class handling status updates and error messages
- BatchStatusHandler statusHandler(!batchCfg.batchExCfg.runMinimized, //throw AbortProcess, BatchRequestSwitchToMainDialog
- batchCfg.batchExCfg.autoCloseSummary,
- extractJobName(cfgFilePath),
- globalCfg.soundFileSyncFinished,
- syncStartTime,
- batchCfg.batchExCfg.logFolderPathPhrase,
- batchCfg.batchExCfg.logfilesCountLimit,
- globalCfg.lastSyncsLogFileSizeMax,
- batchCfg.mainCfg.ignoreErrors,
- batchCfg.batchExCfg.batchErrorDialog,
- batchCfg.mainCfg.automaticRetryCount,
- batchCfg.mainCfg.automaticRetryDelay,
- returnCode,
- batchCfg.mainCfg.postSyncCommand,
- batchCfg.mainCfg.postSyncCondition,
- batchCfg.batchExCfg.postSyncAction);
-
- logNonDefaultSettings(globalCfg, statusHandler); //inform about (important) non-default global settings
-
- const std::vector<FolderPairCfg> fpCfgList = extractCompareCfg(batchCfg.mainCfg);
+ warn_static("consider for removal after FFS 10.3 release")
+#if 1
+ if (batchCfg.batchExCfg.altLogfileCountMax != 0)
+ {
+ if (!trimCpy(batchCfg.batchExCfg.altLogFolderPathPhrase).empty())
+ statusHandler.reportWarning(replaceCpy(L"Beginning with FreeFileSync 10.3 the batch-specific log folder path %x will not be used.\n"
+ L"Instead all synchronization logs will be written into " + fmtPath(getDefaultLogFolderPath()) +
+ L"\n(See Menu -> Tools: Options; re-save this configuation to remove this warning)",
+ L"%x", fmtPath(batchCfg.batchExCfg.altLogFolderPathPhrase)), globalCfg.warnDlgs.warnBatchLoggingDeprecated);
+ }
+#endif
+
+ //inform about (important) non-default global settings
+ logNonDefaultSettings(globalCfg, statusHandler); //throw AbortProcess
//batch mode: place directory locks on directories during both comparison AND synchronization
std::unique_ptr<LockHolder> dirLocks;
- const std::map<AbstractPath, size_t>& deviceParallelOps = batchCfg.mainCfg.deviceParallelOps;
-
//COMPARE DIRECTORIES
FolderComparison cmpResult = compare(globalCfg.warnDlgs,
globalCfg.fileTimeTolerance,
@@ -582,15 +596,10 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat
globalCfg.folderAccessTimeout,
globalCfg.createLockFile,
dirLocks,
- fpCfgList,
+ extractCompareCfg(batchCfg.mainCfg),
deviceParallelOps,
- statusHandler); //throw X
-
+ statusHandler); //throw AbortProcess
//START SYNCHRONIZATION
- const std::vector<FolderPairSyncCfg> syncProcessCfg = extractSyncCfg(batchCfg.mainCfg);
- if (syncProcessCfg.size() != cmpResult.size())
- throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__));
-
synchronize(syncStartTime,
globalCfg.verifyFileCopy,
globalCfg.copyLockedFiles,
@@ -598,26 +607,38 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat
globalCfg.failSafeFileCopy,
globalCfg.runWithBackgroundPriority,
globalCfg.folderAccessTimeout,
- syncProcessCfg,
+ extractSyncCfg(batchCfg.mainCfg),
cmpResult,
deviceParallelOps,
globalCfg.warnDlgs,
- statusHandler); //throw X
+ statusHandler); //throw AbortProcess
+ }
+ catch (AbortProcess&) {} //exit used by statusHandler
- //not cancelled? => update last sync date for the selected cfg file
- for (ConfigFileItem& cfi : globalCfg.gui.mainDlg.cfgFileHistory)
- if (equalFilePath(cfi.filePath, cfgFilePath))
+ BatchStatusHandler::Result r = statusHandler.reportFinalStatus(globalCfg.logfilesMaxAgeDays, logFilePathsToKeep); //noexcept
+ //----------------------------------------------------------------------
+
+ raiseReturnCode(returnCode, mapToReturnCode(r.finalStatus));
+
+ //update last sync stats for the selected cfg file
+ for (ConfigFileItem& cfi : globalCfg.gui.mainDlg.cfgFileHistory)
+ if (equalFilePath(cfi.cfgFilePath, cfgFilePath))
+ {
+ if (r.finalStatus != SyncResult::ABORTED)
+ cfi.lastSyncTime = std::chrono::system_clock::to_time_t(syncStartTime);
+ assert(!r.logFilePath.empty());
+ if (!r.logFilePath.empty())
{
- cfi.lastSyncTime = std::time(nullptr);
- break;
+ cfi.logFilePath = r.logFilePath;
+ cfi.logResult = r.finalStatus;
}
- }
- catch (AbortProcess&) {} //exit used by statusHandler
- catch (BatchRequestSwitchToMainDialog&)
- {
- //open new toplevel window *after* progress dialog is gone => run on main event loop
+ break;
+ }
+
+ //open new top-level window *after* progress dialog is gone => run on main event loop
+ if (r.switchToGuiRequested)
return MainDialog::create(globalConfigFilePath, &globalCfg, convertBatchToGui(batchCfg), { cfgFilePath }, true /*startComparison*/);
- }
+
try //save global settings to XML: e.g. ignored warnings
{
diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp
index 4d530c90..00303c17 100755
--- a/FreeFileSync/Source/base/comparison.cpp
+++ b/FreeFileSync/Source/base/comparison.cpp
@@ -423,7 +423,7 @@ namespace parallel
//--------------------------------------------------------------
inline
bool filesHaveSameContent(const AbstractPath& filePath1, const AbstractPath& filePath2, //throw FileError
- const zen::IOCallback& notifyUnbufferedIO, //may be nullptr
+ const IOCallback& notifyUnbufferedIO, //may be nullptr
std::mutex& singleThread)
{ return parallelScope([=] { return filesHaveSameContent(filePath1, filePath2, notifyUnbufferedIO); /*throw FileError*/ }, singleThread); }
}
@@ -971,7 +971,7 @@ FolderComparison fff::compare(WarningDialogs& warnings,
//indicator at the very beginning of the log to make sense of "total time"
//init process: keep at beginning so that all gui elements are initialized properly
- callback.initNewPhase(-1, 0, ProcessCallback::PHASE_SCANNING); //throw X; it's unknown how many files will be scanned => -1 objects
+ callback.initNewPhase(-1, -1, ProcessCallback::PHASE_SCANNING); //throw X; it's unknown how many files will be scanned => -1 objects
//callback.reportInfo(Comparison started")); -> still useful?
//-------------------------------------------------------------------------------
diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h
index b6183578..345b29c2 100755
--- a/FreeFileSync/Source/base/dir_exist_async.h
+++ b/FreeFileSync/Source/base/dir_exist_async.h
@@ -39,7 +39,7 @@ FolderStatus getFolderStatusNonBlocking(const std::set<AbstractPath>& folderPath
std::map<AbstractPath, std::set<AbstractPath>> perDevicePaths;
for (const AbstractPath& folderPath : folderPaths)
- if (!AFS::isNullPath(folderPath)) //skip empty dirs
+ if (!AFS::isNullPath(folderPath)) //skip empty folders
perDevicePaths[AFS::getRootPath(folderPath)].insert(folderPath);
std::vector<std::pair<AbstractPath, std::future<bool>>> futureInfo;
@@ -83,7 +83,7 @@ FolderStatus getFolderStatusNonBlocking(const std::set<AbstractPath>& folderPath
procCallback.reportStatus(replaceCpy(_("Searching for folder %x..."), L"%x", displayPathFmt)); //throw X
- while (numeric::dist(std::chrono::steady_clock::now(), startTime) < std::chrono::seconds(folderAccessTimeout) && //handle potential chrono wrap-around!
+ while (std::chrono::steady_clock::now() < startTime + std::chrono::seconds(folderAccessTimeout) &&
fi.second.wait_for(UI_UPDATE_INTERVAL / 2) != std::future_status::ready)
procCallback.requestUiRefresh(); //throw X
diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp
index cb574ab3..ab38ed53 100755
--- a/FreeFileSync/Source/base/dir_lock.cpp
+++ b/FreeFileSync/Source/base/dir_lock.cpp
@@ -121,7 +121,7 @@ struct LockInformation //throw FileError
LockInformation getLockInfoFromCurrentProcess() //throw FileError
{
LockInformation lockInfo = {};
- lockInfo.lockId = zen::generateGUID();
+ lockInfo.lockId = generateGUID();
//wxGetFullHostName() is a performance killer and can hang for some users, so don't touch!
diff --git a/FreeFileSync/Source/base/generate_logfile.cpp b/FreeFileSync/Source/base/generate_logfile.cpp
index ecd34f0c..221441b1 100755
--- a/FreeFileSync/Source/base/generate_logfile.cpp
+++ b/FreeFileSync/Source/base/generate_logfile.cpp
@@ -7,208 +7,326 @@
#include "generate_logfile.h"
#include <zen/file_io.h>
#include <wx/datetime.h>
+#include "ffs_paths.h"
+#include "../fs/concrete.h"
using namespace zen;
using namespace fff;
+using AFS = AbstractFileSystem;
namespace
{
-std::wstring generateLogHeader(const LogSummary& s)
+std::wstring generateLogHeader(const ProcessSummary& s, const ErrorLog& log, const std::wstring& finalStatusMsg)
{
- assert(s.itemsProcessed <= s.itemsTotal);
- assert(s.bytesProcessed <= s.bytesTotal);
-
- std::wstring output;
+ //assemble summary box
+ std::vector<std::wstring> summary;
//write header
std::wstring headerLine = formatTime<std::wstring>(FORMAT_DATE);
if (!s.jobName.empty())
headerLine += L" | " + s.jobName;
- headerLine += L" | " + s.finalStatus;
+ headerLine += L" | " + finalStatusMsg;
- //assemble results box
- std::vector<std::wstring> results;
- results.push_back(headerLine);
- results.push_back(L"");
+ summary.push_back(headerLine);
+ summary.push_back(L"");
- const wchar_t tabSpace[] = L" ";
+ const std::wstring tabSpace(4, L' '); //4, the one true space count for tabs
- std::wstring itemsProc = tabSpace + _("Items processed:") + L" " + formatNumber(s.itemsProcessed); //show always, even if 0!
- if (s.itemsProcessed != 0 || s.bytesProcessed != 0) //[!] don't show 0 bytes processed if 0 items were processed
- itemsProc += + L" (" + formatFilesizeShort(s.bytesProcessed) + L")";
- results.push_back(itemsProc);
+ const int errorCount = log.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR);
+ const int warningCount = log.getItemCount(MSG_TYPE_WARNING);
- if (s.itemsTotal != 0 || s.bytesTotal != 0) //=: sync phase was reached and there were actual items to sync
- {
- if (s.itemsProcessed != s.itemsTotal ||
- s.bytesProcessed != s.bytesTotal)
- results.push_back(tabSpace + _("Items remaining:") + L" " + formatNumber(s.itemsTotal - s.itemsProcessed) + L" (" + formatFilesizeShort(s.bytesTotal - s.bytesProcessed) + L")");
- }
+ if (errorCount > 0) summary.push_back(tabSpace + _("Error" ) + L": " + formatNumber(errorCount));
+ if (warningCount > 0) summary.push_back(tabSpace + _("Warning") + L": " + formatNumber(warningCount));
+
+
+ std::wstring itemsProc = tabSpace + _("Items processed:") + L" " + formatNumber(s.statsProcessed.items); //show always, even if 0!
+ itemsProc += L" (" + formatFilesizeShort(s.statsProcessed.bytes) + L")";
+ summary.push_back(itemsProc);
- results.push_back(tabSpace + _("Total time:") + L" " + copyStringTo<std::wstring>(wxTimeSpan::Seconds(s.totalTime).Format()));
+ if ((s.statsTotal.items < 0 && s.statsTotal.bytes < 0) || //no total items/bytes: e.g. for pure folder comparison
+ s.statsProcessed == s.statsTotal) //...if everything was processed successfully
+ ;
+ else
+ summary.push_back(tabSpace + _("Items remaining:") +
+ L" " + formatNumber (s.statsTotal.items - s.statsProcessed.items) +
+ L" (" + formatFilesizeShort(s.statsTotal.bytes - s.statsProcessed.bytes) + L")");
- //calculate max width, this considers UTF-16 only, not true Unicode...but maybe good idea? those 2-char-UTF16 codes are usually wider than fixed width chars anyway!
+ const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(s.totalTime).count();
+ summary.push_back(tabSpace + _("Total time:") + L" " + copyStringTo<std::wstring>(wxTimeSpan::Seconds(totalTimeSec).Format()));
+
+ //calculate max width, this considers UTF-16 only, not true Unicode...but maybe good idea? those 2-byte-UTF16 codes are usually wider than fixed width chars anyway!
size_t sepLineLen = 0;
- for (const std::wstring& str : results) sepLineLen = std::max(sepLineLen, str.size());
+ for (const std::wstring& str : summary) sepLineLen = std::max(sepLineLen, str.size());
- output.resize(output.size() + sepLineLen + 1, L'_');
+ std::wstring output(sepLineLen + 1, L'_');
output += L'\n';
- for (const std::wstring& str : results) { output += L'|'; output += str; output += L'\n'; }
+ for (const std::wstring& str : summary) { output += L'|'; output += str; output += L'\n'; }
output += L'|';
- output.resize(output.size() + sepLineLen, L'_');
+ output.append(sepLineLen, L'_');
output += L'\n';
return output;
}
-}
-void fff::streamToLogFile(const LogSummary& summary, //throw FileError
- const zen::ErrorLog& log,
- AFS::OutputStream& streamOut)
+void streamToLogFile(const ProcessSummary& summary, //throw FileError
+ const ErrorLog& log,
+ const std::wstring& finalStatusLabel,
+ AFS::OutputStream& streamOut)
{
- const std::string header = replaceCpy(utfTo<std::string>(generateLogHeader(summary)), '\n', LINE_BREAK); //don't replace line break any earlier
+ auto fmtForTxtFile = [needLbReplace = !strEqual(LINE_BREAK, '\n')](const std::wstring& str)
+ {
+ std::string utfStr = utfTo<std::string>(str);
+ if (needLbReplace)
+ replace(utfStr, '\n', LINE_BREAK);
+ return utfStr;
+ };
- streamOut.write(&header[0], header.size()); //throw FileError, X
+ std::string buffer = fmtForTxtFile(generateLogHeader(summary, log, finalStatusLabel)); //don't replace line break any earlier
+
+ streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
+ buffer.clear(); //flush out header if entry.empty()
- //write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries!
- std::string buffer;
buffer += LINE_BREAK;
for (const LogEntry& entry : log)
{
- buffer += replaceCpy(utfTo<std::string>(formatMessage<std::wstring>(entry)), '\n', LINE_BREAK);
+ buffer += fmtForTxtFile(formatMessage(entry));
buffer += LINE_BREAK;
+ //write log items in blocks instead of creating one big string: memory allocation might fail; think 1 million entries!
streamOut.write(&buffer[0], buffer.size()); //throw FileError, X
buffer.clear();
}
}
-void fff::saveToLastSyncsLog(const LogSummary& summary, //throw FileError
- const zen::ErrorLog& log,
- size_t maxBytesToWrite, //log may be *huge*, e.g. 1 million items; LastSyncs.log *must not* create performance problems!
- const std::function<void(const std::wstring& msg)>& notifyStatus)
+const int TIME_STAMP_LENGTH = 21;
+const Zchar STATUS_BEGIN_TOKEN[] = Zstr(" [");
+const Zchar STATUS_END_TOKEN = Zstr(']');
+
+//"Backup FreeFileSync 2013-09-15 015052.123.log" ->
+//"Backup FreeFileSync 2013-09-15 015052.123 [Error].log"
+AbstractPath saveNewLogFile(const ProcessSummary& summary, //throw FileError
+ const ErrorLog& log,
+ const AbstractPath& logFolderPath,
+ const std::chrono::system_clock::time_point& syncStartTime,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
{
- const Zstring filePath = getConfigDirPathPf() + Zstr("LastSyncs.log");
+ //create logfile folder if required
+ AFS::createFolderIfMissingRecursion(logFolderPath); //throw FileError
- Utf8String newStream = utfTo<Utf8String>(generateLogHeader(summary));
- replace(newStream, '\n', LINE_BREAK); //don't replace line break any earlier
- newStream += LINE_BREAK;
+ //const std::string colon = "\xcb\xb8"; //="modifier letter raised colon" => regular colon is forbidden in file names on Windows and OS X
+ //=> too many issues, most notably cmd.exe is not Unicode-aware: https://freefilesync.org/forum/viewtopic.php?t=1679
- //check size of "newStream": memory allocation might fail - think 1 million entries!
- for (const LogEntry& entry : log)
- {
- newStream += replaceCpy(utfTo<Utf8String>(formatMessage<std::wstring>(entry)), '\n', LINE_BREAK);
- newStream += LINE_BREAK;
+ //assemble logfile name
+ const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(syncStartTime));
+ if (tc == TimeComp())
+ throw FileError(L"Failed to determine current time: " + numberTo<std::wstring>(syncStartTime.time_since_epoch().count()));
+
+ const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(syncStartTime.time_since_epoch()).count() % 1000;
+ assert(std::chrono::duration_cast<std::chrono::seconds>(syncStartTime.time_since_epoch()).count() == std::chrono::system_clock::to_time_t(syncStartTime));
- if (newStream.size() > maxBytesToWrite)
+ Zstring logFileName;
+
+ if (!summary.jobName.empty())
+ logFileName += utfTo<Zstring>(summary.jobName) + Zstr(' ');
+
+ logFileName += formatTime<Zstring>(Zstr("%Y-%m-%d %H%M%S"), tc) +
+ Zstr(".") + printNumber<Zstring>(Zstr("%03d"), static_cast<int>(timeMs)); //[ms] should yield a fairly unique name
+ static_assert(TIME_STAMP_LENGTH == 21);
+
+ const std::wstring failStatus = [&]
+ {
+ switch (summary.finalStatus)
{
- newStream += "[...]";
- newStream += LINE_BREAK;
- break;
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ break;
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return _("Warning");
+ case SyncResult::FINISHED_WITH_ERROR:
+ return _("Error");
+ case SyncResult::ABORTED:
+ return _("Stopped");
}
- }
+ return std::wstring();
+ }();
- auto notifyUnbufferedIOLoad = [notifyStatus,
- bytesRead_ = int64_t(0),
- msg_ = replaceCpy(_("Loading file %x..."), L"%x", fmtPath(filePath))]
- (int64_t bytesDelta) mutable
- {
- if (notifyStatus)
- notifyStatus(msg_ + L" (" + formatFilesizeShort(bytesRead_ += bytesDelta) + L")"); /*throw X*/
- };
+ if (!failStatus.empty())
+ logFileName += STATUS_BEGIN_TOKEN + utfTo<Zstring>(failStatus) + STATUS_END_TOKEN;
+ logFileName += Zstr(".log");
- auto notifyUnbufferedIOSave = [notifyStatus,
- bytesWritten_ = int64_t(0),
- msg_ = replaceCpy(_("Saving file %x..."), L"%x", fmtPath(filePath))]
+ const AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, logFileName);
+
+ auto notifyUnbufferedIO = [notifyStatus,
+ bytesWritten_ = int64_t(0),
+ msg_ = replaceCpy(_("Saving file %x..."), L"%x", fmtPath(AFS::getDisplayPath(logFilePath)))]
(int64_t bytesDelta) mutable
{
if (notifyStatus)
- notifyStatus(msg_ + L" (" + formatFilesizeShort(bytesWritten_ += bytesDelta) + L")"); /*throw X*/
+ notifyStatus(msg_ + L" (" + formatFilesizeShort(bytesWritten_ += bytesDelta) + L")"); //throw X
};
- //fill up the rest of permitted space by appending old log
- if (newStream.size() < maxBytesToWrite)
+ const std::wstring& finalStatusLabel = getFinalStatusLabel(summary.finalStatus);
+
+ std::unique_ptr<AFS::OutputStream> logFileStream = AFS::getOutputStream(logFilePath, nullptr, /*streamSize*/ notifyUnbufferedIO); //throw FileError
+ streamToLogFile(summary, log, finalStatusLabel, *logFileStream); //throw FileError, X
+ logFileStream->finalize(); //throw FileError, X
+
+ return logFilePath;
+}
+
+
+struct LogFileInfo
+{
+ AbstractPath filePath;
+ time_t timeStamp;
+ std::wstring jobName; //may be empty
+};
+std::vector<LogFileInfo> getLogFiles(const AbstractPath& logFolderPath) //throw FileError
+{
+ std::vector<LogFileInfo> logfiles;
+
+ AFS::traverseFolderFlat(logFolderPath, [&](const AFS::FileInfo& fi) //throw FileError
{
- Utf8String oldStream;
- try
- {
- oldStream = loadBinContainer<Utf8String>(filePath, notifyUnbufferedIOLoad); //throw FileError, X
- //Note: we also report the loaded bytes via onUpdateSaveStatus()!
- }
- catch (FileError&) {}
+ //"Backup FreeFileSync 2013-09-15 015052.123.log"
+ //"2013-09-15 015052.123 [Error].log"
+ static_assert(TIME_STAMP_LENGTH == 21);
- if (!oldStream.empty())
+ if (endsWith(fi.itemName, Zstr(".log"), CmpFilePath()))
{
- newStream += LINE_BREAK;
- newStream += LINE_BREAK;
- newStream += oldStream; //implicitly limited by "maxBytesToWrite"!
-
- //truncate size if required
- if (newStream.size() > maxBytesToWrite)
+ auto tsBegin = fi.itemName.begin();
+ auto tsEnd = fi.itemName.end() - 4;
+
+ if (tsBegin != tsEnd && tsEnd[-1] == STATUS_END_TOKEN)
+ tsEnd = search_last(tsBegin, tsEnd,
+ std::begin(STATUS_BEGIN_TOKEN), std::end(STATUS_BEGIN_TOKEN) - 1);
+
+ if (tsEnd - tsBegin >= TIME_STAMP_LENGTH &&
+ tsEnd[-4] == Zstr('.') &&
+ isdigit(tsEnd[-3]) &&
+ isdigit(tsEnd[-2]) &&
+ isdigit(tsEnd[-1]))
{
- //but do not cut in the middle of a row
- auto it = std::search(newStream.cbegin() + maxBytesToWrite, newStream.cend(), std::begin(LINE_BREAK), std::end(LINE_BREAK) - 1);
- if (it != newStream.cend())
+ tsBegin = tsEnd - TIME_STAMP_LENGTH;
+ const TimeComp tc = parseTime(Zstr("%Y-%m-%d %H%M%S"), StringRef<const Zchar>(tsBegin, tsBegin + 17)); //returns TimeComp() on error
+ const time_t t = localToTimeT(tc); //returns -1 on error
+ if (t != -1)
{
- newStream.resize(it - newStream.cbegin());
- newStream += LINE_BREAK;
-
- newStream += "[...]";
- newStream += LINE_BREAK;
+ Zstring jobName(fi.itemName.begin(), tsBegin);
+ if (!jobName.empty())
+ {
+ assert(jobName.size() >= 2 && jobName.end()[-1] == Zstr(' '));
+ jobName.pop_back();
+ }
+
+ logfiles.push_back({ AFS::appendRelPath(logFolderPath, fi.itemName), t, utfTo<std::wstring>(jobName) });
}
}
}
- }
+ },
+ nullptr /*onFolder*/, //traverse only one level deep
+ nullptr /*onSymlink*/);
- saveBinContainer(filePath, newStream, notifyUnbufferedIOSave); //throw FileError, X
+ return logfiles;
}
-void fff::limitLogfileCount(const AbstractPath& logFolderPath, const std::wstring& jobname, size_t maxCount, //throw FileError
- const std::function<void(const std::wstring& msg)>& notifyStatus)
+void limitLogfileCount(const AbstractPath& logFolderPath, //throw FileError
+ int logfilesMaxAgeDays, //<= 0 := no limit
+ const std::set<AbstractPath>& logFilePathsToKeep,
+ const std::function<void(const std::wstring& msg)>& notifyStatus)
{
- const std::wstring cleaningMsg = _("Cleaning up log files:");
- const Zstring prefix = utfTo<Zstring>(jobname);
+ if (logfilesMaxAgeDays > 0)
+ {
+ if (notifyStatus) notifyStatus(_("Cleaning up log files:") + L" " + fmtPath(AFS::getDisplayPath(logFolderPath)));
- //traverse source directory one level deep
- if (notifyStatus) notifyStatus(cleaningMsg + L" " + fmtPath(AFS::getDisplayPath(logFolderPath)));
+ std::vector<LogFileInfo> logFiles = getLogFiles(logFolderPath); //throw FileError
- std::vector<Zstring> logFileNames;
+ const time_t lastMidnightTime = []
+ {
+ TimeComp tc = getLocalTime(); //returns TimeComp() on error
+ tc.second = 0;
+ tc.minute = 0;
+ tc.hour = 0;
+ return localToTimeT(tc); //returns -1 on error => swallow => no versions trimmed by versionMaxAgeDays
+ }();
+ const time_t cutOffTime = lastMidnightTime - logfilesMaxAgeDays * 24 * 3600;
+
+ std::exception_ptr firstError;
+
+ for (const LogFileInfo& lfi : logFiles)
+ if (lfi.timeStamp < cutOffTime &&
+ logFilePathsToKeep.find(lfi.filePath) == logFilePathsToKeep.end()) //don't trim latest log files corresponding to last used config files!
+ {
+ if (notifyStatus) notifyStatus(_("Cleaning up log files:") + L" " + fmtPath(AFS::getDisplayPath(lfi.filePath)));
+ try
+ {
+ AFS::removeFilePlain(lfi.filePath); //throw FileError
+ }
+ catch (const FileError&) { if (!firstError) firstError = std::current_exception(); };
+ }
- AFS::traverseFolderFlat(logFolderPath, [&](const AFS::FileInfo& fi) //throw FileError
- {
- if (startsWith(fi.itemName, prefix, CmpFilePath() /*even on Linux!*/) && endsWith(fi.itemName, Zstr(".log"), CmpFilePath()))
- logFileNames.push_back(fi.itemName);
- },
- nullptr /*onFolder*/,
- nullptr /*onSymlink*/);
+ if (firstError) //late failure!
+ std::rethrow_exception(firstError);
+ }
+}
+}
+
+
+Zstring fff::getDefaultLogFolderPath() { return getConfigDirPathPf() + Zstr("Logs") ; }
- Opt<FileError> lastError;
- if (logFileNames.size() > maxCount)
+MessageType fff::getFinalMsgType(SyncResult finalStatus)
+{
+ switch (finalStatus)
{
- //delete oldest logfiles: take advantage of logfile naming convention to find them
- std::nth_element(logFileNames.begin(), logFileNames.end() - maxCount, logFileNames.end(), LessFilePath());
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ return MSG_TYPE_INFO;
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return MSG_TYPE_WARNING;
+ case SyncResult::FINISHED_WITH_ERROR:
+ case SyncResult::ABORTED:
+ return MSG_TYPE_ERROR;
+ }
+ assert(false);
+ return MSG_TYPE_FATAL_ERROR;
+}
- std::for_each(logFileNames.begin(), logFileNames.end() - maxCount, [&](const Zstring& logFileName)
- {
- const AbstractPath filePath = AFS::appendRelPath(logFolderPath, logFileName);
- if (notifyStatus) notifyStatus(cleaningMsg + L" " + fmtPath(AFS::getDisplayPath(filePath)));
- try
- {
- AFS::removeFilePlain(filePath); //throw FileError
- }
- catch (const FileError& e) { if (!lastError) lastError = e; };
- });
+Zstring fff::saveLogFile(const ProcessSummary& summary, //throw FileError
+ const ErrorLog& log,
+ const std::chrono::system_clock::time_point& syncStartTime,
+ int logfilesMaxAgeDays,
+ const std::set<Zstring, LessFilePath>& logFilePathsToKeep,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/)
+{
+ //let's keep our log handling abstract; we might need it some time
+ const AbstractPath logFolderPath = createAbstractPath(getDefaultLogFolderPath());
+
+ std::set<AbstractPath> abstractLogFilePathsToKeep;
+ for (const Zstring& filePath : logFilePathsToKeep)
+ abstractLogFilePathsToKeep.insert(createAbstractPath(filePath));
+
+ Opt<AbstractPath> logFilePath;
+ std::exception_ptr firstError;
+ try
+ {
+ logFilePath = saveNewLogFile(summary, log, logFolderPath, syncStartTime, notifyStatus); //throw FileError, X
}
+ catch (const FileError&) { if (!firstError) firstError = std::current_exception(); };
+
+ try
+ {
+ limitLogfileCount(logFolderPath, logfilesMaxAgeDays, abstractLogFilePathsToKeep, notifyStatus); //throw FileError, X
+ }
+ catch (const FileError&) { if (!firstError) firstError = std::current_exception(); };
+
+ if (firstError) //late failure!
+ std::rethrow_exception(firstError);
- if (lastError) //late failure!
- throw* lastError;
+ return *AFS::getNativeItemPath(*logFilePath); //logFilePath *is* native because getDefaultLogFolderPath() is!
}
diff --git a/FreeFileSync/Source/base/generate_logfile.h b/FreeFileSync/Source/base/generate_logfile.h
index 40be8d4a..8b321c8b 100755
--- a/FreeFileSync/Source/base/generate_logfile.h
+++ b/FreeFileSync/Source/base/generate_logfile.h
@@ -7,38 +7,25 @@
#ifndef GENERATE_LOGFILE_H_931726432167489732164
#define GENERATE_LOGFILE_H_931726432167489732164
+#include <chrono>
#include <zen/error_log.h>
-#include "ffs_paths.h"
-#include "file_hierarchy.h"
+#include "return_codes.h"
+#include "status_handler.h"
namespace fff
{
-struct LogSummary
-{
- std::wstring jobName; //may be empty
- std::wstring finalStatus;
- int itemsProcessed = 0;
- int64_t bytesProcessed = 0;
- int itemsTotal = 0;
- int64_t bytesTotal = 0;
- int64_t totalTime = 0; //unit: [sec]
-};
-
-void streamToLogFile(const LogSummary& summary, //throw FileError
- const zen::ErrorLog& log,
- AFS::OutputStream& streamOut);
-
-void saveToLastSyncsLog(const LogSummary& summary, //throw FileError
- const zen::ErrorLog& log,
- size_t maxBytesToWrite,
- const std::function<void(const std::wstring& msg)>& notifyStatus);
+Zstring getDefaultLogFolderPath();
-inline Zstring getDefaultLogFolderPath() { return getConfigDirPathPf() + Zstr("Logs") ; }
+Zstring saveLogFile(const ProcessSummary& summary, //throw FileError
+ const zen::ErrorLog& log,
+ const std::chrono::system_clock::time_point& syncStartTime,
+ int logfilesMaxAgeDays,
+ const std::set<Zstring, LessFilePath>& logFilePathsToKeep,
+ const std::function<void(const std::wstring& msg)>& notifyStatus /*throw X*/);
-void limitLogfileCount(const AbstractPath& logFolderPath, const std::wstring& jobname, size_t maxCount, //throw FileError
- const std::function<void(const std::wstring& msg)>& notifyStatus);
+zen::MessageType getFinalMsgType(SyncResult finalStatus);
}
#endif //GENERATE_LOGFILE_H_931726432167489732164
diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp
index 11f2993b..805c4223 100755
--- a/FreeFileSync/Source/base/parallel_scan.cpp
+++ b/FreeFileSync/Source/base/parallel_scan.cpp
@@ -220,10 +220,8 @@ public:
if (threadIdx != notifyingThreadIdx_) //only one thread at a time may report status: the first in sequential order
return false;
- const auto now = std::chrono::steady_clock::now(); //0 on error
-
- //perform ui updates not more often than necessary + handle potential chrono wrap-around!
- if (numeric::dist(now, lastReportTime) > cbInterval_)
+ const auto now = std::chrono::steady_clock::now();
+ if (now > lastReportTime + cbInterval_) //perform ui updates not more often than necessary
{
lastReportTime = now; //keep "lastReportTime" at worker thread level to avoid locking!
return true;
diff --git a/FreeFileSync/Source/base/perf_check.h b/FreeFileSync/Source/base/perf_check.h
index 401d08f5..b4845a90 100755
--- a/FreeFileSync/Source/base/perf_check.h
+++ b/FreeFileSync/Source/base/perf_check.h
@@ -36,9 +36,9 @@ private:
std::tuple<double, int, double> getBlockDeltas(std::chrono::milliseconds windowSize) const;
- const std::chrono::milliseconds windowSizeRemTime_;
- const std::chrono::milliseconds windowSizeSpeed_;
- const std::chrono::milliseconds windowMax_;
+ std::chrono::milliseconds windowSizeRemTime_;
+ std::chrono::milliseconds windowSizeSpeed_;
+ std::chrono::milliseconds windowMax_;
std::map<std::chrono::nanoseconds, Record> samples_;
};
diff --git a/FreeFileSync/Source/base/process_callback.h b/FreeFileSync/Source/base/process_callback.h
index 0a8f487e..34c4c7e5 100755
--- a/FreeFileSync/Source/base/process_callback.h
+++ b/FreeFileSync/Source/base/process_callback.h
@@ -58,7 +58,7 @@ struct ProcessCallback
virtual void forceUiRefresh () = 0; //throw X - called before starting long running tasks which don't update regularly
//UI info only, should not be logged: called periodically after data was processed: expected(!) to request GUI update
- virtual void reportStatus(const std::wstring& msg) = 0; //throw X
+ virtual void reportStatus(const std::wstring& text) = 0; //throw X
//logging only, no status update!
virtual void logInfo(const std::wstring& msg) = 0;
@@ -70,7 +70,7 @@ struct ProcessCallback
reportStatus(msg); //throw X
}
- virtual void reportWarning(const std::wstring& warningMessage, bool& warningActive) = 0; //throw X
+ virtual void reportWarning(const std::wstring& msg, bool& warningActive) = 0; //throw X
//error handling:
enum Response
@@ -78,8 +78,8 @@ struct ProcessCallback
IGNORE_ERROR,
RETRY
};
- virtual Response reportError (const std::wstring& errorMessage, size_t retryNumber) = 0; //throw X; recoverable error situation
- virtual void reportFatalError(const std::wstring& errorMessage) = 0; //throw X; non-recoverable error situation
+ virtual Response reportError (const std::wstring& msg, size_t retryNumber) = 0; //throw X; recoverable error situation
+ virtual void reportFatalError(const std::wstring& msg) = 0; //throw X; non-recoverable error situation
virtual void abortProcessNow() = 0; //will throw an exception => don't call while in a C GUI callstack
};
diff --git a/FreeFileSync/Source/base/process_xml.cpp b/FreeFileSync/Source/base/process_xml.cpp
index 4aa7ebcf..e9a6fd47 100755
--- a/FreeFileSync/Source/base/process_xml.cpp
+++ b/FreeFileSync/Source/base/process_xml.cpp
@@ -10,6 +10,7 @@
#include <zen/file_io.h>
#include <zen/xml_io.h>
#include <zen/optional.h>
+#include <zen/time.h>
#include <wx/intl.h>
#include "ffs_paths.h"
//#include "../fs/concrete.h"
@@ -22,8 +23,8 @@ using namespace fff; //functionally needed for correct overload resolution!!!
namespace
{
//-------------------------------------------------------------------------------------------------------------------------------
-const int XML_FORMAT_VER_GLOBAL = 9; //2018-03-14
-const int XML_FORMAT_VER_FFS_CFG = 12; //2018-06-21
+const int XML_FORMAT_VER_GLOBAL = 10; //2018-07-27
+const int XML_FORMAT_VER_FFS_CFG = 13; //2018-07-14
//-------------------------------------------------------------------------------------------------------------------------------
}
@@ -217,27 +218,27 @@ bool readText(const std::string& input, SyncDirection& value)
template <> inline
-void writeText(const BatchErrorDialog& value, std::string& output)
+void writeText(const BatchErrorHandling& value, std::string& output)
{
switch (value)
{
- case BatchErrorDialog::SHOW:
+ case BatchErrorHandling::SHOW_POPUP:
output = "Show";
break;
- case BatchErrorDialog::CANCEL:
+ case BatchErrorHandling::CANCEL:
output = "Cancel";
break;
}
}
template <> inline
-bool readText(const std::string& input, BatchErrorDialog& value)
+bool readText(const std::string& input, BatchErrorHandling& value)
{
const std::string tmp = trimCpy(input);
if (tmp == "Show")
- value = BatchErrorDialog::SHOW;
+ value = BatchErrorHandling::SHOW_POPUP;
else if (tmp == "Cancel")
- value = BatchErrorDialog::CANCEL;
+ value = BatchErrorHandling::CANCEL;
else
return false;
return true;
@@ -490,6 +491,9 @@ void writeText(const ColumnTypeCfg& value, std::string& output)
case ColumnTypeCfg::LAST_SYNC:
output = "Last";
break;
+ case ColumnTypeCfg::LAST_LOG:
+ output = "Log";
+ break;
}
}
@@ -501,6 +505,8 @@ bool readText(const std::string& input, ColumnTypeCfg& value)
value = ColumnTypeCfg::NAME;
else if (tmp == "Last")
value = ColumnTypeCfg::LAST_SYNC;
+ else if (tmp == "Log")
+ value = ColumnTypeCfg::LAST_LOG;
else
return false;
return true;
@@ -834,6 +840,44 @@ void writeStruc(const ExternalApp& value, XmlElement& output)
out(value.cmdLine);
out.attribute("Label", value.description);
}
+
+
+template <> inline
+void writeText(const SyncResult& value, std::string& output)
+{
+ switch (value)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ output = "Success";
+ break;
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ output = "Warning";
+ break;
+ case SyncResult::FINISHED_WITH_ERROR:
+ output = "Error";
+ break;
+ case SyncResult::ABORTED:
+ output = "Stopped";
+ break;
+ }
+}
+
+template <> inline
+bool readText(const std::string& input, SyncResult& value)
+{
+ const std::string tmp = trimCpy(input);
+ if (tmp == "Success")
+ value = SyncResult::FINISHED_WITH_SUCCESS;
+ else if (tmp == "Warning")
+ value = SyncResult::FINISHED_WITH_WARNINGS;
+ else if (tmp == "Error")
+ value = SyncResult::FINISHED_WITH_ERROR;
+ else if (tmp == "Stopped")
+ value = SyncResult::ABORTED;
+ else
+ return false;
+ return true;
+}
}
@@ -856,28 +900,54 @@ Zstring resolveFreeFileSyncDriveMacro(const Zstring& cfgFilePhrase)
namespace zen
{
-//FFS portable: use special syntax for config file paths: e.g. "ffs_drive:\SyncJob.ffs_gui"
template <> inline
bool readStruc(const XmlElement& input, ConfigFileItem& value)
{
XmlIn in(input);
- Zstring rawPath;
- const bool rv1 = in(rawPath);
- if (rv1)
- value.filePath = resolveFreeFileSyncDriveMacro(rawPath);
+ const bool rv1 = in.attribute("Result", value.logResult);
- const bool rv2 = in.attribute("LastSync", value.lastSyncTime);
+ //FFS portable: use special syntax for config file paths: e.g. "FFS:\SyncJob.ffs_gui"
+ Zstring cfgPathRaw;
+ const bool rv2 = in.attribute("CfgPath", cfgPathRaw);
+ if (rv2) value.cfgFilePath = resolveFreeFileSyncDriveMacro(cfgPathRaw);
- return rv1 && rv2;
+ const bool rv3 = in.attribute("LastSync", value.lastSyncTime);
+
+ Zstring logPathRaw;
+ const bool rv4 = in.attribute("LogPath", logPathRaw);
+ if (rv4) value.logFilePath = resolveFreeFileSyncDriveMacro(logPathRaw);
+
+ return rv1 && rv2 && rv3 && rv4;
}
template <> inline
void writeStruc(const ConfigFileItem& value, XmlElement& output)
{
XmlOut out(output);
- out(substituteFreeFileSyncDriveLetter(value.filePath));
+ out.attribute("Result", value.logResult);
+ out.attribute("CfgPath", substituteFreeFileSyncDriveLetter(value.cfgFilePath));
out.attribute("LastSync", value.lastSyncTime);
+ out.attribute("LogPath", substituteFreeFileSyncDriveLetter(value.logFilePath));
+}
+
+//TODO: remove after migration! 2018-07-27
+struct ConfigFileItemV9
+{
+ Zstring filePath;
+ time_t lastSyncTime = 0;
+};
+template <> inline
+bool readStruc(const XmlElement& input, ConfigFileItemV9& value)
+{
+ 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;
}
}
@@ -970,9 +1040,19 @@ void readConfig(const XmlIn& in, SyncConfig& syncCfg, std::map<AbstractPath, siz
if (syncCfg.versioningStyle != VersioningStyle::REPLACE)
if (const XmlElement* e = in["VersioningFolder"].get())
{
- e->getAttribute("MaxAge", syncCfg.versionMaxAgeDays); //try to get attributes if available
- e->getAttribute("CountMin", syncCfg.versionCountMin); // => *no error* if not available
- e->getAttribute("CountMax", syncCfg.versionCountMax); //
+ e->getAttribute("MaxAge", syncCfg.versionMaxAgeDays); //try to get attributes if available
+
+ //TODO: remove if clause after migration! 2018-07-12
+ if (formatVer < 13)
+ {
+ e->getAttribute("CountMin", syncCfg.versionCountMin); // => *no error* if not available
+ e->getAttribute("CountMax", syncCfg.versionCountMax); //
+ }
+ else
+ {
+ e->getAttribute("MinCount", syncCfg.versionCountMin); // => *no error* if not available
+ e->getAttribute("MaxCount", syncCfg.versionCountMax); //
+ }
}
}
}
@@ -1211,10 +1291,10 @@ void readConfig(const XmlIn& in, BatchExclusiveConfig& cfg, int formatVer)
{
std::string str;
if (inBatchCfg["HandleError"](str))
- cfg.batchErrorDialog = str == "Stop" ? BatchErrorDialog::CANCEL : BatchErrorDialog::SHOW;
+ cfg.batchErrorHandling = str == "Stop" ? BatchErrorHandling::CANCEL : BatchErrorHandling::SHOW_POPUP;
}
else
- inBatchCfg["ErrorDialog"](cfg.batchErrorDialog);
+ inBatchCfg["ErrorDialog"](cfg.batchErrorHandling);
//TODO: remove if clause after migration! 2017-10-24
if (formatVer < 8)
@@ -1239,8 +1319,17 @@ void readConfig(const XmlIn& in, BatchExclusiveConfig& cfg, int formatVer)
else
inBatchCfg["PostSyncAction"](cfg.postSyncAction);
- inBatchCfg["LogfileFolder"](cfg.logFolderPathPhrase);
- inBatchCfg["LogfileFolder"].attribute("Limit", cfg.logfilesCountLimit);
+ //TODO: remove if clause after migration! 2018-07-12
+ if (formatVer < 13)
+ {
+ inBatchCfg["LogfileFolder"](cfg.altLogFolderPathPhrase);
+ inBatchCfg["LogfileFolder"].attribute("Limit", cfg.altLogfileCountMax);
+ }
+ else
+ {
+ inBatchCfg["LogfileFolder"](cfg.altLogFolderPathPhrase);
+ inBatchCfg["LogfileFolder"].attribute("MaxCount", cfg.altLogfileCountMax);
+ }
}
@@ -1302,7 +1391,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
inGeneral["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority);
inGeneral["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile);
inGeneral["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy);
- inGeneral["LastSyncsLogSizeMax" ].attribute("Bytes", cfg.lastSyncsLogFileSizeMax);
+ inGeneral["LogFiles" ].attribute("MaxAge", cfg.logfilesMaxAgeDays);
inGeneral["NotificationSound" ].attribute("CompareFinished", cfg.soundFileCompareFinished);
inGeneral["NotificationSound" ].attribute("SyncFinished", cfg.soundFileSyncFinished);
inGeneral["ProgressDialog" ].attribute("AutoClose", cfg.autoCloseProgressDialog);
@@ -1342,6 +1431,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
inOpt["WarnDependentBaseFolders" ].attribute("Show", cfg.warnDlgs.warnDependentBaseFolders);
inOpt["WarnDirectoryLockFailed" ].attribute("Show", cfg.warnDlgs.warnDirectoryLockFailed);
inOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync);
+ inOpt["WarnBatchLoggingDeprecated" ].attribute("Show", cfg.warnDlgs.warnBatchLoggingDeprecated);
}
//gui specific global settings (optional)
@@ -1382,6 +1472,10 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
inConfig["Columns"](cfg.gui.mainDlg.cfgGridColumnAttribs);
+ //TODO: remove after migration! 2018-07-27
+ if (formatVer < 10) //reset once to show the new log column
+ cfg.gui.mainDlg.cfgGridColumnAttribs = XmlGlobalSettings().gui.mainDlg.cfgGridColumnAttribs;
+
//TODO: remove parameter migration after some time! 2018-01-08
if (formatVer < 6)
{
@@ -1391,7 +1485,18 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
inGui["ConfigHistory"](cfgHist);
for (const Zstring& cfgPath : cfgHist)
- cfg.gui.mainDlg.cfgFileHistory.emplace_back(cfgPath, 0);
+ cfg.gui.mainDlg.cfgFileHistory.emplace_back(cfgPath, 0, Zstring(), SyncResult::FINISHED_WITH_SUCCESS);
+ }
+ //TODO: remove after migration! 2018-07-27
+ else if (formatVer < 10)
+ {
+ inConfig["Configurations"].attribute("MaxSize", cfg.gui.mainDlg.cfgHistItemsMax);
+
+ std::vector<ConfigFileItemV9> cfgFileHistory;
+ inConfig["Configurations"](cfgFileHistory);
+
+ for (const ConfigFileItemV9& item : cfgFileHistory)
+ cfg.gui.mainDlg.cfgFileHistory.emplace_back(item.filePath, item.lastSyncTime, Zstring(), SyncResult::FINISHED_WITH_SUCCESS);
}
else
{
@@ -1474,6 +1579,20 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer)
else
inWnd["Perspective"](cfg.gui.mainDlg.guiPerspectiveLast);
+ //TODO: remove after migration! 2018-07-27
+ if (formatVer < 10)
+ {
+ wxString newPersp;
+ for (wxString& item : split(cfg.gui.mainDlg.guiPerspectiveLast, L"|", SplitType::SKIP_EMPTY))
+ {
+ if (contains(item, L"name=SearchPanel;"))
+ replace(item, L";row=2;", L";row=3;");
+
+ newPersp += (newPersp.empty() ? L"" : L"|") + item;
+ }
+ cfg.gui.mainDlg.guiPerspectiveLast = newPersp;
+ }
+
std::vector<Zstring> tmp = splitFilterByLines(cfg.gui.defaultExclusionFilter); //default value
inGui["DefaultExclusionFilter"](tmp);
cfg.gui.defaultExclusionFilter = mergeFilterLines(tmp);
@@ -1742,8 +1861,8 @@ void writeConfig(const SyncConfig& syncCfg, const std::map<AbstractPath, size_t>
if (syncCfg.versioningStyle != VersioningStyle::REPLACE)
{
if (syncCfg.versionMaxAgeDays > 0) out["VersioningFolder"].attribute("MaxAge", syncCfg.versionMaxAgeDays);
- if (syncCfg.versionCountMin > 0) out["VersioningFolder"].attribute("CountMin", syncCfg.versionCountMin);
- if (syncCfg.versionCountMax > 0) out["VersioningFolder"].attribute("CountMax", syncCfg.versionCountMax);
+ if (syncCfg.versionCountMin > 0) out["VersioningFolder"].attribute("MinCount", syncCfg.versionCountMin);
+ if (syncCfg.versionCountMax > 0) out["VersioningFolder"].attribute("MaxCount", syncCfg.versionCountMax);
}
}
@@ -1858,10 +1977,10 @@ void writeConfig(const BatchExclusiveConfig& cfg, XmlOut& out)
outBatchCfg["ProgressDialog"].attribute("Minimized", cfg.runMinimized);
outBatchCfg["ProgressDialog"].attribute("AutoClose", cfg.autoCloseSummary);
- outBatchCfg["ErrorDialog" ](cfg.batchErrorDialog);
+ outBatchCfg["ErrorDialog" ](cfg.batchErrorHandling);
outBatchCfg["PostSyncAction"](cfg.postSyncAction);
- outBatchCfg["LogfileFolder"](cfg.logFolderPathPhrase);
- outBatchCfg["LogfileFolder"].attribute("Limit", cfg.logfilesCountLimit);
+ outBatchCfg["LogfileFolder"](cfg.altLogFolderPathPhrase);
+ outBatchCfg["LogfileFolder"].attribute("MaxCount", cfg.altLogfileCountMax);
}
@@ -1886,7 +2005,7 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out)
outGeneral["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority);
outGeneral["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile);
outGeneral["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy);
- outGeneral["LastSyncsLogSizeMax" ].attribute("Bytes", cfg.lastSyncsLogFileSizeMax);
+ outGeneral["LogFiles" ].attribute("MaxAge", cfg.logfilesMaxAgeDays);
outGeneral["NotificationSound" ].attribute("CompareFinished", cfg.soundFileCompareFinished);
outGeneral["NotificationSound" ].attribute("SyncFinished", cfg.soundFileSyncFinished);
outGeneral["ProgressDialog" ].attribute("AutoClose", cfg.autoCloseProgressDialog);
@@ -1906,6 +2025,7 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out)
outOpt["WarnDependentBaseFolders" ].attribute("Show", cfg.warnDlgs.warnDependentBaseFolders);
outOpt["WarnDirectoryLockFailed" ].attribute("Show", cfg.warnDlgs.warnDirectoryLockFailed);
outOpt["WarnVersioningFolderPartOfSync"].attribute("Show", cfg.warnDlgs.warnVersioningFolderPartOfSync);
+ outOpt["WarnBatchLoggingDeprecated" ].attribute("Show", cfg.warnDlgs.warnBatchLoggingDeprecated);
//gui specific global settings (optional)
XmlOut outGui = out["Gui"];
diff --git a/FreeFileSync/Source/base/process_xml.h b/FreeFileSync/Source/base/process_xml.h
index c9f42e08..cd11a3b8 100755
--- a/FreeFileSync/Source/base/process_xml.h
+++ b/FreeFileSync/Source/base/process_xml.h
@@ -29,9 +29,9 @@ enum XmlType
XmlType getXmlType(const Zstring& filePath); //throw FileError
-enum class BatchErrorDialog
+enum class BatchErrorHandling
{
- SHOW,
+ SHOW_POPUP,
CANCEL
};
@@ -68,12 +68,15 @@ inline bool operator!=(const XmlGuiConfig& lhs, const XmlGuiConfig& rhs) { retur
struct BatchExclusiveConfig
{
- BatchErrorDialog batchErrorDialog = BatchErrorDialog::SHOW;
+ BatchErrorHandling batchErrorHandling = BatchErrorHandling::SHOW_POPUP;
bool runMinimized = false;
bool autoCloseSummary = false;
PostSyncAction postSyncAction = PostSyncAction::NONE;
- Zstring logFolderPathPhrase;
- int logfilesCountLimit = -1; //max logfiles; 0 := don't save logfiles; < 0 := no limit
+ warn_static("consider for removal after FFS 10.3 release")
+#if 1
+ Zstring altLogFolderPathPhrase; //store log file copy (in addition to %appdata%\FreeFileSync\Logs): MANDATORY if altLogfileCountMax != 0
+ int altLogfileCountMax = 0; //max log file count; 0 := don't save logfiles; < 0 := no limit
+#endif
};
@@ -112,6 +115,10 @@ struct WarningDialogs
bool warnInputFieldEmpty = true;
bool warnDirectoryLockFailed = true;
bool warnVersioningFolderPartOfSync = true;
+ warn_static("consider for removal after FFS 10.3 release")
+#if 1
+ bool warnBatchLoggingDeprecated = true;
+#endif
};
inline bool operator==(const WarningDialogs& lhs, const WarningDialogs& rhs)
{
@@ -125,7 +132,8 @@ inline bool operator==(const WarningDialogs& lhs, const WarningDialogs& rhs)
lhs.warnRecyclerMissing == rhs.warnRecyclerMissing &&
lhs.warnInputFieldEmpty == rhs.warnInputFieldEmpty &&
lhs.warnDirectoryLockFailed == rhs.warnDirectoryLockFailed &&
- lhs.warnVersioningFolderPartOfSync == rhs.warnVersioningFolderPartOfSync;
+ lhs.warnVersioningFolderPartOfSync == rhs.warnVersioningFolderPartOfSync &&
+ lhs.warnBatchLoggingDeprecated == rhs.warnBatchLoggingDeprecated;
}
inline bool operator!=(const WarningDialogs& lhs, const WarningDialogs& rhs) { return !(lhs == rhs); }
@@ -162,19 +170,9 @@ struct ViewFilterDefault
};
-struct ConfigFileItem
-{
- ConfigFileItem() {}
- ConfigFileItem(const Zstring& fp, time_t lst) : filePath(fp), lastSyncTime(lst) {}
-
- Zstring filePath;
- time_t lastSyncTime = 0;
- //Zstring logFilePath;
-};
-
-
Zstring getGlobalConfigFile();
+
struct XmlGlobalSettings
{
XmlGlobalSettings(); //clang needs this anyway
@@ -191,7 +189,8 @@ struct XmlGlobalSettings
bool runWithBackgroundPriority = false;
bool createLockFile = true;
bool verifyFileCopy = false;
- size_t lastSyncsLogFileSizeMax = 100000; //maximum size for LastSyncs.log: use a human-readable number
+ int logfilesMaxAgeDays = 14; //<= 0 := no limit; for log files under %appdata%\FreeFileSync\Logs
+
Zstring soundFileCompareFinished;
Zstring soundFileSyncFinished = Zstr("gong.wav");
@@ -215,14 +214,14 @@ struct XmlGlobalSettings
bool overwriteIfExists = false;
Zstring lastUsedPath;
std::vector<Zstring> folderHistory;
- size_t historySizeMax = 15;
+ size_t historySizeMax = 15;
} copyToCfg;
bool textSearchRespectCase = false; //good default for Linux, too!
int maxFolderPairsVisible = 6;
- size_t cfgGridTopRowPos = 0;
- int cfgGridSyncOverdueDays = 7;
+ size_t cfgGridTopRowPos = 0;
+ int cfgGridSyncOverdueDays = 7;
ColumnTypeCfg cfgGridLastSortColumn = cfgGridLastSortColumnDefault;
bool cfgGridLastSortAscending = getDefaultSortDirection(cfgGridLastSortColumnDefault);
std::vector<ColAttributesCfg> cfgGridColumnAttribs = getCfgGridDefaultColAttribs();
diff --git a/FreeFileSync/Source/base/return_codes.h b/FreeFileSync/Source/base/return_codes.h
index 9604142c..9bbd2b11 100755
--- a/FreeFileSync/Source/base/return_codes.h
+++ b/FreeFileSync/Source/base/return_codes.h
@@ -7,9 +7,12 @@
#ifndef RETURN_CODES_H_81307482137054156
#define RETURN_CODES_H_81307482137054156
+#include <zen/i18n.h>
+
+
namespace fff
{
-enum FfsReturnCode
+enum FfsReturnCode //as returned after process exit
{
FFS_RC_SUCCESS = 0,
FFS_RC_FINISHED_WITH_WARNINGS,
@@ -25,6 +28,53 @@ void raiseReturnCode(FfsReturnCode& rc, FfsReturnCode rcProposed)
if (rc < rcProposed)
rc = rcProposed;
}
+
+
+enum class SyncResult
+{
+ FINISHED_WITH_SUCCESS,
+ FINISHED_WITH_WARNINGS,
+ FINISHED_WITH_ERROR,
+ ABORTED,
+};
+
+
+inline
+FfsReturnCode mapToReturnCode(SyncResult syncStatus)
+{
+ switch (syncStatus)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ return FFS_RC_SUCCESS;
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return FFS_RC_FINISHED_WITH_WARNINGS;
+ case SyncResult::FINISHED_WITH_ERROR:
+ return FFS_RC_FINISHED_WITH_ERRORS;
+ case SyncResult::ABORTED:
+ return FFS_RC_ABORTED;
+ }
+ assert(false);
+ return FFS_RC_ABORTED;
+}
+
+
+inline
+std::wstring getFinalStatusLabel(SyncResult finalStatus)
+{
+ switch (finalStatus)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ return _("Completed successfully");
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return _("Completed with warnings");
+ case SyncResult::FINISHED_WITH_ERROR:
+ return _("Completed with errors");
+ case SyncResult::ABORTED:
+ return _("Stopped");
+ }
+ assert(false);
+ return std::wstring();
+}
}
#endif //RETURN_CODES_H_81307482137054156
diff --git a/FreeFileSync/Source/base/status_handler.cpp b/FreeFileSync/Source/base/status_handler.cpp
index 9e2f78db..aba4810c 100755
--- a/FreeFileSync/Source/base/status_handler.cpp
+++ b/FreeFileSync/Source/base/status_handler.cpp
@@ -19,7 +19,7 @@ bool fff::updateUiIsAllowed()
{
const auto now = std::chrono::steady_clock::now();
- if (numeric::dist(now, lastExec) > UI_UPDATE_INTERVAL) //handle potential chrono wrap-around!
+ if (now >= lastExec + UI_UPDATE_INTERVAL)
{
lastExec = now;
return true;
diff --git a/FreeFileSync/Source/base/status_handler.h b/FreeFileSync/Source/base/status_handler.h
index a5cbab86..4145b795 100755
--- a/FreeFileSync/Source/base/status_handler.h
+++ b/FreeFileSync/Source/base/status_handler.h
@@ -14,6 +14,7 @@
#include <zen/i18n.h>
#include <zen/basic_math.h>
#include "process_callback.h"
+#include "return_codes.h"
namespace fff
@@ -45,6 +46,14 @@ struct AbortCallback
};
+struct ProgressStats
+{
+ int items = 0;
+ int64_t bytes = 0;
+};
+inline bool operator==(const ProgressStats& lhs, const ProgressStats& rhs) { return lhs.items == rhs.items && lhs.bytes == rhs.bytes; }
+
+
//common statistics "everybody" needs
struct Statistics
{
@@ -52,33 +61,38 @@ struct Statistics
virtual ProcessCallback::Phase currentPhase() const = 0;
- virtual int getItemsCurrent(ProcessCallback::Phase phaseId) const = 0;
- virtual int getItemsTotal (ProcessCallback::Phase phaseId) const = 0;
+ virtual ProgressStats getStatsCurrent(ProcessCallback::Phase phase) const = 0;
+ virtual ProgressStats getStatsTotal (ProcessCallback::Phase phase) const = 0;
- virtual int64_t getBytesCurrent(ProcessCallback::Phase phaseId) const = 0;
- virtual int64_t getBytesTotal (ProcessCallback::Phase phaseId) const = 0;
-
- virtual zen::Opt<AbortTrigger> getAbortStatus() const = 0;
+ virtual zen::Opt<AbortTrigger> getAbortStatus() const = 0;
virtual const std::wstring& currentStatusText() const = 0;
};
+struct ProcessSummary
+{
+ SyncResult finalStatus = SyncResult::ABORTED;
+ std::wstring jobName; //may be empty
+ ProgressStats statsProcessed ;
+ ProgressStats statsTotal;
+ std::chrono::milliseconds totalTime{};
+};
+
+
//partial callback implementation with common functionality for "batch", "GUI/Compare" and "GUI/Sync"
class StatusHandler : public ProcessCallback, public AbortCallback, public Statistics
{
public:
- StatusHandler() : numbersCurrent_(4), //init with phase count
- numbersTotal_ (4) {} //
-
//implement parts of ProcessCallback
- void initNewPhase(int itemsTotal, int64_t bytesTotal, Phase phaseId) override //(throw X)
+ void initNewPhase(int itemsTotal, int64_t bytesTotal, Phase phase) override //(throw X)
{
- currentPhase_ = phaseId;
- refNumbers(numbersTotal_, currentPhase_) = { itemsTotal, bytesTotal };
+ assert(itemsTotal < 0 == bytesTotal < 0);
+ currentPhase_ = phase;
+ refStats(statsTotal_, currentPhase_) = { itemsTotal, bytesTotal };
}
- void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override { updateData(numbersCurrent_, itemsDelta, bytesDelta); } //note: these methods MUST NOT throw in order
- void updateDataTotal (int itemsDelta, int64_t bytesDelta) override { updateData(numbersTotal_, itemsDelta, bytesDelta); } //to allow usage within destructors!
+ void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override { updateData(statsCurrent_, itemsDelta, bytesDelta); } //note: these methods MUST NOT throw in order
+ void updateDataTotal (int itemsDelta, int64_t bytesDelta) override { updateData(statsTotal_, itemsDelta, bytesDelta); } //to allow usage within destructors!
void requestUiRefresh() override final //throw X
{
@@ -122,7 +136,7 @@ public:
void userAbortProcessNow()
{
abortRequested_ = AbortTrigger::USER; //may overwrite AbortTrigger::PROGRAM
- forceUiRefreshNoThrow();
+ forceUiRefreshNoThrow(); //flush GUI to show new abort state
throw AbortProcess();
}
@@ -131,38 +145,31 @@ public:
{
abortRequested_ = AbortTrigger::USER; //may overwrite AbortTrigger::PROGRAM
} //called from GUI code: this does NOT call abortProcessNow() immediately, but later when we're out of the C GUI call stack
+ //=> don't call forceUiRefreshNoThrow() here
//implement Statistics
Phase currentPhase() const override final { return currentPhase_; }
- int getItemsCurrent(Phase phaseId) const override { return refNumbers(numbersCurrent_, phaseId).items; }
- int getItemsTotal (Phase phaseId) const override { assert(phaseId != PHASE_SCANNING); return refNumbers(numbersTotal_, phaseId).items; }
-
- int64_t getBytesCurrent(Phase phaseId) const override { assert(phaseId != PHASE_SCANNING); return refNumbers(numbersCurrent_, phaseId).bytes; }
- int64_t getBytesTotal (Phase phaseId) const override { assert(phaseId != PHASE_SCANNING); return refNumbers(numbersTotal_, phaseId).bytes; }
+ ProgressStats getStatsCurrent(ProcessCallback::Phase phase) const override { return refStats(statsCurrent_, phase); }
+ ProgressStats getStatsTotal (ProcessCallback::Phase phase) const override { return refStats(statsTotal_, phase); }
const std::wstring& currentStatusText() const override { return statusText_; }
zen::Opt<AbortTrigger> getAbortStatus() const override { return abortRequested_; }
private:
- struct StatNumber
- {
- int items = 0;
- int64_t bytes = 0;
- };
- using StatNumbers = std::vector<StatNumber>;
-
- void updateData(StatNumbers& num, int itemsDelta, int64_t bytesDelta)
+ void updateData(std::vector<ProgressStats>& num, int itemsDelta, int64_t bytesDelta)
{
- auto& st = refNumbers(num, currentPhase_);
+ auto& st = refStats(num, currentPhase_);
+ assert(st.items >= 0);
+ assert(st.bytes >= 0);
st.items += itemsDelta;
st.bytes += bytesDelta;
}
- static const StatNumber& refNumbers(const StatNumbers& num, Phase phaseId)
+ static const ProgressStats& refStats(const std::vector<ProgressStats>& num, Phase phase)
{
- switch (phaseId)
+ switch (phase)
{
case PHASE_SCANNING:
return num[0];
@@ -173,15 +180,14 @@ private:
case PHASE_NONE:
break;
}
- assert(false);
return num[3]; //dummy entry!
}
- static StatNumber& refNumbers(StatNumbers& num, Phase phaseId) { return const_cast<StatNumber&>(refNumbers(static_cast<const StatNumbers&>(num), phaseId)); }
+ static ProgressStats& refStats(std::vector<ProgressStats>& num, Phase phase) { return const_cast<ProgressStats&>(refStats(static_cast<const std::vector<ProgressStats>&>(num), phase)); }
Phase currentPhase_ = PHASE_NONE;
- StatNumbers numbersCurrent_;
- StatNumbers numbersTotal_;
+ std::vector<ProgressStats> statsCurrent_ = std::vector<ProgressStats>(4); //init with phase count
+ std::vector<ProgressStats> statsTotal_ = std::vector<ProgressStats>(4); //
std::wstring statusText_;
zen::Opt<AbortTrigger> abortRequested_;
diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp
index 1e46b4fd..96244abd 100755
--- a/FreeFileSync/Source/base/synchronization.cpp
+++ b/FreeFileSync/Source/base/synchronization.cpp
@@ -572,14 +572,14 @@ void verifyFiles(const AbstractPath& apSource, const AbstractPath& apTarget, con
//#################################################################################################################
//#################################################################################################################
-class DeletionHandling //abstract deletion variants: permanently, recycle bin, user-defined directory
+class DeletionHandler //abstract deletion variants: permanently, recycle bin, user-defined directory
{
public:
- DeletionHandling(const AbstractPath& baseFolderPath,
- DeletionPolicy handleDel, //nothrow!
- const AbstractPath& versioningFolderPath,
- VersioningStyle versioningStyle,
- time_t syncStartTime);
+ DeletionHandler(const AbstractPath& baseFolderPath, //nothrow!
+ DeletionPolicy deletionPolicy,
+ const AbstractPath& versioningFolderPath,
+ VersioningStyle versioningStyle,
+ time_t syncStartTime);
//clean-up temporary directory (recycle bin optimization)
void tryCleanup(ProcessCallback& cb /*throw X*/, bool allowCallbackException); //throw FileError -> call this in non-exceptional code path, i.e. somewhere after sync!
@@ -593,8 +593,8 @@ public:
const std::wstring& getTxtRemovingSymLink() const { return txtRemovingSymlink_; } //
private:
- DeletionHandling (const DeletionHandling&) = delete;
- DeletionHandling& operator=(const DeletionHandling&) = delete;
+ DeletionHandler (const DeletionHandler&) = delete;
+ DeletionHandler& operator=(const DeletionHandler&) = delete;
AFS::RecycleSession& getOrCreateRecyclerSession() //throw FileError => dont create in constructor!!!
{
@@ -621,7 +621,7 @@ private:
const AbstractPath versioningFolderPath_;
const VersioningStyle versioningStyle_;
const time_t syncStartTime_;
- std::unique_ptr<FileVersioner> versioner_; //throw FileError in constructor => create on demand!
+ std::unique_ptr<FileVersioner> versioner_;
//buffer status texts:
const std::wstring txtRemovingFile_;
@@ -632,19 +632,19 @@ private:
};
-DeletionHandling::DeletionHandling(const AbstractPath& baseFolderPath, //nothrow!
- DeletionPolicy handleDel,
- const AbstractPath& versioningFolderPath,
- VersioningStyle versioningStyle,
- time_t syncStartTime) :
- deletionPolicy_(handleDel),
+DeletionHandler::DeletionHandler(const AbstractPath& baseFolderPath, //nothrow!
+ DeletionPolicy deletionPolicy,
+ const AbstractPath& versioningFolderPath,
+ VersioningStyle versioningStyle,
+ time_t syncStartTime) :
+ deletionPolicy_(deletionPolicy),
baseFolderPath_(baseFolderPath),
versioningFolderPath_(versioningFolderPath),
versioningStyle_(versioningStyle),
syncStartTime_(syncStartTime),
txtRemovingFile_([&]
{
- switch (handleDel)
+ switch (deletionPolicy)
{
case DeletionPolicy::PERMANENT:
return _("Deleting file %x");
@@ -657,7 +657,7 @@ DeletionHandling::DeletionHandling(const AbstractPath& baseFolderPath, //nothrow
}()),
txtRemovingSymlink_([&]
{
- switch (handleDel)
+ switch (deletionPolicy)
{
case DeletionPolicy::PERMANENT:
return _("Deleting symbolic link %x");
@@ -670,7 +670,7 @@ txtRemovingSymlink_([&]
}()),
txtRemovingFolder_([&]
{
- switch (handleDel)
+ switch (deletionPolicy)
{
case DeletionPolicy::PERMANENT:
return _("Deleting folder %x");
@@ -683,14 +683,11 @@ txtRemovingFolder_([&]
}()) {}
-void DeletionHandling::tryCleanup(ProcessCallback& cb /*throw X*/, bool allowCallbackException) //throw FileError
+void DeletionHandler::tryCleanup(ProcessCallback& cb /*throw X*/, bool allowCallbackException) //throw FileError
{
assert(runningMainThread());
switch (deletionPolicy_)
{
- case DeletionPolicy::PERMANENT:
- break;
-
case DeletionPolicy::RECYCLER:
if (recyclerSession_)
{
@@ -711,24 +708,20 @@ void DeletionHandling::tryCleanup(ProcessCallback& cb /*throw X*/, bool allowCal
};
//move content of temporary directory to recycle bin in a single call
- getOrCreateRecyclerSession().tryCleanup(notifyDeletionStatus); //throw FileError
+ recyclerSession_->tryCleanup(notifyDeletionStatus); //throw FileError
}
break;
+ case DeletionPolicy::PERMANENT:
case DeletionPolicy::VERSIONING:
- //if (versioner_)
- //{
- // cb_.reportStatus(Removing old versions...")); //throw X
- // versioner->limitVersions([&] { cb_.requestUiRefresh(); /*throw X */ }); //throw FileError
- //}
break;
}
}
-void DeletionHandling::removeDirWithCallback(const AbstractPath& folderPath,//throw FileError, ThreadInterruption
- const Zstring& relativePath,
- AsyncItemStatReporter& statReporter, std::mutex& singleThread)
+void DeletionHandler::removeDirWithCallback(const AbstractPath& folderPath,//throw FileError, ThreadInterruption
+ const Zstring& relativePath,
+ AsyncItemStatReporter& statReporter, std::mutex& singleThread)
{
switch (deletionPolicy_)
{
@@ -774,9 +767,9 @@ void DeletionHandling::removeDirWithCallback(const AbstractPath& folderPath,//th
}
-void DeletionHandling::removeFileWithCallback(const FileDescriptor& fileDescr, //throw FileError, ThreadInterruption
- const Zstring& relativePath,
- AsyncItemStatReporter& statReporter, std::mutex& singleThread)
+void DeletionHandler::removeFileWithCallback(const FileDescriptor& fileDescr, //throw FileError, ThreadInterruption
+ const Zstring& relativePath,
+ AsyncItemStatReporter& statReporter, std::mutex& singleThread)
{
if (endsWith(relativePath, AFS::TEMP_FILE_ENDING)) //special rule for .ffs_tmp files: always delete permanently!
@@ -806,9 +799,9 @@ void DeletionHandling::removeFileWithCallback(const FileDescriptor& fileDescr, /
}
-void DeletionHandling::removeLinkWithCallback(const AbstractPath& linkPath, //throw FileError, throw ThreadInterruption
- const Zstring& relativePath,
- AsyncItemStatReporter& statReporter, std::mutex& singleThread)
+void DeletionHandler::removeLinkWithCallback(const AbstractPath& linkPath, //throw FileError, throw ThreadInterruption
+ const Zstring& relativePath,
+ AsyncItemStatReporter& statReporter, std::mutex& singleThread)
{
switch (deletionPolicy_)
{
@@ -929,8 +922,8 @@ public:
bool copyFilePermissions;
bool failSafeFileCopy;
std::vector<FileError>& errorsModTime;
- DeletionHandling& delHandlingLeft;
- DeletionHandling& delHandlingRight;
+ DeletionHandler& delHandlerLeft;
+ DeletionHandler& delHandlerRight;
size_t threadCount;
};
@@ -953,8 +946,8 @@ private:
FolderPairSyncer(SyncCtx& syncCtx, std::mutex& singleThread, AsyncCallback& acb) :
errorsModTime_ (syncCtx.errorsModTime),
- delHandlingLeft_ (syncCtx.delHandlingLeft),
- delHandlingRight_ (syncCtx.delHandlingRight),
+ delHandlerLeft_ (syncCtx.delHandlerLeft),
+ delHandlerRight_ (syncCtx.delHandlerRight),
verifyCopiedFiles_ (syncCtx.verifyCopiedFiles),
copyFilePermissions_(syncCtx.copyFilePermissions),
failSafeFileCopy_ (syncCtx.failSafeFileCopy),
@@ -999,8 +992,8 @@ private:
AsyncItemStatReporter& statReporter);
std::vector<FileError>& errorsModTime_;
- DeletionHandling& delHandlingLeft_;
- DeletionHandling& delHandlingRight_;
+ DeletionHandler& delHandlerLeft_;
+ DeletionHandler& delHandlerRight_;
const bool verifyCopiedFiles_;
const bool copyFilePermissions_;
@@ -1052,9 +1045,9 @@ void FolderPairSyncer::runPass(PassNo pass, SyncCtx& syncCtx, BaseFolderPair& ba
std::mutex singleThread; //only a single worker thread may run at a time, except for parallel file I/O
- AsyncCallback acb; //
- FolderPairSyncer fps(syncCtx, singleThread, acb); //manage life time: enclose InterruptibleThread's!!!
- Workload workload(threadCount, acb); //
+ AsyncCallback acb; //
+ FolderPairSyncer fps(syncCtx, singleThread, acb); //manage life time: enclose InterruptibleThread's!!!
+ Workload workload(threadCount, acb); //
workload.addWorkItems(fps.getFolderLevelWorkItems(pass, baseFolder, workload)); //initial workload: set *before* threads get access!
std::vector<InterruptibleThread> worker;
@@ -1166,7 +1159,7 @@ III) c -> d caveat: move-sequence needs to be processed in correct order!
*/
template <class List> inline
-bool haveNameClash(const Zstring& shortname, List& m)
+bool haveNameClash(const Zstring& shortname, const List& m)
{
return std::any_of(m.begin(), m.end(),
[&](const typename List::value_type& obj) { return equalFilePath(obj.getPairItemName(), shortname); });
@@ -1512,7 +1505,7 @@ template <SelectedSide sideTrg>
void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //throw FileError, ThreadInterruption
{
constexpr SelectedSide sideSrc = OtherSide<sideTrg>::value;
- DeletionHandling& delHandlingTrg = SelectParam<sideTrg>::ref(delHandlingLeft_, delHandlingRight_);
+ DeletionHandler& delHandlerTrg = SelectParam<sideTrg>::ref(delHandlerLeft_, delHandlerRight_);
switch (syncOp)
{
@@ -1570,12 +1563,12 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp)
case SO_DELETE_LEFT:
case SO_DELETE_RIGHT:
- reportInfo(delHandlingTrg.getTxtRemovingFile(), AFS::getDisplayPath(file.getAbstractPath<sideTrg>())); //throw ThreadInterruption
+ reportInfo(delHandlerTrg.getTxtRemovingFile(), AFS::getDisplayPath(file.getAbstractPath<sideTrg>())); //throw ThreadInterruption
{
AsyncItemStatReporter statReporter(1, 0, acb_);
- delHandlingTrg.removeFileWithCallback({ file.getAbstractPath<sideTrg>(), file.getAttributes<sideTrg>() },
- file.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
+ delHandlerTrg.removeFileWithCallback({ file.getAbstractPath<sideTrg>(), file.getAttributes<sideTrg>() },
+ file.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
file.removeObject<sideTrg>(); //update FilePair
}
break;
@@ -1637,14 +1630,14 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp)
auto onDeleteTargetFile = [&] //delete target at appropriate time
{
- //reportStatus(this->delHandlingTrg.getTxtRemovingFile(), AFS::getDisplayPath(targetPathResolvedOld)); -> superfluous/confuses user
+ //reportStatus(this->delHandlerTrg.getTxtRemovingFile(), AFS::getDisplayPath(targetPathResolvedOld)); -> superfluous/confuses user
FileAttributes followedTargetAttr = file.getAttributes<sideTrg>();
followedTargetAttr.isFollowedSymlink = false;
- delHandlingTrg.removeFileWithCallback({ targetPathResolvedOld, followedTargetAttr }, file.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
+ delHandlerTrg.removeFileWithCallback({ targetPathResolvedOld, followedTargetAttr }, file.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
//no (logical) item count update desired - but total byte count may change, e.g. move(copy) old file to versioning dir
- statReporter.reportDelta(-1, 0); //undo item stats reporting within DeletionHandling::removeFileWithCallback()
+ statReporter.reportDelta(-1, 0); //undo item stats reporting within DeletionHandler::removeFileWithCallback()
//file.removeObject<sideTrg>(); -> doesn't make sense for isFollowedSymlink(); "file, sideTrg" evaluated below!
@@ -1736,7 +1729,7 @@ template <SelectedSide sideTrg>
void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation syncOp) //throw FileError, ThreadInterruption
{
constexpr SelectedSide sideSrc = OtherSide<sideTrg>::value;
- DeletionHandling& delHandlingTrg = SelectParam<sideTrg>::ref(delHandlingLeft_, delHandlingRight_);
+ DeletionHandler& delHandlerTrg = SelectParam<sideTrg>::ref(delHandlerLeft_, delHandlerRight_);
switch (syncOp)
{
@@ -1786,11 +1779,11 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy
case SO_DELETE_LEFT:
case SO_DELETE_RIGHT:
- reportInfo(delHandlingTrg.getTxtRemovingSymLink(), AFS::getDisplayPath(symlink.getAbstractPath<sideTrg>())); //throw ThreadInterruption
+ reportInfo(delHandlerTrg.getTxtRemovingSymLink(), AFS::getDisplayPath(symlink.getAbstractPath<sideTrg>())); //throw ThreadInterruption
{
AsyncItemStatReporter statReporter(1, 0, acb_);
- delHandlingTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
+ delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
symlink.removeObject<sideTrg>(); //update SymlinkPair
}
@@ -1802,9 +1795,9 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy
{
AsyncItemStatReporter statReporter(1, 0, acb_);
- //reportStatus(delHandlingTrg.getTxtRemovingSymLink(), AFS::getDisplayPath(symlink.getAbstractPath<sideTrg>()));
- delHandlingTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
- statReporter.reportDelta(-1, 0); //undo item stats reporting within DeletionHandling::removeLinkWithCallback()
+ //reportStatus(delHandlerTrg.getTxtRemovingSymLink(), AFS::getDisplayPath(symlink.getAbstractPath<sideTrg>()));
+ delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
+ statReporter.reportDelta(-1, 0); //undo item stats reporting within DeletionHandler::removeLinkWithCallback()
//symlink.removeObject<sideTrg>(); -> "symlink, sideTrg" evaluated below!
@@ -1880,7 +1873,7 @@ template <SelectedSide sideTrg>
void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation syncOp) //throw FileError, ThreadInterruption
{
constexpr SelectedSide sideSrc = OtherSide<sideTrg>::value;
- DeletionHandling& delHandlingTrg = SelectParam<sideTrg>::ref(delHandlingLeft_, delHandlingRight_);
+ DeletionHandler& delHandlerTrg = SelectParam<sideTrg>::ref(delHandlerLeft_, delHandlerRight_);
switch (syncOp)
{
@@ -1941,12 +1934,12 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy
case SO_DELETE_LEFT:
case SO_DELETE_RIGHT:
- reportInfo(delHandlingTrg.getTxtRemovingFolder(), AFS::getDisplayPath(folder.getAbstractPath<sideTrg>())); //throw ThreadInterruption
+ reportInfo(delHandlerTrg.getTxtRemovingFolder(), AFS::getDisplayPath(folder.getAbstractPath<sideTrg>())); //throw ThreadInterruption
{
const SyncStatistics subStats(folder); //counts sub-objects only!
AsyncItemStatReporter statReporter(1 + getCUD(subStats), subStats.getBytesToProcess(), acb_);
- delHandlingTrg.removeDirWithCallback(folder.getAbstractPath<sideTrg>(), folder.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
+ delHandlerTrg.removeDirWithCallback(folder.getAbstractPath<sideTrg>(), folder.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X
//TODO: implement parallel folder deletion
@@ -2104,7 +2097,7 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, int
if (Opt<AbstractPath> parentPath = AFS::getParentFolderPath(baseFolderPath))
if (AFS::getParentFolderPath(*parentPath)) //not device root
AFS::createFolderIfMissingRecursion(*parentPath); //throw FileError
-
+
AFS::copyNewFolder(baseFolder.getAbstractPath<sideSrc>(), baseFolderPath, copyFilePermissions); //throw FileError
}
else
@@ -2219,7 +2212,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//status of base directories which are set to DeletionPolicy::RECYCLER (and contain actual items to be deleted)
std::map<AbstractPath, bool> recyclerSupported; //expensive to determine on Win XP => buffer + check recycle bin existence only once per base folder!
- std::set<AbstractPath> verCheckVersioningPaths;
+ std::set<AbstractPath> verCheckVersioningPaths;
std::vector<std::pair<AbstractPath, const HardFilter*>> verCheckBaseFolderPaths; //hard filter creates new logical hierarchies for otherwise equal AbstractPath...
//start checking folder pairs
@@ -2562,17 +2555,17 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
};
const AbstractPath versioningFolderPath = createAbstractPath(folderPairCfg.versioningFolderPhrase);
- DeletionHandling delHandlerL(baseFolder.getAbstractPath<LEFT_SIDE>(),
- getEffectiveDeletionPolicy(baseFolder.getAbstractPath<LEFT_SIDE>()),
- versioningFolderPath,
- folderPairCfg.versioningStyle,
- std::chrono::system_clock::to_time_t(syncStartTime));
+ DeletionHandler delHandlerL(baseFolder.getAbstractPath<LEFT_SIDE>(),
+ getEffectiveDeletionPolicy(baseFolder.getAbstractPath<LEFT_SIDE>()),
+ versioningFolderPath,
+ folderPairCfg.versioningStyle,
+ std::chrono::system_clock::to_time_t(syncStartTime));
- DeletionHandling delHandlerR(baseFolder.getAbstractPath<RIGHT_SIDE>(),
- getEffectiveDeletionPolicy(baseFolder.getAbstractPath<RIGHT_SIDE>()),
- versioningFolderPath,
- folderPairCfg.versioningStyle,
- std::chrono::system_clock::to_time_t(syncStartTime));
+ DeletionHandler delHandlerR(baseFolder.getAbstractPath<RIGHT_SIDE>(),
+ getEffectiveDeletionPolicy(baseFolder.getAbstractPath<RIGHT_SIDE>()),
+ versioningFolderPath,
+ folderPairCfg.versioningStyle,
+ std::chrono::system_clock::to_time_t(syncStartTime));
//always (try to) clean up, even if synchronization is aborted!
ZEN_ON_SCOPE_EXIT(
@@ -2606,11 +2599,10 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
};
FolderPairSyncer::runSync(syncCtx, baseFolder, callback);
- //(try to gracefully) cleanup temporary Recycle bin folders and versioning -> will be done in ~DeletionHandling anyway...
+ //(try to gracefully) cleanup temporary Recycle Bin folders and versioning -> will be done in ~DeletionHandler anyway...
tryReportingError([&] { delHandlerL.tryCleanup(callback, true /*allowCallbackException*/); /*throw FileError*/}, callback); //throw X
tryReportingError([&] { delHandlerR.tryCleanup(callback, true ); /*throw FileError*/}, callback); //throw X
-
if (folderPairCfg.handleDeletion == DeletionPolicy::VERSIONING &&
folderPairCfg.versioningStyle != VersioningStyle::REPLACE)
versionLimitFolders.insert(
@@ -2640,7 +2632,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime
//-----------------------------------------------------------------------------------------------------
- applyVersioningLimit(versionLimitFolders, deviceParallelOps, callback);
+ applyVersioningLimit(versionLimitFolders, deviceParallelOps, callback); //throw X
//------------------- show warnings after end of synchronization --------------------------------------
diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp
index cdb203f4..b8344b44 100755
--- a/FreeFileSync/Source/base/versioning.cpp
+++ b/FreeFileSync/Source/base/versioning.cpp
@@ -53,9 +53,9 @@ std::pair<time_t, Zstring> fff::impl::parseVersionedFileName(const Zstring& file
//e.g. "2012-05-15 131513"
-time_t fff::impl::parseVersionedFolderName(const Zstring& fileName)
+time_t fff::impl::parseVersionedFolderName(const Zstring& folderName)
{
- const TimeComp tc = parseTime(Zstr("%Y-%m-%d %H%M%S"), fileName); //returns TimeComp() on error
+ const TimeComp tc = parseTime(Zstr("%Y-%m-%d %H%M%S"), folderName); //returns TimeComp() on error
const time_t t = localToTimeT(tc); //returns -1 on error
if (t == -1)
return 0;
@@ -81,6 +81,7 @@ AbstractPath FileVersioner::generateVersionedPath(const Zstring& relativePath) c
case VersioningStyle::TIMESTAMP_FILE: //assemble time-stamped version name
versionedRelPath = relativePath + Zstr(' ') + timeStamp_ + getDotExtension(relativePath);
assert(impl::parseVersionedFileName(versionedRelPath) == std::pair(syncStartTime_, relativePath));
+ (void)syncStartTime_; //silence clang's "unused variable" arning
break;
}
return AFS::appendRelPath(versioningFolderPath_, versionedRelPath);
@@ -126,7 +127,9 @@ void moveExistingItemToVersioning(const AbstractPath& sourcePath, const Abstract
//parent folder missing => create + retry
//parent folder existing => maybe created shortly after move attempt by parallel thread! => retry
AbstractPath intermediatePath = ps.existingPath;
- for (const Zstring& itemName : std::vector<Zstring>(ps.relPath.begin(), ps.relPath.end() - 1))
+
+ std::for_each(ps.relPath.begin(), ps.relPath.end() - 1, [&](const Zstring& itemName)
+ {
try
{
AFS::createFolderPlain(intermediatePath = AFS::appendRelPath(intermediatePath, itemName)); //throw FileError
@@ -136,12 +139,13 @@ void moveExistingItemToVersioning(const AbstractPath& sourcePath, const Abstract
try //already existing => possible, if moveExistingItemToVersioning() is run in parallel
{
if (AFS::getItemType(intermediatePath) != AFS::ItemType::FILE) //throw FileError
- continue;
+ return; //=continue
}
catch (FileError&) {}
throw;
}
+ });
};
try //first try to move directly without copying
@@ -408,8 +412,10 @@ bool fff::operator<(const VersioningLimitFolder& lhs, const VersioningLimitFolde
void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolders,
const std::map<AbstractPath, size_t>& deviceParallelOps,
- ProcessCallback& callback)
+ ProcessCallback& callback /*throw X*/)
{
+ warn_static("what if folder does not yet exist?")
+
//--------- traverse all versioning folders ---------
std::set<DirectoryKey> foldersToRead;
for (const VersioningLimitFolder& vlf : limitFolders)
@@ -418,7 +424,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde
auto onError = [&](const std::wstring& msg, size_t retryNumber)
{
- switch (callback.reportError(msg, retryNumber))
+ switch (callback.reportError(msg, retryNumber)) //throw X
{
case ProcessCallback::IGNORE_ERROR:
return AFS::TraverserCallback::ON_ERROR_CONTINUE;
@@ -441,7 +447,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde
parallelDeviceTraversal(foldersToRead, folderBuf,
deviceParallelOps,
- onError, onStatusUpdate,
+ onError, onStatusUpdate, //throw X
UI_UPDATE_INTERVAL / 2); //every ~50 ms
//--------- group versions per (original) relative path ---------
@@ -503,7 +509,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde
if (vlf.versionCountMax > 0)
versionsToKeep = std::min<size_t>(versionsToKeep, vlf.versionCountMax);
- if (versionsToKeep < versions.size())
+ if (versions.size() > versionsToKeep)
{
std::nth_element(versions.begin(), versions.end() - versionsToKeep, versions.end(),
[](const VersionInfo& lhs, const VersionInfo& rhs) { return lhs.versionTime < rhs.versionTime; });
@@ -527,7 +533,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde
const std::wstring errMsg = tryReportingError([&] //throw ThreadInterruption
{
ctx.acb.reportStatus(replaceCpy(textDeletingFolder, L"%x", fmtPath(AFS::getDisplayPath(ctx.itemPath)))); //throw ThreadInterruption
- AFS::removeEmptyFolderfExists(ctx.itemPath); //throw FileError
+ AFS::removeEmptyFolderIfExists(ctx.itemPath); //throw FileError
}, ctx.acb);
if (errMsg.empty())
diff --git a/FreeFileSync/Source/base/versioning.h b/FreeFileSync/Source/base/versioning.h
index 84f4627e..835ba67e 100755
--- a/FreeFileSync/Source/base/versioning.h
+++ b/FreeFileSync/Source/base/versioning.h
@@ -104,13 +104,13 @@ bool operator<(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rh
void applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolders,
const std::map<AbstractPath, size_t>& deviceParallelOps,
- ProcessCallback& callback);
+ ProcessCallback& callback /*throw X*/);
namespace impl //declare for unit tests:
{
std::pair<time_t, Zstring> parseVersionedFileName (const Zstring& fileName);
-time_t parseVersionedFolderName(const Zstring& fileName);
+time_t parseVersionedFolderName(const Zstring& folderName);
}
}
diff --git a/FreeFileSync/Source/fs/abstract.cpp b/FreeFileSync/Source/fs/abstract.cpp
index 3b74fab5..d53019be 100755
--- a/FreeFileSync/Source/fs/abstract.cpp
+++ b/FreeFileSync/Source/fs/abstract.cpp
@@ -469,7 +469,7 @@ bool AFS::removeSymlinkIfExists(const AbstractPath& ap) //throw FileError
}
-void AFS::removeEmptyFolderfExists(const AbstractPath& ap) //throw FileError
+void AFS::removeEmptyFolderIfExists(const AbstractPath& ap) //throw FileError
{
try
{
diff --git a/FreeFileSync/Source/fs/abstract.h b/FreeFileSync/Source/fs/abstract.h
index 6c05e419..d5b08c73 100755
--- a/FreeFileSync/Source/fs/abstract.h
+++ b/FreeFileSync/Source/fs/abstract.h
@@ -99,7 +99,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t
static bool removeFileIfExists (const AbstractPath& ap); //throw FileError; return "false" if file is not existing
static bool removeSymlinkIfExists(const AbstractPath& ap); //
- static void removeEmptyFolderfExists(const AbstractPath& ap); //throw FileError
+ static void removeEmptyFolderIfExists(const AbstractPath& ap); //throw FileError
static void removeFolderIfExistsRecursion(const AbstractPath& ap, //throw FileError
const std::function<void (const std::wstring& displayPath)>& onBeforeFileDeletion, //optional
const std::function<void (const std::wstring& displayPath)>& onBeforeFolderDeletion); //one call for each object!
diff --git a/FreeFileSync/Source/fs/native.cpp b/FreeFileSync/Source/fs/native.cpp
index ab859d1f..ad8f9be1 100755
--- a/FreeFileSync/Source/fs/native.cpp
+++ b/FreeFileSync/Source/fs/native.cpp
@@ -522,7 +522,10 @@ private:
ZEN_ON_SCOPE_FAIL(try { removeDirectoryPlain(targetPath); }
catch (FileError&) {});
- tryCopyDirectoryAttributes(sourcePath, targetPath); //throw FileError
+ //do NOT copy attributes for volume root paths which return as: FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_DIRECTORY
+ //https://freefilesync.org/forum/viewtopic.php?t=5550
+ if (getParentAfsPath(afsPathSource)) //=> not a root path
+ tryCopyDirectoryAttributes(sourcePath, targetPath); //throw FileError
if (copyFilePermissions)
copyItemPermissions(sourcePath, targetPath, ProcSymlink::FOLLOW); //throw FileError
diff --git a/FreeFileSync/Source/ui/batch_config.cpp b/FreeFileSync/Source/ui/batch_config.cpp
index d0135d53..83d297d5 100755
--- a/FreeFileSync/Source/ui/batch_config.cpp
+++ b/FreeFileSync/Source/ui/batch_config.cpp
@@ -11,6 +11,7 @@
#include <wx+/image_resources.h>
#include <wx+/image_tools.h>
#include <wx+/choice_enum.h>
+#include <wx+/popup_dlg.h>
#include "gui_generated.h"
#include "folder_selector.h"
#include "../base/help_provider.h"
@@ -87,7 +88,7 @@ BatchDialog::BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg) :
[](const Zstring& folderPathPhrase) { return 1; } /*getDeviceParallelOps*/,
nullptr /*setDeviceParallelOps*/);
- logfileDir_->setBackgroundText(utfTo<std::wstring>(getDefaultLogFolderPath()));
+ //logfileDir_->setBackgroundText(utfTo<std::wstring>(getDefaultLogFolderPath()));
enumPostSyncAction_.
add(PostSyncAction::NONE, L"").
@@ -96,6 +97,15 @@ BatchDialog::BatchDialog(wxWindow* parent, BatchDialogConfig& dlgCfg) :
setConfig(dlgCfg);
+ warn_static("consider for removal after FFS 10.3 release")
+#if 1
+ m_panelLogfile->Hide();
+ m_bitmapLogFile->Hide();
+ m_checkBoxSaveLog->Hide();
+ m_checkBoxLogfilesLimit->Hide();
+ m_spinCtrlLogfileLimit->Hide();
+#endif
+
//enable dialog-specific key events
Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(BatchDialog::onLocalKeyEvent), nullptr, this);
@@ -116,11 +126,9 @@ void BatchDialog::updateGui() //re-evaluate gui after config changes
m_radioBtnErrorDialogShow ->Enable(!dlgCfg.ignoreErrors);
m_radioBtnErrorDialogCancel->Enable(!dlgCfg.ignoreErrors);
-
m_bitmapMinimizeToTray->SetBitmap(dlgCfg.batchExCfg.runMinimized ? getResourceImage(L"minimize_to_tray") : greyScale(getResourceImage(L"minimize_to_tray")));
-
- m_panelLogfile->Enable(m_checkBoxSaveLog->GetValue()); //enabled status is *not* directly dependent from resolved config! (but transitively)
+ m_panelLogfile ->Enable (m_checkBoxSaveLog->GetValue()); //enabled status is *not* directly dependent from resolved config! (but transitively)
m_bitmapLogFile->SetBitmap(m_checkBoxSaveLog->GetValue() ? getResourceImage(L"log_file") : greyScale(getResourceImage(L"log_file")));
m_checkBoxLogfilesLimit->Enable(m_checkBoxSaveLog->GetValue());
m_spinCtrlLogfileLimit ->Enable(m_checkBoxSaveLog->GetValue() && m_checkBoxLogfilesLimit->GetValue());
@@ -136,12 +144,12 @@ void BatchDialog::setConfig(const BatchDialogConfig& dlgCfg)
m_radioBtnErrorDialogShow ->SetValue(false);
m_radioBtnErrorDialogCancel->SetValue(false);
- switch (dlgCfg.batchExCfg.batchErrorDialog)
+ switch (dlgCfg.batchExCfg.batchErrorHandling)
{
- case BatchErrorDialog::SHOW:
+ case BatchErrorHandling::SHOW_POPUP:
m_radioBtnErrorDialogShow->SetValue(true);
break;
- case BatchErrorDialog::CANCEL:
+ case BatchErrorHandling::CANCEL:
m_radioBtnErrorDialogCancel->SetValue(true);
break;
}
@@ -149,12 +157,11 @@ void BatchDialog::setConfig(const BatchDialogConfig& dlgCfg)
m_checkBoxRunMinimized->SetValue(dlgCfg.batchExCfg.runMinimized);
m_checkBoxAutoClose ->SetValue(dlgCfg.batchExCfg.autoCloseSummary);
setEnumVal(enumPostSyncAction_, *m_choicePostSyncAction, dlgCfg.batchExCfg.postSyncAction);
- logfileDir_->setPath(dlgCfg.batchExCfg.logFolderPathPhrase);
- //map single parameter "logfiles limit" to all three checkboxs and spin ctrl:
- m_checkBoxSaveLog ->SetValue(dlgCfg.batchExCfg.logfilesCountLimit != 0);
- m_checkBoxLogfilesLimit->SetValue(dlgCfg.batchExCfg.logfilesCountLimit > 0);
- m_spinCtrlLogfileLimit ->SetValue(dlgCfg.batchExCfg.logfilesCountLimit > 0 ? dlgCfg.batchExCfg.logfilesCountLimit : 100 /*XmlBatchConfig().logfilesCountLimit*/);
+ logfileDir_->setPath(dlgCfg.batchExCfg.altLogFolderPathPhrase);
+ m_checkBoxSaveLog ->SetValue(dlgCfg.batchExCfg.altLogfileCountMax != 0);
+ m_checkBoxLogfilesLimit->SetValue(dlgCfg.batchExCfg.altLogfileCountMax > 0);
+ m_spinCtrlLogfileLimit ->SetValue(dlgCfg.batchExCfg.altLogfileCountMax > 0 ? dlgCfg.batchExCfg.altLogfileCountMax : 100);
//attention: emits a "change value" event!! => updateGui() called implicitly!
updateGui(); //re-evaluate gui after config changes
@@ -167,14 +174,18 @@ BatchDialogConfig BatchDialog::getConfig() const
dlgCfg.ignoreErrors = m_checkBoxIgnoreErrors->GetValue();
- dlgCfg.batchExCfg.batchErrorDialog = m_radioBtnErrorDialogCancel->GetValue() ? BatchErrorDialog::CANCEL : BatchErrorDialog::SHOW;
+ dlgCfg.batchExCfg.batchErrorHandling = m_radioBtnErrorDialogCancel->GetValue() ? BatchErrorHandling::CANCEL : BatchErrorHandling::SHOW_POPUP;
dlgCfg.batchExCfg.runMinimized = m_checkBoxRunMinimized->GetValue();
dlgCfg.batchExCfg.autoCloseSummary = m_checkBoxAutoClose ->GetValue();
dlgCfg.batchExCfg.postSyncAction = getEnumVal(enumPostSyncAction_, *m_choicePostSyncAction);
- dlgCfg.batchExCfg.logFolderPathPhrase = utfTo<Zstring>(logfileDir_->getPath());
- dlgCfg.batchExCfg.logfilesCountLimit = m_checkBoxSaveLog->GetValue() ? (m_checkBoxLogfilesLimit->GetValue() ? m_spinCtrlLogfileLimit->GetValue() : -1) : 0;
- //get single parameter "logfiles limit" from all three checkboxes and spin ctrl
+ dlgCfg.batchExCfg.altLogFolderPathPhrase = utfTo<Zstring>(logfileDir_->getPath());
+ dlgCfg.batchExCfg.altLogfileCountMax = m_checkBoxSaveLog->GetValue() ? (m_checkBoxLogfilesLimit->GetValue() ? m_spinCtrlLogfileLimit->GetValue() : -1) : 0;
+
+ warn_static("consider for removal after FFS 10.3 release")
+#if 1
+ dlgCfg.batchExCfg.altLogfileCountMax = 0;
+#endif
return dlgCfg;
}
@@ -188,6 +199,22 @@ void BatchDialog::onLocalKeyEvent(wxKeyEvent& event)
void BatchDialog::OnSaveBatchJob(wxCommandEvent& event)
{
+ BatchDialogConfig dlgCfg = getConfig();
+
+ //------- parameter validation (BEFORE writing output!) -------
+ warn_static("consider for removal after FFS 10.3 release")
+#if 0
+ if (dlgCfg.batchExCfg.altLogfileCountMax != 0 &&
+ trimCpy(dlgCfg.batchExCfg.altLogFolderPathPhrase).empty())
+ {
+ showNotificationDialog(this, DialogInfoType::INFO, PopupDialogCfg().setMainInstructions(_("A folder input field is empty.")));
+ //don't show error icon to follow "Windows' encouraging tone"
+ m_logFolderPath->SetFocus();
+ return;
+ }
+#endif
+ //-------------------------------------------------------------
+
dlgCfgOut_ = getConfig();
EndModal(ReturnBatchConfig::BUTTON_SAVE_AS);
}
diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp
index 66459d8d..04201a9d 100755
--- a/FreeFileSync/Source/ui/batch_status_handler.cpp
+++ b/FreeFileSync/Source/ui/batch_status_handler.cpp
@@ -6,13 +6,10 @@
#include "batch_status_handler.h"
#include <zen/shell_execute.h>
-#include <zen/thread.h>
#include <zen/shutdown.h>
#include <wx+/popup_dlg.h>
#include <wx/app.h>
-#include "../base/ffs_paths.h"
#include "../base/resolve_path.h"
-#include "../base/status_handler_impl.h"
#include "../base/generate_logfile.h"
#include "../fs/concrete.h"
@@ -20,78 +17,21 @@ using namespace zen;
using namespace fff;
-namespace
-{
-//"Backup FreeFileSync 2013-09-15 015052.123.log" ->
-//"Backup FreeFileSync 2013-09-15 015052.123 [Error].log"
-
-
-//return value always bound!
-std::unique_ptr<AFS::OutputStream> prepareNewLogfile(const AbstractPath& logFolderPath, //throw FileError
- const std::wstring& jobName,
- const std::chrono::system_clock::time_point& syncStartTime,
- const std::wstring& failStatus,
- const std::function<void(const std::wstring& msg)>& notifyStatus)
-{
- assert(!jobName.empty());
-
- //create logfile folder if required
- AFS::createFolderIfMissingRecursion(logFolderPath); //throw FileError
-
- //const std::string colon = "\xcb\xb8"; //="modifier letter raised colon" => regular colon is forbidden in file names on Windows and OS X
- //=> too many issues, most notably cmd.exe is not Unicode-aware: https://freefilesync.org/forum/viewtopic.php?t=1679
-
- //assemble logfile name
- const TimeComp tc = getLocalTime(std::chrono::system_clock::to_time_t(syncStartTime));
- if (tc == TimeComp())
- throw FileError(L"Failed to determine current time: " + numberTo<std::wstring>(syncStartTime.time_since_epoch().count()));
-
- const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(syncStartTime.time_since_epoch()).count() % 1000;
-
- Zstring logFileName = utfTo<Zstring>(jobName) +
- Zstr(" ") + formatTime<Zstring>(Zstr("%Y-%m-%d %H%M%S"), tc) +
- Zstr(".") + printNumber<Zstring>(Zstr("%03d"), static_cast<int>(timeMs)); //[ms] should yield a fairly unique name
- if (!failStatus.empty())
- logFileName += utfTo<Zstring>(L" [" + failStatus + L"]");
- logFileName += Zstr(".log");
-
- const AbstractPath logFilePath = AFS::appendRelPath(logFolderPath, logFileName);
-
- auto notifyUnbufferedIO = [notifyStatus,
- bytesWritten_ = int64_t(0),
- msg_ = replaceCpy(_("Saving file %x..."), L"%x", fmtPath(AFS::getDisplayPath(logFilePath)))]
- (int64_t bytesDelta) mutable
- {
- if (notifyStatus)
- notifyStatus(msg_ + L" (" + formatFilesizeShort(bytesWritten_ += bytesDelta) + L")"); /*throw X*/
- };
-
- return AFS::getOutputStream(logFilePath, nullptr, /*streamSize*/ notifyUnbufferedIO); //throw FileError
-}
-}
-
-//##############################################################################################################################
-
BatchStatusHandler::BatchStatusHandler(bool showProgress,
bool autoCloseDialog,
const std::wstring& jobName,
const Zstring& soundFileSyncComplete,
const std::chrono::system_clock::time_point& startTime,
- const Zstring& logFolderPathPhrase, //may be empty
- int logfilesCountLimit,
- size_t lastSyncsLogFileSizeMax,
+ int altLogfileCountMax, //0: logging inactive; < 0: no limit
+ const Zstring& altLogFolderPathPhrase,
bool ignoreErrors,
- BatchErrorDialog batchErrorDialog,
+ BatchErrorHandling batchErrorHandling,
size_t automaticRetryCount,
size_t automaticRetryDelay,
- FfsReturnCode& returnCode,
const Zstring& postSyncCommand,
PostSyncCondition postSyncCondition,
PostSyncAction postSyncAction) :
- logfilesCountLimit_(logfilesCountLimit),
- lastSyncsLogFileSizeMax_(lastSyncsLogFileSizeMax),
- batchErrorDialog_(batchErrorDialog),
- returnCode_(returnCode),
+ batchErrorHandling_(batchErrorHandling),
automaticRetryCount_(automaticRetryCount),
automaticRetryDelay_(automaticRetryDelay),
progressDlg_(createProgressDialog(*this, [this] { this->onProgressDialogTerminate(); }, *this, nullptr /*parentWindow*/, showProgress, autoCloseDialog,
@@ -111,9 +51,10 @@ jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, [&]
}())),
jobName_(jobName),
startTime_(startTime),
- logFolderPathPhrase_(logFolderPathPhrase),
postSyncCommand_(postSyncCommand),
- postSyncCondition_(postSyncCondition)
+ postSyncCondition_(postSyncCondition),
+ altLogfileCountMax_(altLogfileCountMax),
+ altLogFolderPathPhrase_(altLogFolderPathPhrase)
{
//ATTENTION: "progressDlg_" is an unmanaged resource!!! However, at this point we already consider construction complete! =>
//ZEN_ON_SCOPE_FAIL( cleanup(); ); //destructor call would lead to member double clean-up!!!
@@ -127,46 +68,45 @@ jobName_(jobName),
BatchStatusHandler::~BatchStatusHandler()
{
- const int totalErrors = errorLog_.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR); //evaluate before finalizing log
- const int totalWarnings = errorLog_.getItemCount(MSG_TYPE_WARNING);
-
- //finalize error log
- SyncProgressDialog::SyncResult finalStatus = SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS;
- std::wstring finalStatusMsg;
- std::wstring failStatus; //additionally indicate errors in log file name
- if (getAbortStatus())
- {
- finalStatus = SyncProgressDialog::RESULT_ABORTED;
- raiseReturnCode(returnCode_, FFS_RC_ABORTED);
- finalStatusMsg = _("Stopped");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_ERROR);
- failStatus = _("Stopped");
- }
- else if (totalErrors > 0)
- {
- finalStatus = SyncProgressDialog::RESULT_FINISHED_WITH_ERROR;
- raiseReturnCode(returnCode_, FFS_RC_FINISHED_WITH_ERRORS);
- finalStatusMsg = _("Completed with errors");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_ERROR);
- failStatus = _("Error");
- }
- else if (totalWarnings > 0)
- {
- finalStatus = SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS;
- raiseReturnCode(returnCode_, FFS_RC_FINISHED_WITH_WARNINGS);
- finalStatusMsg = _("Completed with warnings");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_WARNING);
- failStatus = _("Warning");
- }
- else
+ if (progressDlg_) //reportFinalStatus() was not called!
+ std::abort();
+}
+
+
+BatchStatusHandler::Result BatchStatusHandler::reportFinalStatus(int logfilesMaxAgeDays, const std::set<Zstring, LessFilePath>& logFilePathsToKeep) //noexcept!!
+{
+ const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_);
+
+ if (progressDlg_) progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep
+
+ //determine post-sync status irrespective of further errors during tear-down
+ const SyncResult finalStatus = [&]
{
- if (getItemsTotal(PHASE_SYNCHRONIZING) == 0 && //we're past "initNewPhase(PHASE_SYNCHRONIZING)" at this point!
- getBytesTotal(PHASE_SYNCHRONIZING) == 0)
- finalStatusMsg = _("Nothing to synchronize"); //even if "ignored conflicts" occurred!
+ if (getAbortStatus())
+ return SyncResult::ABORTED;
+ else if (errorLog_.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR) > 0)
+ return SyncResult::FINISHED_WITH_ERROR;
+ else if (errorLog_.getItemCount(MSG_TYPE_WARNING) > 0)
+ return SyncResult::FINISHED_WITH_WARNINGS;
else
- finalStatusMsg = _("Completed successfully");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_INFO);
- }
+ return SyncResult::FINISHED_WITH_SUCCESS;
+ }();
+
+ assert(finalStatus == SyncResult::ABORTED || currentPhase() == PHASE_SYNCHRONIZING);
+
+ ProcessSummary summary
+ {
+ finalStatus, jobName_,
+ getStatsCurrent(currentPhase()),
+ getStatsTotal (currentPhase()),
+ totalTime
+ };
+
+ const std::wstring& finalStatusLabel = finalStatus == SyncResult::FINISHED_WITH_SUCCESS &&
+ summary.statsTotal.items == 0 &&
+ summary.statsTotal.bytes == 0 ? _("Nothing to synchronize") :
+ getFinalStatusLabel(finalStatus);
+ errorLog_.logMsg(finalStatusLabel, getFinalMsgType(finalStatus));
//post sync command
Zstring commandLine = [&]
@@ -179,13 +119,13 @@ BatchStatusHandler::~BatchStatusHandler()
case PostSyncCondition::COMPLETION:
return postSyncCommand_;
case PostSyncCondition::ERRORS:
- if (finalStatus == SyncProgressDialog::RESULT_ABORTED ||
- finalStatus == SyncProgressDialog::RESULT_FINISHED_WITH_ERROR)
+ if (finalStatus == SyncResult::ABORTED ||
+ finalStatus == SyncResult::FINISHED_WITH_ERROR)
return postSyncCommand_;
break;
case PostSyncCondition::SUCCESS:
- if (finalStatus == SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS ||
- finalStatus == SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS)
+ if (finalStatus == SyncResult::FINISHED_WITH_WARNINGS ||
+ finalStatus == SyncResult::FINISHED_WITH_SUCCESS)
return postSyncCommand_;
break;
}
@@ -196,57 +136,33 @@ BatchStatusHandler::~BatchStatusHandler()
if (!commandLine.empty())
errorLog_.logMsg(replaceCpy(_("Executing command %x"), L"%x", fmtPath(commandLine)), MSG_TYPE_INFO);
- //----------------- write results into user-specified logfile ------------------------
- const LogSummary summary =
- {
- jobName_,
- finalStatusMsg,
- getItemsCurrent(PHASE_SYNCHRONIZING), getBytesCurrent(PHASE_SYNCHRONIZING),
- getItemsTotal (PHASE_SYNCHRONIZING), getBytesTotal (PHASE_SYNCHRONIZING),
- std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - startTime_).count()
- };
- if (progressDlg_) progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep
-
- //do NOT use tryReportingError()! saving log files should not be cancellable!
- auto notifyStatusNoThrow = [&](const std::wstring& msg)
- {
- try { reportStatus(msg); /*throw X*/ }
- catch (...) {}
- };
-
- //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. simplify transactional retry on failure 3. no need to rename log file to include status
+ //----------------- always save log under %appdata%\FreeFileSync\Logs ------------------------
+ //create not before destruction: 1. avoid issues with FFS trying to sync open log file 2. simplify transactional retry on failure 3. include status in log file name without rename
// 4. failure to write to particular stream must not be retried!
- if (logfilesCountLimit_ != 0)
- {
- const AbstractPath logFolderPath = createAbstractPath(trimCpy(logFolderPathPhrase_).empty() ? getDefaultLogFolderPath() : logFolderPathPhrase_); //noexcept
-
- try
- {
- std::unique_ptr<AFS::OutputStream> logFileStream = prepareNewLogfile(logFolderPath, jobName_, startTime_, failStatus, notifyStatusNoThrow); //throw FileError; return value always bound!
-
- streamToLogFile(summary, errorLog_, *logFileStream); //throw FileError, (X)
- logFileStream->finalize(); //throw FileError, (X)
- }
- catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
-
- if (logfilesCountLimit_ > 0)
- try
- {
- limitLogfileCount(logFolderPath, jobName_, logfilesCountLimit_, notifyStatusNoThrow); //throw FileError, (X)
- }
- catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
- }
-
+ Zstring logFilePath;
try
{
- saveToLastSyncsLog(summary, errorLog_, lastSyncsLogFileSizeMax_, notifyStatusNoThrow); //throw FileError, (X)
+ //do NOT use tryReportingError()! saving log files should not be cancellable!
+ auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { reportStatus(msg); /*throw X*/ } catch (...) {} };
+ logFilePath = saveLogFile(summary, errorLog_, startTime_, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatusNoThrow /*throw X*/); //throw FileError
}
catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
+ warn_static("consider for removal after FFS 10.3 release")
+#if 1
+ ////save additional logfile copy to user-defined path if requested
+ //doSaveLogFile(altLogFolderPathPhrase_, altLogfileCountMax_);
+ (void)altLogFolderPathPhrase_;
+ (void)altLogfileCountMax_;
+#endif
+
//execute post sync command *after* writing log files, so that user can refer to the log via the command!
if (!commandLine.empty())
try
{
+ //----------------------------------------------------------------------
+ ::wxSetEnv(L"logfile_path", utfTo<wxString>(logFilePath));
+ //----------------------------------------------------------------------
//use ExecutionType::ASYNC until there is reason not to: https://freefilesync.org/forum/viewtopic.php?t=31
shellExecute(expandMacros(commandLine), ExecutionType::ASYNC); //throw FileError
}
@@ -254,33 +170,34 @@ BatchStatusHandler::~BatchStatusHandler()
if (progressDlg_)
{
- auto mayRunAfterCountDown = [&](const std::wstring& operationName)
- {
- auto notifyStatusThrowOnCancel = [&](const std::wstring& msg)
- {
- try { reportStatus(msg); /*throw X*/ }
- catch (...)
- {
- if (getAbortStatus() && *getAbortStatus() == AbortTrigger::USER)
- throw;
- }
- };
-
- if (progressDlg_->getWindowIfVisible())
- try
- {
- delayAndCountDown(operationName, 5 /*delayInSec*/, notifyStatusThrowOnCancel); //throw X
- }
- catch (...) { return false; }
-
- return true;
- };
-
//post sync action
bool autoClose = false;
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::USER)
; //user cancelled => don't run post sync command!
else
+ {
+ auto mayRunAfterCountDown = [&](const std::wstring& operationName)
+ {
+ auto notifyStatusThrowOnCancel = [&](const std::wstring& msg)
+ {
+ try { reportStatus(msg); /*throw X*/ }
+ catch (...)
+ {
+ if (getAbortStatus() && *getAbortStatus() == AbortTrigger::USER)
+ throw;
+ }
+ };
+
+ if (progressDlg_->getWindowIfVisible())
+ try
+ {
+ delayAndCountDown(operationName, 5 /*delayInSec*/, notifyStatusThrowOnCancel); //throw X
+ }
+ catch (...) { return false; }
+
+ return true;
+ };
+
switch (progressDlg_->getOptionPostSyncAction())
{
case PostSyncAction2::NONE:
@@ -308,16 +225,19 @@ BatchStatusHandler::~BatchStatusHandler()
catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
break;
}
+ }
if (switchToGuiRequested_) //-> avoid recursive yield() calls, thous switch not before ending batch mode
autoClose = true;
+ auto errorLogFinal = std::make_shared<const ErrorLog>(std::move(errorLog_));
+
//close progress dialog
if (autoClose) //warning: wxWindow::Show() is called within showSummary()!
progressDlg_->closeDirectly(true /*restoreParentFrame: n/a here*/); //progressDlg_ is main window => program will quit shortly after
else
//notify about (logical) application main window => program won't quit, but stay on this dialog
//setMainWindow(progressDlg_->getAsWindow()); -> not required anymore since we block waiting until dialog is closed below
- progressDlg_->showSummary(finalStatus, errorLog_);
+ progressDlg_->showSummary(finalStatus, errorLogFinal);
//wait until progress dialog notified shutdown via onProgressDialogTerminate()
//-> required since it has our "this" pointer captured in lambda "notifyWindowTerminate"!
@@ -329,6 +249,8 @@ BatchStatusHandler::~BatchStatusHandler()
std::this_thread::sleep_for(UI_UPDATE_INTERVAL);
}
}
+
+ return { finalStatus, switchToGuiRequested_, logFilePath };
}
@@ -346,9 +268,9 @@ void BatchStatusHandler::updateDataProcessed(int itemsDelta, int64_t bytesDelta)
{
StatusHandler::updateDataProcessed(itemsDelta, bytesDelta);
- if (progressDlg_)
- progressDlg_->notifyProgressChange(); //noexcept
//note: this method should NOT throw in order to properly allow undoing setting of statistics!
+ if (progressDlg_) progressDlg_->notifyProgressChange(); //noexcept
+ //for "curveDataBytes_->addRecord()"
}
@@ -358,26 +280,26 @@ void BatchStatusHandler::logInfo(const std::wstring& msg)
}
-void BatchStatusHandler::reportWarning(const std::wstring& warningMessage, bool& warningActive)
+void BatchStatusHandler::reportWarning(const std::wstring& msg, bool& warningActive)
{
if (!progressDlg_) abortProcessNow();
+ PauseTimers dummy(*progressDlg_);
- errorLog_.logMsg(warningMessage, MSG_TYPE_WARNING);
+ errorLog_.logMsg(msg, MSG_TYPE_WARNING);
if (!warningActive)
return;
if (!progressDlg_->getOptionIgnoreErrors())
- switch (batchErrorDialog_)
+ switch (batchErrorHandling_)
{
- case BatchErrorDialog::SHOW:
+ case BatchErrorHandling::SHOW_POPUP:
{
- PauseTimers dummy(*progressDlg_);
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
bool dontWarnAgain = false;
- switch (showQuestionDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::WARNING, PopupDialogCfg().
- setDetailInstructions(warningMessage + L"\n\n" + _("You can switch to FreeFileSync's main window to resolve this issue.")).
+ switch (showQuestionDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::WARNING,
+ PopupDialogCfg().setDetailInstructions(msg + L"\n\n" + _("You can switch to FreeFileSync's main window to resolve this issue.")).
setCheckBox(dontWarnAgain, _("&Don't show this warning again"), QuestionButton2::NO),
_("&Ignore"), _("&Switch")))
{
@@ -388,8 +310,7 @@ void BatchStatusHandler::reportWarning(const std::wstring& warningMessage, bool&
case QuestionButton2::NO: //switch
errorLog_.logMsg(_("Switching to FreeFileSync's main window"), MSG_TYPE_INFO);
switchToGuiRequested_ = true; //treat as a special kind of cancel
- userRequestAbort(); //
- throw BatchRequestSwitchToMainDialog();
+ userAbortProcessNow(); //throw AbortProcess
case QuestionButton2::CANCEL:
userAbortProcessNow(); //throw AbortProcess
@@ -398,39 +319,39 @@ void BatchStatusHandler::reportWarning(const std::wstring& warningMessage, bool&
}
break; //keep it! last switch might not find match
- case BatchErrorDialog::CANCEL:
+ case BatchErrorHandling::CANCEL:
abortProcessNow(); //not user-initiated! throw AbortProcess
break;
}
}
-ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& errorMessage, size_t retryNumber)
+ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& msg, size_t retryNumber)
{
if (!progressDlg_) abortProcessNow();
+ PauseTimers dummy(*progressDlg_);
//auto-retry
if (retryNumber < automaticRetryCount_)
{
- errorLog_.logMsg(errorMessage + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO);
- delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& msg) { this->reportStatus(_("Error") + L": " + msg); });
+ errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO);
+ delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); });
return ProcessCallback::RETRY;
}
//always, except for "retry":
- auto guardWriteLog = makeGuard<ScopeGuardRunMode::ON_EXIT>([&] { errorLog_.logMsg(errorMessage, MSG_TYPE_ERROR); });
+ auto guardWriteLog = makeGuard<ScopeGuardRunMode::ON_EXIT>([&] { errorLog_.logMsg(msg, MSG_TYPE_ERROR); });
if (!progressDlg_->getOptionIgnoreErrors())
{
- switch (batchErrorDialog_)
+ switch (batchErrorHandling_)
{
- case BatchErrorDialog::SHOW:
+ case BatchErrorHandling::SHOW_POPUP:
{
- PauseTimers dummy(*progressDlg_);
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
- switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::ERROR2, PopupDialogCfg().
- setDetailInstructions(errorMessage),
+ switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::ERROR2,
+ PopupDialogCfg().setDetailInstructions(msg),
_("&Ignore"), _("Ignore &all"), _("&Retry")))
{
case ConfirmationButton3::ACCEPT: //ignore
@@ -442,7 +363,7 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& er
case ConfirmationButton3::DECLINE: //retry
guardWriteLog.dismiss();
- errorLog_.logMsg(errorMessage + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO);
+ errorLog_.logMsg(msg + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO);
return ProcessCallback::RETRY;
case ConfirmationButton3::CANCEL:
@@ -452,7 +373,7 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& er
}
break; //used if last switch didn't find a match
- case BatchErrorDialog::CANCEL:
+ case BatchErrorHandling::CANCEL:
abortProcessNow(); //not user-initiated! throw AbortProcess
break;
}
@@ -465,23 +386,23 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& er
}
-void BatchStatusHandler::reportFatalError(const std::wstring& errorMessage)
+void BatchStatusHandler::reportFatalError(const std::wstring& msg)
{
if (!progressDlg_) abortProcessNow();
+ PauseTimers dummy(*progressDlg_);
- errorLog_.logMsg(errorMessage, MSG_TYPE_FATAL_ERROR);
+ errorLog_.logMsg(msg, MSG_TYPE_FATAL_ERROR);
if (!progressDlg_->getOptionIgnoreErrors())
- switch (batchErrorDialog_)
+ switch (batchErrorHandling_)
{
- case BatchErrorDialog::SHOW:
+ case BatchErrorHandling::SHOW_POPUP:
{
- PauseTimers dummy(*progressDlg_);
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::ERROR2,
PopupDialogCfg().setTitle(_("Serious Error")).
- setDetailInstructions(errorMessage),
+ setDetailInstructions(msg),
_("&Ignore"), _("Ignore &all")))
{
case ConfirmationButton2::ACCEPT:
@@ -498,7 +419,7 @@ void BatchStatusHandler::reportFatalError(const std::wstring& errorMessage)
}
break;
- case BatchErrorDialog::CANCEL:
+ case BatchErrorHandling::CANCEL:
abortProcessNow(); //not user-initiated! throw AbortProcess
break;
}
diff --git a/FreeFileSync/Source/ui/batch_status_handler.h b/FreeFileSync/Source/ui/batch_status_handler.h
index baf7f102..3ed11e4d 100755
--- a/FreeFileSync/Source/ui/batch_status_handler.h
+++ b/FreeFileSync/Source/ui/batch_status_handler.h
@@ -12,54 +12,55 @@
#include "progress_indicator.h"
#include "../base/status_handler.h"
#include "../base/process_xml.h"
-#include "../base/return_codes.h"
+//#include "../base/return_codes.h"
namespace fff
{
-class BatchRequestSwitchToMainDialog {};
-
-
//BatchStatusHandler(SyncProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks!
-class BatchStatusHandler : public StatusHandler //throw AbortProcess, BatchRequestSwitchToMainDialog
+class BatchStatusHandler : public StatusHandler
{
public:
- BatchStatusHandler(bool showProgress, //defines: -start minimized and -quit immediately when finished
+ BatchStatusHandler(bool showProgress,
bool autoCloseDialog,
const std::wstring& jobName, //should not be empty for a batch job!
const Zstring& soundFileSyncComplete,
const std::chrono::system_clock::time_point& startTime,
- const Zstring& logFolderPathPhrase,
- int logfilesCountLimit, //0: logging inactive; < 0: no limit
- size_t lastSyncsLogFileSizeMax,
+ int altLogfileCountMax, //0: logging inactive; < 0: no limit
+ const Zstring& altLogFolderPathPhrase,
bool ignoreErrors,
- BatchErrorDialog batchErrorDialog,
+ BatchErrorHandling batchErrorHandling,
size_t automaticRetryCount,
size_t automaticRetryDelay,
- FfsReturnCode& returnCode,
const Zstring& postSyncCommand,
PostSyncCondition postSyncCondition,
- PostSyncAction postSyncAction);
+ PostSyncAction postSyncAction); //noexcept!!
~BatchStatusHandler();
- void initNewPhase (int itemsTotal, int64_t bytesTotal, Phase phaseID) override;
- void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override;
- void logInfo (const std::wstring& msg) override;
- void forceUiRefreshNoThrow() override;
+ void initNewPhase (int itemsTotal, int64_t bytesTotal, Phase phaseID) override; //
+ void logInfo (const std::wstring& msg) override; //
+ void reportWarning (const std::wstring& msg, bool& warningActive) override; //throw AbortProcess
+ Response reportError (const std::wstring& msg, size_t retryNumber) override; //
+ void reportFatalError(const std::wstring& msg) override; //
+
+ void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override; //noexcept
+ void forceUiRefreshNoThrow() override; //
- void reportWarning (const std::wstring& warningMessage, bool& warningActive) override;
- Response reportError (const std::wstring& errorMessage, size_t retryNumber ) override;
- void reportFatalError(const std::wstring& errorMessage ) override;
+ struct Result
+ {
+ SyncResult finalStatus;
+ bool switchToGuiRequested;
+ Zstring logFilePath;
+ };
+ Result reportFinalStatus(int logfilesMaxAgeDays, const std::set<Zstring, LessFilePath>& logFilePathsToKeep); //noexcept!!
private:
void onProgressDialogTerminate();
bool switchToGuiRequested_ = false;
- const int logfilesCountLimit_;
- const size_t lastSyncsLogFileSizeMax_;
- const BatchErrorDialog batchErrorDialog_;
+
+ const BatchErrorHandling batchErrorHandling_;
zen::ErrorLog errorLog_; //list of non-resolved errors and warnings
- FfsReturnCode& returnCode_;
const size_t automaticRetryCount_;
const size_t automaticRetryDelay_;
@@ -69,9 +70,11 @@ private:
const std::wstring jobName_;
const std::chrono::system_clock::time_point startTime_;
- const Zstring logFolderPathPhrase_;
const Zstring postSyncCommand_;
const PostSyncCondition postSyncCondition_;
+
+ const int altLogfileCountMax_;
+ const Zstring altLogFolderPathPhrase_;
};
}
diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp
index 4932969f..939050d6 100755
--- a/FreeFileSync/Source/ui/cfg_grid.cpp
+++ b/FreeFileSync/Source/ui/cfg_grid.cpp
@@ -7,9 +7,11 @@
#include "cfg_grid.h"
#include <zen/time.h>
#include <zen/basic_math.h>
+#include <zen/shell_execute.h>
#include <wx+/dc.h>
#include <wx+/rtl.h>
#include <wx+/image_resources.h>
+#include <wx+/popup_dlg.h>
#include <wx/settings.h>
#include "../base/icon_buffer.h"
#include "../base/ffs_paths.h"
@@ -24,6 +26,44 @@ Zstring fff::getLastRunConfigPath()
}
+std::vector<ConfigFileItem> ConfigView::get() const
+{
+ std::map<int, ConfigFileItem, std::greater<>> itemsSorted; //sort by last use; put most recent items *first* (looks better in XML than reverted)
+
+ for (const auto& item : cfgList_)
+ itemsSorted.emplace(item.second.lastUseIndex, item.second.cfgItem);
+
+ std::vector<ConfigFileItem> cfgHistory;
+ for (const auto& item : itemsSorted)
+ cfgHistory.emplace_back(item.second);
+
+ return cfgHistory;
+}
+
+
+void ConfigView::set(const std::vector<ConfigFileItem>& cfgItems)
+{
+ std::vector<Zstring> filePaths;
+ for (const ConfigFileItem& item : cfgItems)
+ filePaths.push_back(item.cfgFilePath);
+
+ //list is stored with last used files first in XML, however m_gridCfgHistory expects them last!!!
+ std::reverse(filePaths.begin(), filePaths.end());
+
+ //make sure <Last session> is always part of history list (if existing)
+ filePaths.push_back(lastRunConfigPath_);
+
+ cfgList_ .clear();
+ cfgListView_.clear();
+ addCfgFiles(filePaths);
+
+ for (const ConfigFileItem& item : cfgItems)
+ cfgList_.find(item.cfgFilePath)->second.cfgItem = item; //cfgFilePath must exist after addCfgFiles()!
+
+ sortListView(); //needed if sorted by last sync time
+}
+
+
void ConfigView::addCfgFiles(const std::vector<Zstring>& filePaths)
{
//determine highest "last use" index number of m_listBoxHistory
@@ -37,7 +77,7 @@ void ConfigView::addCfgFiles(const std::vector<Zstring>& filePaths)
if (it == cfgList_.end())
{
Details detail = {};
- detail.filePath = filePath;
+ detail.cfgItem.cfgFilePath = filePath;
detail.lastUseIndex = ++lastUseIndexMax;
std::tie(detail.name, detail.cfgType, detail.isLastRunCfg) = [&]
@@ -79,13 +119,24 @@ void ConfigView::removeItems(const std::vector<Zstring>& filePaths)
}
-void ConfigView::setLastSyncTime(const std::vector<std::pair<Zstring, time_t>>& syncTimes)
+//coordinate with similar code in application.cpp
+void ConfigView::setLastRunStats(const std::vector<Zstring>& filePaths, const LastRunStats& lastRun)
{
- for (const auto& st : syncTimes)
+ for (const Zstring& filePath : filePaths)
{
- auto it = cfgList_.find(st.first);
+ auto it = cfgList_.find(filePath);
+ assert(it != cfgList_.end());
if (it != cfgList_.end())
- it->second.lastSyncTime = st.second;
+ {
+ if (lastRun.result != SyncResult::ABORTED)
+ it->second.cfgItem.lastSyncTime = lastRun.lastRunTime;
+
+ if (!lastRun.logFilePath.empty())
+ {
+ it->second.cfgItem.logFilePath = lastRun.logFilePath;
+ it->second.cfgItem.logResult = lastRun.result;
+ }
+ }
}
sortListView(); //needed if sorted by last sync time
}
@@ -124,10 +175,28 @@ void ConfigView::sortListViewImpl()
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<>(), std::bool_constant<ascending>())(lhs->second.lastSyncTime, rhs->second.lastSyncTime);
+ return makeSortDirection(std::greater<>(), std::bool_constant<ascending>())(lhs->second.cfgItem.lastSyncTime, rhs->second.cfgItem.lastSyncTime);
//[!] ascending LAST_SYNC shows lowest "days past" first <=> highest lastSyncTime first
};
+ const auto lessLastLog = [](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
+
+ const bool hasLogL = !lhs->second.cfgItem.logFilePath.empty();
+ const bool hasLogR = !rhs->second.cfgItem.logFilePath.empty();
+ if (hasLogL != hasLogR)
+ return hasLogL > hasLogR; //move sync jobs that were never run to the back
+
+ //primary sort order
+ if (hasLogL && lhs->second.cfgItem.logResult != rhs->second.cfgItem.logResult)
+ return makeSortDirection(std::greater<>(), std::bool_constant<ascending>())(lhs->second.cfgItem.logResult, rhs->second.cfgItem.logResult);
+
+ //secondary sort order
+ return LessNaturalSort()(lhs->second.name, rhs->second.name);
+ };
+
switch (sortColumn_)
{
case ColumnTypeCfg::NAME:
@@ -136,6 +205,9 @@ void ConfigView::sortListViewImpl()
case ColumnTypeCfg::LAST_SYNC:
std::sort(cfgListView_.begin(), cfgListView_.end(), lessLastSync);
break;
+ case ColumnTypeCfg::LAST_LOG:
+ std::sort(cfgListView_.begin(), cfgListView_.end(), lessLastLog);
+ break;
}
}
@@ -153,16 +225,20 @@ void ConfigView::sortListView()
namespace
{
-class GridDataCfg : public GridData
+class GridDataCfg : private wxEvtHandler, public GridData
{
public:
- GridDataCfg(int fileIconSize) : fileIconSize_(fileIconSize) {}
+ GridDataCfg(Grid& grid) : grid_(grid)
+ {
+ grid.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler(GridDataCfg::onMouseLeft), nullptr, this);
+ grid.Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(GridDataCfg::onMouseLeftDouble), nullptr, this);
+ }
ConfigView& getDataView() { return cfgView_; }
static int getRowDefaultHeight(const Grid& grid)
{
- return grid.getMainWin().GetCharHeight();
+ return std::max(getResourceImage(L"msg_error_sicon").GetHeight(), grid.getMainWin().GetCharHeight()) + fastFromDIP(1); //+ some space
}
int getSyncOverdueDays() const { return syncOverdueDays_; }
@@ -199,24 +275,31 @@ private:
return utfTo<std::wstring>(item->name);
case ColumnTypeCfg::LAST_SYNC:
- {
- if (item->isLastRunCfg)
- return std::wstring();
-
- if (item->lastSyncTime == 0)
- return std::wstring(1, EN_DASH);
+ if (!item->isLastRunCfg)
+ {
+ if (item->cfgItem.lastSyncTime == 0)
+ return std::wstring(1, EN_DASH);
- const int daysPast = getDaysPast(item->lastSyncTime);
- if (daysPast == 0)
- return _("Today");
+ const int daysPast = getDaysPast(item->cfgItem.lastSyncTime);
+ return daysPast == 0 ? _("Today") : _P("1 day", "%x days", daysPast);
+ //return formatTime<std::wstring>(FORMAT_DATE_TIME, getLocalTime(item->lastSyncTime));
+ }
+ break;
- return _P("1 day", "%x days", daysPast);
- }
- //return formatTime<std::wstring>(FORMAT_DATE_TIME, getLocalTime(item->lastSyncTime));
+ case ColumnTypeCfg::LAST_LOG:
+ if (!item->isLastRunCfg &&
+ !item->cfgItem.logFilePath.empty())
+ return getFinalStatusLabel(item->cfgItem.logResult);
+ break;
}
return std::wstring();
}
+ enum class HoverAreaLog
+ {
+ LINK,
+ };
+
void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override
{
wxRect rectTmp = rect;
@@ -226,46 +309,79 @@ private:
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 (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<ColumnTypeCfg>(colType))
{
case ColumnTypeCfg::NAME:
+ {
rectTmp.x += getColumnGapLeft();
rectTmp.width -= getColumnGapLeft();
- switch (item->cfgType)
+ const wxBitmap cfgIcon = [&]
{
- 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;
- }
+ switch (item->cfgType)
+ {
+ case ConfigView::Details::CFG_TYPE_NONE:
+ return wxNullBitmap;
+ case ConfigView::Details::CFG_TYPE_GUI:
+ return getResourceImage(L"file_sync_sicon");
+ case ConfigView::Details::CFG_TYPE_BATCH:
+ return getResourceImage(L"file_batch_sicon");
+ }
+ assert(false);
+ return wxNullBitmap;
+ }();
+ if (cfgIcon.IsOk())
+ drawBitmapRtlNoMirror(dc, enabled ? cfgIcon : cfgIcon.ConvertToDisabled(), rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+
rectTmp.x += fileIconSize_ + getColumnGapLeft();
rectTmp.width -= fileIconSize_ + getColumnGapLeft();
drawCellText(dc, rectTmp, getValue(row, colType));
- break;
+ }
+ break;
case ColumnTypeCfg::LAST_SYNC:
{
wxDCTextColourChanger dummy2(dc);
if (syncOverdueDays_ > 0)
- if (getDaysPast(item->lastSyncTime) >= syncOverdueDays_)
+ if (getDaysPast(item->cfgItem.lastSyncTime) >= syncOverdueDays_)
dummy2.Set(*wxRED);
drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER);
}
break;
+
+ case ColumnTypeCfg::LAST_LOG:
+ if (!item->isLastRunCfg &&
+ !item->cfgItem.logFilePath.empty())
+ {
+ const wxBitmap statusIcon = [&]
+ {
+ switch (item->cfgItem.logResult)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ return getResourceImage(L"msg_finished_sicon");
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return getResourceImage(L"msg_warning_sicon");
+ case SyncResult::FINISHED_WITH_ERROR:
+ case SyncResult::ABORTED:
+ return getResourceImage(L"msg_error_sicon");
+ }
+ assert(false);
+ return wxNullBitmap;
+ }();
+ drawBitmapRtlNoMirror(dc, enabled ? statusIcon : statusIcon.ConvertToDisabled(), rectTmp, wxALIGN_CENTER);
+ }
+ if (static_cast<HoverAreaLog>(rowHover) == HoverAreaLog::LINK)
+ drawBitmapRtlNoMirror(dc, getResourceImage(L"link_16"), rectTmp, wxALIGN_CENTER);
+ break;
}
}
@@ -280,7 +396,11 @@ private:
case ColumnTypeCfg::LAST_SYNC:
return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth() + getColumnGapLeft();
+
+ case ColumnTypeCfg::LAST_LOG:
+ return fileIconSize_;
}
+ assert(false);
return 0;
}
@@ -292,20 +412,59 @@ private:
clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
}
+ HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override
+ {
+ if (const ConfigView::Details* item = cfgView_.getItem(row))
+ switch (static_cast<ColumnTypeCfg>(colType))
+ {
+ case ColumnTypeCfg::NAME:
+ case ColumnTypeCfg::LAST_SYNC:
+ break;
+ case ColumnTypeCfg::LAST_LOG:
+
+ if (!item->isLastRunCfg &&
+ !item->cfgItem.logFilePath.empty())
+ return static_cast<HoverArea>(HoverAreaLog::LINK);
+ break;
+ }
+ return HoverArea::NONE;
+ }
+
void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override
{
- wxRect rectInside = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectInside, highlighted);
+ const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
+ wxRect rectRemain = rectInner;
- rectInside.x += getColumnGapLeft();
- rectInside.width -= getColumnGapLeft();
- drawColumnLabelText(dc, rectInside, getColumnLabel(colType));
+ wxBitmap sortMarker;
- auto sortInfo = cfgView_.getSortDirection();
+ const auto sortInfo = cfgView_.getSortDirection();
if (colType == static_cast<ColumnType>(sortInfo.first))
+ sortMarker = getResourceImage(sortInfo.second ? L"sort_ascending" : L"sort_descending");
+
+ switch (static_cast<ColumnTypeCfg>(colType))
{
- const wxBitmap& marker = getResourceImage(sortInfo.second ? L"sort_ascending" : L"sort_descending");
- drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL);
+ case ColumnTypeCfg::NAME:
+ case ColumnTypeCfg::LAST_SYNC:
+ rectRemain.x += getColumnGapLeft();
+ rectRemain.width -= getColumnGapLeft();
+ drawColumnLabelText(dc, rectRemain, getColumnLabel(colType));
+
+ if (sortMarker.IsOk())
+ drawBitmapRtlNoMirror(dc, sortMarker, rectInner, wxALIGN_CENTER_HORIZONTAL);
+ break;
+
+ case ColumnTypeCfg::LAST_LOG:
+ drawBitmapRtlNoMirror(dc, getResourceImage(L"log_file_sicon"), rectInner, wxALIGN_CENTER);
+
+ if (sortMarker.IsOk())
+ {
+ const int gapLeft = (rectInner.width + getResourceImage(L"log_file_sicon").GetWidth()) / 2;
+ rectRemain.x += gapLeft;
+ rectRemain.width -= gapLeft;
+
+ drawBitmapRtlNoMirror(dc, sortMarker, rectRemain, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+ }
+ break;
}
}
@@ -317,16 +476,75 @@ private:
return _("Name");
case ColumnTypeCfg::LAST_SYNC:
return _("Last sync");
+ case ColumnTypeCfg::LAST_LOG:
+ return _("Log");
+ }
+ return std::wstring();
+ }
+
+ std::wstring getToolTip(ColumnType colType) const override
+ {
+ switch (static_cast<ColumnTypeCfg>(colType))
+ {
+ case ColumnTypeCfg::NAME:
+ case ColumnTypeCfg::LAST_SYNC:
+ break;
+ case ColumnTypeCfg::LAST_LOG:
+ return getColumnLabel(colType);
}
return std::wstring();
}
+ std::wstring getToolTip(size_t row, ColumnType colType) const override
+ {
+ if (const ConfigView::Details* item = cfgView_.getItem(row))
+ switch (static_cast<ColumnTypeCfg>(colType))
+ {
+ case ColumnTypeCfg::NAME:
+ case ColumnTypeCfg::LAST_SYNC:
+ break;
+ case ColumnTypeCfg::LAST_LOG:
+
+ if (!item->isLastRunCfg &&
+ !item->cfgItem.logFilePath.empty())
+ return getFinalStatusLabel(item->cfgItem.logResult) + SPACED_DASH + utfTo<std::wstring>(item->cfgItem.logFilePath);
+ break;
+ }
+ return std::wstring();
+ }
+
+ void onMouseLeft(GridClickEvent& event)
+ {
+ if (const ConfigView::Details* item = cfgView_.getItem(event.row_))
+ switch (static_cast<HoverAreaLog>(event.hoverArea_))
+ {
+ case HoverAreaLog::LINK:
+ assert(!item->cfgItem.logFilePath.empty()); //see getRowMouseHover()
+ try
+ {
+ openWithDefaultApplication(item->cfgItem.logFilePath); //throw FileError
+ }
+ catch (const FileError& e) { showNotificationDialog(&grid_, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); }
+ return;
+ }
+ event.Skip();
+ }
+
+ void onMouseLeftDouble(GridClickEvent& event)
+ {
+ switch (static_cast<HoverAreaLog>(event.hoverArea_))
+ {
+ case HoverAreaLog::LINK:
+ return; //swallow event here before MainDialog considers it as a request to start comparison
+ }
+ event.Skip();
+ }
+
private:
+ Grid& grid_;
ConfigView cfgView_;
int syncOverdueDays_ = 0;
- const int fileIconSize_;
- const wxBitmap syncIconSmall_ = getResourceImage(L"file_sync" ).ConvertToImage().Scale(fileIconSize_, fileIconSize_, wxIMAGE_QUALITY_BILINEAR); //looks sharper than wxIMAGE_QUALITY_HIGH!
- const wxBitmap batchIconSmall_ = getResourceImage(L"file_batch").ConvertToImage().Scale(fileIconSize_, fileIconSize_, wxIMAGE_QUALITY_BILINEAR);
+ const int fileIconSize_ = getResourceImage(L"msg_error_sicon").GetHeight();
};
}
@@ -335,16 +553,9 @@ void cfggrid::init(Grid& grid)
{
const int rowHeight = GridDataCfg::getRowDefaultHeight(grid);
- int fileIconSize = rowHeight - fastFromDIP(2); /*border*/
- if (fileIconSize < 16) //no border for very small icons
- fileIconSize = rowHeight;
-
- auto prov = std::make_shared<GridDataCfg>(fileIconSize);
-
- grid.setDataProvider(prov);
+ grid.setDataProvider(std::make_shared<GridDataCfg>(grid));
grid.showRowLabel(false);
grid.setRowHeight(rowHeight);
-
grid.setColumnLabelHeight(rowHeight + fastFromDIP(2));
}
@@ -359,34 +570,25 @@ ConfigView& cfggrid::getDataView(Grid& grid)
void cfggrid::addAndSelect(Grid& grid, const std::vector<Zstring>& filePaths, bool scrollToSelection)
{
- auto* prov = dynamic_cast<GridDataCfg*>(grid.getDataProvider());
- if (!prov)
- throw std::runtime_error(std::string(__FILE__) + "[" + numberTo<std::string>(__LINE__) + "] cfggrid was not initialized.");
-
- prov->getDataView().addCfgFiles(filePaths);
+ getDataView(grid).addCfgFiles(filePaths);
grid.Refresh(); //[!] let Grid know about changed row count *before* fiddling with selection!!!
- grid.clearSelection(GridEventPolicy::DENY_GRID_EVENT);
+ grid.clearSelection(GridEventPolicy::DENY);
const std::set<Zstring, LessFilePath> pathsSorted(filePaths.begin(), filePaths.end());
- ptrdiff_t selectionTopRow = -1;
+ Opt<size_t> selectionTopRow;
for (size_t i = 0; i < grid.getRowCount(); ++i)
- if (const ConfigView::Details* cfg = prov->getDataView().getItem(i))
+ if (pathsSorted.find(getDataView(grid).getItem(i)->cfgItem.cfgFilePath) != pathsSorted.end())
{
- if (pathsSorted.find(cfg->filePath) != pathsSorted.end())
- {
- if (selectionTopRow < 0)
- selectionTopRow = i;
+ if (!selectionTopRow)
+ selectionTopRow = i;
- grid.selectRow(i, GridEventPolicy::DENY_GRID_EVENT);
- }
+ grid.selectRow(i, GridEventPolicy::DENY);
}
- else
- assert(false);
- if (scrollToSelection && selectionTopRow >= 0)
- grid.makeRowVisible(selectionTopRow);
+ if (scrollToSelection && selectionTopRow)
+ grid.makeRowVisible(*selectionTopRow);
}
diff --git a/FreeFileSync/Source/ui/cfg_grid.h b/FreeFileSync/Source/ui/cfg_grid.h
index d0d02442..fc99a40d 100755
--- a/FreeFileSync/Source/ui/cfg_grid.h
+++ b/FreeFileSync/Source/ui/cfg_grid.h
@@ -8,16 +8,37 @@
#define CONFIG_HISTORY_3248789479826359832
#include <wx+/grid.h>
-#include <zen/zstring.h>
#include <wx+/dc.h>
+#include <zen/zstring.h>
+#include "../base/return_codes.h"
namespace fff
{
+struct ConfigFileItem
+{
+ ConfigFileItem() {}
+ ConfigFileItem(const Zstring& filePath,
+ time_t syncTime,
+ const Zstring& logPath,
+ SyncResult result) :
+ cfgFilePath(filePath),
+ lastSyncTime(syncTime),
+ logFilePath(logPath),
+ logResult(result) {}
+
+ Zstring cfgFilePath;
+ time_t lastSyncTime = 0; //last COMPLETED sync (aborted syncs don't count)
+ Zstring logFilePath; //ANY last sync attempt (including aborted syncs)
+ SyncResult logResult = SyncResult::ABORTED; //
+};
+
+
enum class ColumnTypeCfg
{
NAME,
LAST_SYNC,
+ LAST_LOG,
};
@@ -35,8 +56,9 @@ std::vector<ColAttributesCfg> getCfgGridDefaultColAttribs()
using namespace zen;
return
{
- { ColumnTypeCfg::NAME, fastFromDIP(-75), 1, true },
- { ColumnTypeCfg::LAST_SYNC, fastFromDIP( 75), 0, true },
+ { ColumnTypeCfg::NAME, fastFromDIP(-117), 1, true },
+ { ColumnTypeCfg::LAST_SYNC, fastFromDIP( 75), 0, true },
+ { ColumnTypeCfg::LAST_LOG, fastFromDIP( 42), 0, true }, //leave some room for the sort direction indicator
};
}
@@ -51,6 +73,8 @@ bool getDefaultSortDirection(ColumnTypeCfg colType)
return true;
case ColumnTypeCfg::LAST_SYNC: //actual sort order is "time since last sync"
return false;
+ case ColumnTypeCfg::LAST_LOG:
+ return true;
}
assert(false);
return true;
@@ -64,16 +88,25 @@ class ConfigView
public:
ConfigView() {}
+ std::vector<ConfigFileItem> get() const;
+ void set(const std::vector<ConfigFileItem>& cfgItems);
+
void addCfgFiles(const std::vector<Zstring>& filePaths);
void removeItems(const std::vector<Zstring>& filePaths);
- void setLastSyncTime(const std::vector<std::pair<Zstring /*filePath*/, time_t /*lastSyncTime*/>>& syncTimes);
+ struct LastRunStats
+ {
+ time_t lastRunTime = 0;
+ SyncResult result = SyncResult::ABORTED;
+ Zstring logFilePath; //optional
+ };
+ void setLastRunStats(const std::vector<Zstring>& filePaths, const LastRunStats& lastRun);
struct Details
{
- Zstring filePath;
+ ConfigFileItem cfgItem;
+
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
diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp
index 29fc2080..cc1c5ece 100755
--- a/FreeFileSync/Source/ui/file_grid.cpp
+++ b/FreeFileSync/Source/ui/file_grid.cpp
@@ -52,7 +52,7 @@ class hierarchy:
| |
GridDataRim |
/|\ |
- __________|__________ |
+ __________|_________ |
| | |
GridDataLeft GridDataRight GridDataCenter
*/
@@ -71,7 +71,6 @@ std::pair<ptrdiff_t, ptrdiff_t> getVisibleRows(const Grid& grid) //returns range
if (rowFrom >= 0 && rowTo >= 0)
return { rowFrom, std::min(rowTo + 1, rowCount) };
}
- assert(false);
return {};
}
@@ -671,12 +670,12 @@ private:
void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override
{
- wxRect rectInside = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectInside, highlighted);
+ const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
+ wxRect rectRemain = rectInner;
- rectInside.x += getColumnGapLeft();
- rectInside.width -= getColumnGapLeft();
- drawColumnLabelText(dc, rectInside, getColumnLabel(colType));
+ rectRemain.x += getColumnGapLeft();
+ rectRemain.width -= getColumnGapLeft();
+ drawColumnLabelText(dc, rectRemain, getColumnLabel(colType));
//draw sort marker
if (getGridDataView())
@@ -687,7 +686,7 @@ private:
if (colType == static_cast<ColumnType>(sortInfo->type) && (side == LEFT_SIDE) == sortInfo->onLeft)
{
const wxBitmap& marker = getResourceImage(sortInfo->ascending ? L"sort_ascending" : L"sort_descending");
- drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL);
+ drawBitmapRtlNoMirror(dc, marker, rectInner, wxALIGN_CENTER_HORIZONTAL);
}
}
}
@@ -856,13 +855,13 @@ public:
void onSelectBegin()
{
selectionInProgress_ = true;
- refGrid().clearSelection(DENY_GRID_EVENT); //don't emit event, prevent recursion!
+ refGrid().clearSelection(GridEventPolicy::DENY); //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!
+ refGrid().clearSelection(GridEventPolicy::DENY); //don't emit event, prevent recursion!
//issue custom event
if (selectionInProgress_) //don't process selections initiated by right-click
@@ -1104,26 +1103,24 @@ private:
switch (static_cast<ColumnTypeCenter>(colType))
{
case ColumnTypeCenter::CHECKBOX:
- drawColumnLabelBackground(dc, rect, false);
+ drawColumnLabelBackground(dc, rect, false /*highlighted*/);
break;
case ColumnTypeCenter::CMP_CATEGORY:
{
- wxRect rectInside = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectInside, highlighted);
+ const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
const wxBitmap& cmpIcon = getResourceImage(L"compare_sicon");
- drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? greyScale(cmpIcon) : cmpIcon, rectInside, wxALIGN_CENTER);
+ drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? greyScale(cmpIcon) : cmpIcon, rectInner, wxALIGN_CENTER);
}
break;
case ColumnTypeCenter::SYNC_ACTION:
{
- wxRect rectInside = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectInside, highlighted);
+ const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
const wxBitmap& syncIcon = getResourceImage(L"file_sync_sicon");
- drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? syncIcon : greyScale(syncIcon), rectInside, wxALIGN_CENTER);
+ drawBitmapRtlNoMirror(dc, highlightSyncAction_ ? syncIcon : greyScale(syncIcon), rectInner, wxALIGN_CENTER);
}
break;
}
@@ -1374,8 +1371,8 @@ private:
{
if (event.positive_)
{
- if (event.mouseSelect_)
- provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseSelect_->click.hoverArea_, event.mouseSelect_->click.row_);
+ if (event.mouseClick_)
+ provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, event.mouseClick_->hoverArea_, event.mouseClick_->row_);
else
provCenter_.onSelectEnd(event.rowFirst_, event.rowLast_, HoverArea::NONE, -1);
}
@@ -1400,7 +1397,7 @@ private:
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!
+ other.clearSelection(GridEventPolicy::DENY); //don't emit event, prevent recursion!
}
void onKeyDownL(wxKeyEvent& event) { onKeyDown(event, gridL_); }
@@ -1430,7 +1427,7 @@ private:
{
case WXK_LEFT:
case WXK_NUMPAD_LEFT:
- gridL_.setGridCursor(row);
+ gridL_.setGridCursor(row, GridEventPolicy::ALLOW);
gridL_.SetFocus();
//since key event is likely originating from right grid, we need to set scrollMaster manually!
scrollMaster_ = &gridL_; //onKeyDown is called *after* onGridAccessL()!
@@ -1438,7 +1435,7 @@ private:
case WXK_RIGHT:
case WXK_NUMPAD_RIGHT:
- gridR_.setGridCursor(row);
+ gridR_.setGridCursor(row, GridEventPolicy::ALLOW);
gridR_.SetFocus();
scrollMaster_ = &gridR_;
return; //swallow event
diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp
index 3f96e152..09986aac 100755
--- a/FreeFileSync/Source/ui/file_view.cpp
+++ b/FreeFileSync/Source/ui/file_view.cpp
@@ -482,8 +482,8 @@ struct FileView::LessSyncDirection
void FileView::sortView(ColumnTypeRim type, ItemPathFormat pathFmt, bool onLeft, bool ascending)
{
- viewRef_.clear();
- rowPositions_.clear();
+ viewRef_ .clear();
+ rowPositions_ .clear();
rowPositionsFirstChild_.clear();
currentSort_ = SortInfo({ type, onLeft, ascending });
diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp
index 6dd4222e..820b5f6f 100755
--- a/FreeFileSync/Source/ui/gui_generated.cpp
+++ b/FreeFileSync/Source/ui/gui_generated.cpp
@@ -13,7 +13,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
{
this->SetSizeHints( wxSize( 640, 400 ), wxDefaultSize );
- m_menubar1 = new wxMenuBar( 0 );
+ m_menubar = new wxMenuBar( 0 );
m_menuFile = new wxMenu();
m_menuItemNew = new wxMenuItem( m_menuFile, wxID_NEW, wxString( _("&New") ) + wxT('\t') + wxT("Ctrl+N"), wxEmptyString, wxITEM_NORMAL );
m_menuFile->Append( m_menuItemNew );
@@ -38,9 +38,14 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_menuItem4 = new wxMenuItem( m_menuFile, wxID_EXIT, wxString( _("E&xit") ), wxEmptyString, wxITEM_NORMAL );
m_menuFile->Append( m_menuItem4 );
- m_menubar1->Append( m_menuFile, _("&File") );
+ m_menubar->Append( m_menuFile, _("&File") );
m_menu4 = new wxMenu();
+ m_menuItemShowLog = new wxMenuItem( m_menu4, wxID_ANY, wxString( _("Show &log") ) + wxT('\t') + wxT("F4"), wxEmptyString, wxITEM_NORMAL );
+ m_menu4->Append( m_menuItemShowLog );
+
+ m_menu4->AppendSeparator();
+
m_menuItemCompare = new wxMenuItem( m_menu4, wxID_ANY, wxString( _("Start &comparison") ) + wxT('\t') + wxT("F5"), wxEmptyString, wxITEM_NORMAL );
m_menu4->Append( m_menuItemCompare );
@@ -60,7 +65,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_menuItemSynchronize = new wxMenuItem( m_menu4, wxID_ANY, wxString( _("Start &synchronization") ) + wxT('\t') + wxT("F9"), wxEmptyString, wxITEM_NORMAL );
m_menu4->Append( m_menuItemSynchronize );
- m_menubar1->Append( m_menu4, _("&Actions") );
+ m_menubar->Append( m_menu4, _("&Actions") );
m_menuTools = new wxMenu();
m_menuItemOptions = new wxMenuItem( m_menuTools, wxID_PREFERENCES, wxString( _("&Preferences") ) + wxT('\t') + wxT("Ctrl+,"), wxEmptyString, wxITEM_NORMAL );
@@ -99,7 +104,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_menuItemShowOverview = new wxMenuItem( m_menuTools, wxID_ANY, wxString( _("dummy") ), wxEmptyString, wxITEM_NORMAL );
m_menuTools->Append( m_menuItemShowOverview );
- m_menubar1->Append( m_menuTools, _("&Tools") );
+ m_menubar->Append( m_menuTools, _("&Tools") );
m_menuHelp = new wxMenu();
m_menuItemHelp = new wxMenuItem( m_menuHelp, wxID_HELP, wxString( _("&View help") ) + wxT('\t') + wxT("F1"), wxEmptyString, wxITEM_NORMAL );
@@ -119,9 +124,9 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_menuItemAbout = new wxMenuItem( m_menuHelp, wxID_ABOUT, wxString( _("&About") ) + wxT('\t') + wxT("Shift+F1"), wxEmptyString, wxITEM_NORMAL );
m_menuHelp->Append( m_menuItemAbout );
- m_menubar1->Append( m_menuHelp, _("&Help") );
+ m_menubar->Append( m_menuHelp, _("&Help") );
- this->SetMenuBar( m_menubar1 );
+ this->SetMenuBar( m_menubar );
bSizerPanelHolder = new wxBoxSizer( wxVERTICAL );
@@ -131,55 +136,58 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizerTopButtons = new wxBoxSizer( wxHORIZONTAL );
+ wxBoxSizer* bSizer261;
+ bSizer261 = new wxBoxSizer( wxHORIZONTAL );
- bSizerTopButtons->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 );
+
+ bSizer261->Add( 0, 0, 1, 0, 5 );
m_buttonCancel = new zen::BitmapTextButton( m_panelTopButtons, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
m_buttonCancel->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
m_buttonCancel->Enable( false );
m_buttonCancel->Hide();
- bSizerTopButtons->Add( m_buttonCancel, 0, wxEXPAND, 5 );
+ bSizer261->Add( m_buttonCancel, 0, wxEXPAND, 5 );
m_buttonCompare = new zen::BitmapTextButton( m_panelTopButtons, wxID_ANY, _("Compare"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
m_buttonCompare->SetDefault();
m_buttonCompare->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
m_buttonCompare->SetToolTip( _("dummy") );
- bSizerTopButtons->Add( m_buttonCompare, 0, wxEXPAND, 5 );
+ bSizer261->Add( m_buttonCompare, 0, wxEXPAND, 5 );
- bSizerTopButtons->Add( 4, 0, 0, 0, 5 );
-
- wxBoxSizer* bSizer198;
- bSizer198 = new wxBoxSizer( wxHORIZONTAL );
+ bSizer261->Add( 4, 0, 0, 0, 5 );
m_bpButtonCmpConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW );
m_bpButtonCmpConfig->SetToolTip( _("dummy") );
- bSizer198->Add( m_bpButtonCmpConfig, 1, wxEXPAND, 5 );
+ bSizer261->Add( m_bpButtonCmpConfig, 0, wxEXPAND, 5 );
m_bpButtonCmpContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW );
m_bpButtonCmpContext->SetToolTip( _("dummy") );
- bSizer198->Add( m_bpButtonCmpContext, 0, wxEXPAND, 5 );
+ bSizer261->Add( m_bpButtonCmpContext, 0, wxEXPAND, 5 );
- bSizerTopButtons->Add( bSizer198, 0, wxEXPAND, 5 );
+ bSizer261->Add( 0, 0, 1, 0, 5 );
- bSizerTopButtons->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 );
+ bSizerTopButtons->Add( bSizer261, 1, wxEXPAND, 5 );
- bSizerTopButtons->Add( 5, 5, 0, 0, 5 );
+ bSizerTopButtons->Add( 5, 2, 0, 0, 5 );
wxBoxSizer* bSizer199;
bSizer199 = new wxBoxSizer( wxHORIZONTAL );
+
+ bSizer199->Add( 0, 0, 1, 0, 5 );
+
m_bpButtonFilter = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW|wxFULL_REPAINT_ON_RESIZE );
m_bpButtonFilter->SetToolTip( _("dummy") );
- bSizer199->Add( m_bpButtonFilter, 1, wxEXPAND, 5 );
+ bSizer199->Add( m_bpButtonFilter, 0, wxEXPAND, 5 );
m_bpButtonFilterContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW );
m_bpButtonFilterContext->SetToolTip( _("dummy") );
@@ -187,41 +195,44 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer199->Add( m_bpButtonFilterContext, 0, wxEXPAND, 5 );
+ bSizer199->Add( 0, 0, 1, 0, 5 );
+
+
bSizerTopButtons->Add( bSizer199, 0, wxEXPAND, 5 );
- bSizerTopButtons->Add( 5, 5, 0, 0, 5 );
+ bSizerTopButtons->Add( 5, 2, 0, 0, 5 );
+ wxBoxSizer* bSizer262;
+ bSizer262 = new wxBoxSizer( wxHORIZONTAL );
- bSizerTopButtons->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 );
- wxBoxSizer* bSizer200;
- bSizer200 = new wxBoxSizer( wxHORIZONTAL );
+ bSizer262->Add( 0, 0, 1, 0, 5 );
m_bpButtonSyncConfig = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW );
m_bpButtonSyncConfig->SetToolTip( _("dummy") );
- bSizer200->Add( m_bpButtonSyncConfig, 1, wxEXPAND, 5 );
+ bSizer262->Add( m_bpButtonSyncConfig, 0, wxEXPAND, 5 );
m_bpButtonSyncContext = new wxBitmapButton( m_panelTopButtons, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW );
m_bpButtonSyncContext->SetToolTip( _("dummy") );
- bSizer200->Add( m_bpButtonSyncContext, 0, wxEXPAND, 5 );
-
+ bSizer262->Add( m_bpButtonSyncContext, 0, wxEXPAND, 5 );
- bSizerTopButtons->Add( bSizer200, 0, wxEXPAND, 5 );
-
- bSizerTopButtons->Add( 4, 0, 0, 0, 5 );
+ bSizer262->Add( 4, 0, 0, 0, 5 );
m_buttonSync = new zen::BitmapTextButton( m_panelTopButtons, wxID_ANY, _("Synchronize"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
m_buttonSync->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
m_buttonSync->SetToolTip( _("dummy") );
- bSizerTopButtons->Add( m_buttonSync, 0, wxEXPAND, 5 );
+ bSizer262->Add( m_buttonSync, 0, wxEXPAND, 5 );
+
+ bSizer262->Add( 0, 0, 1, 0, 5 );
- bSizerTopButtons->Add( 0, 0, 1, wxALIGN_CENTER_VERTICAL, 5 );
+
+ bSizerTopButtons->Add( bSizer262, 1, wxEXPAND, 5 );
bSizer1791->Add( bSizerTopButtons, 1, wxEXPAND, 5 );
@@ -390,7 +401,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_gridOverview = new zen::Grid( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL );
m_gridOverview->SetScrollRate( 5, 5 );
- bSizerPanelHolder->Add( m_gridOverview, 1, wxEXPAND, 5 );
+ bSizerPanelHolder->Add( m_gridOverview, 0, 0, 5 );
m_panelCenter = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
wxBoxSizer* bSizer1711;
@@ -601,6 +612,142 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
bSizer1713->Fit( m_panelSearch );
bSizerPanelHolder->Add( m_panelSearch, 0, 0, 5 );
+ m_panelLog = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
+ m_panelLog->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
+
+ bSizerLog = new wxBoxSizer( wxVERTICAL );
+
+ bSizer42 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_bitmapLogStatus = new wxStaticBitmap( m_panelLog, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 );
+ bSizer42->Add( m_bitmapLogStatus, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 );
+
+ m_staticTextLogStatus = new wxStaticText( m_panelLog, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticTextLogStatus->Wrap( -1 );
+ m_staticTextLogStatus->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+ bSizer42->Add( m_staticTextLogStatus, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 5 );
+
+ m_panelItemsProcessed = new wxPanel( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
+ m_panelItemsProcessed->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
+
+ wxBoxSizer* bSizer165;
+ bSizer165 = new wxBoxSizer( wxVERTICAL );
+
+
+ bSizer165->Add( 0, 5, 0, 0, 5 );
+
+ wxStaticText* m_staticText962;
+ m_staticText962 = new wxStaticText( m_panelItemsProcessed, wxID_ANY, _("Items processed:"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticText962->Wrap( -1 );
+ bSizer165->Add( m_staticText962, 0, wxRIGHT|wxLEFT, 5 );
+
+ wxBoxSizer* bSizer169;
+ bSizer169 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_staticTextItemsProcessed = new wxStaticText( m_panelItemsProcessed, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
+ m_staticTextItemsProcessed->Wrap( -1 );
+ m_staticTextItemsProcessed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+ bSizer169->Add( m_staticTextItemsProcessed, 0, wxALIGN_BOTTOM, 5 );
+
+ m_staticTextBytesProcessed = new wxStaticText( m_panelItemsProcessed, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticTextBytesProcessed->Wrap( -1 );
+ bSizer169->Add( m_staticTextBytesProcessed, 0, wxLEFT|wxALIGN_BOTTOM, 5 );
+
+
+ bSizer165->Add( bSizer169, 0, wxRIGHT|wxLEFT, 5 );
+
+
+ bSizer165->Add( 0, 5, 0, 0, 5 );
+
+
+ m_panelItemsProcessed->SetSizer( bSizer165 );
+ m_panelItemsProcessed->Layout();
+ bSizer165->Fit( m_panelItemsProcessed );
+ bSizer42->Add( m_panelItemsProcessed, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 10 );
+
+ m_panelItemsRemaining = new wxPanel( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
+ m_panelItemsRemaining->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
+
+ wxBoxSizer* bSizer166;
+ bSizer166 = new wxBoxSizer( wxVERTICAL );
+
+
+ bSizer166->Add( 0, 5, 0, 0, 5 );
+
+ wxStaticText* m_staticText971;
+ m_staticText971 = new wxStaticText( m_panelItemsRemaining, wxID_ANY, _("Items remaining:"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticText971->Wrap( -1 );
+ bSizer166->Add( m_staticText971, 0, wxRIGHT|wxLEFT, 5 );
+
+ wxBoxSizer* bSizer170;
+ bSizer170 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_staticTextItemsRemaining = new wxStaticText( m_panelItemsRemaining, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
+ m_staticTextItemsRemaining->Wrap( -1 );
+ m_staticTextItemsRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+ bSizer170->Add( m_staticTextItemsRemaining, 0, wxALIGN_BOTTOM, 5 );
+
+ m_staticTextBytesRemaining = new wxStaticText( m_panelItemsRemaining, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticTextBytesRemaining->Wrap( -1 );
+ bSizer170->Add( m_staticTextBytesRemaining, 0, wxLEFT|wxALIGN_BOTTOM, 5 );
+
+
+ bSizer166->Add( bSizer170, 0, wxRIGHT|wxLEFT, 5 );
+
+
+ bSizer166->Add( 0, 5, 0, 0, 5 );
+
+
+ m_panelItemsRemaining->SetSizer( bSizer166 );
+ m_panelItemsRemaining->Layout();
+ bSizer166->Fit( m_panelItemsRemaining );
+ bSizer42->Add( m_panelItemsRemaining, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 10 );
+
+ wxPanel* m_panelTimeElapsed;
+ m_panelTimeElapsed = new wxPanel( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
+ m_panelTimeElapsed->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) );
+
+ wxBoxSizer* bSizer168;
+ bSizer168 = new wxBoxSizer( wxVERTICAL );
+
+
+ bSizer168->Add( 0, 5, 0, 0, 5 );
+
+ wxStaticText* m_staticText9611;
+ m_staticText9611 = new wxStaticText( m_panelTimeElapsed, wxID_ANY, _("Total time:"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticText9611->Wrap( -1 );
+ bSizer168->Add( m_staticText9611, 0, wxRIGHT|wxLEFT, 5 );
+
+ m_staticTextTotalTime = new wxStaticText( m_panelTimeElapsed, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 );
+ m_staticTextTotalTime->Wrap( -1 );
+ m_staticTextTotalTime->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) );
+
+ bSizer168->Add( m_staticTextTotalTime, 0, wxRIGHT|wxLEFT, 5 );
+
+
+ bSizer168->Add( 0, 5, 0, 0, 5 );
+
+
+ m_panelTimeElapsed->SetSizer( bSizer168 );
+ m_panelTimeElapsed->Layout();
+ bSizer168->Fit( m_panelTimeElapsed );
+ bSizer42->Add( m_panelTimeElapsed, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 10 );
+
+
+ bSizerLog->Add( bSizer42, 0, wxALL, 5 );
+
+ m_staticline70 = new wxStaticLine( m_panelLog, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
+ bSizerLog->Add( m_staticline70, 0, wxEXPAND, 5 );
+
+
+ m_panelLog->SetSizer( bSizerLog );
+ m_panelLog->Layout();
+ bSizerLog->Fit( m_panelLog );
+ bSizerPanelHolder->Add( m_panelLog, 0, 0, 5 );
+
m_panelConfig = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
bSizerConfig = new wxBoxSizer( wxHORIZONTAL );
@@ -693,6 +840,12 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
m_panelViewFilter = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
bSizerViewFilter = new wxBoxSizer( wxHORIZONTAL );
+ m_bpButtonShowLog = new wxBitmapButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW );
+ bSizerViewFilter->Add( m_bpButtonShowLog, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
+
+
+ bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 );
+
m_staticTextViewType = new wxStaticText( m_panelViewFilter, wxID_ANY, _("View type:"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextViewType->Wrap( -1 );
bSizerViewFilter->Add( m_staticTextViewType, 0, wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 );
@@ -972,6 +1125,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const
this->Connect( m_menuItemSaveAs->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnConfigSaveAs ) );
this->Connect( m_menuItemSaveAsBatch->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnSaveAsBatchJob ) );
this->Connect( m_menuItem4->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnMenuQuit ) );
+ this->Connect( m_menuItemShowLog->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnShowLog ) );
this->Connect( m_menuItemCompare->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnCompare ) );
this->Connect( m_menuItemCompSettings->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnCmpSettings ) );
this->Connect( m_menuItemFilter->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( MainDialogGenerated::OnConfigureFilter ) );
@@ -1012,6 +1166,7 @@ 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_bpButtonShowLog->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnShowLog ), 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 );
@@ -4010,14 +4165,14 @@ OptionsDlgGenerated::OptionsDlgGenerated( wxWindow* parent, wxWindowID id, const
wxBoxSizer* bSizer1881;
bSizer1881 = new wxBoxSizer( wxVERTICAL );
- m_buttonResetDialogs = new zen::BitmapTextButton( m_panel39, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
- bSizer1881->Add( m_buttonResetDialogs, 0, wxALL, 5 );
-
m_staticTextResetDialogs = new wxStaticText( m_panel39, wxID_ANY, _("Show all permanently hidden dialogs and warning messages again"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextResetDialogs->Wrap( -1 );
m_staticTextResetDialogs->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
- bSizer1881->Add( m_staticTextResetDialogs, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 );
+ bSizer1881->Add( m_staticTextResetDialogs, 0, wxTOP|wxRIGHT|wxLEFT, 5 );
+
+ m_buttonResetDialogs = new zen::BitmapTextButton( m_panel39, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 );
+ bSizer1881->Add( m_buttonResetDialogs, 0, wxALL, 5 );
bSizer186->Add( bSizer1881, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 );
@@ -4028,6 +4183,39 @@ OptionsDlgGenerated::OptionsDlgGenerated( wxWindow* parent, wxWindowID id, const
m_staticline191 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
bSizer166->Add( m_staticline191, 0, wxEXPAND, 5 );
+ wxBoxSizer* bSizer259;
+ bSizer259 = new wxBoxSizer( wxVERTICAL );
+
+ wxBoxSizer* bSizer258;
+ bSizer258 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_bitmapLogFile = new wxStaticBitmap( m_panel39, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
+ bSizer258->Add( m_bitmapLogFile, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 );
+
+ m_hyperlinkLogFolder = new wxHyperlinkCtrl( m_panel39, wxID_ANY, _("dummy"), wxEmptyString, wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE );
+ bSizer258->Add( m_hyperlinkLogFolder, 0, wxALIGN_CENTER_VERTICAL|wxLEFT, 5 );
+
+
+ bSizer259->Add( bSizer258, 0, wxEXPAND|wxALL, 5 );
+
+ wxBoxSizer* bSizer260;
+ bSizer260 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_checkBoxLogFilesMaxAge = new wxCheckBox( m_panel39, wxID_ANY, _("Remove old log files after x days:"), wxDefaultPosition, wxDefaultSize, 0 );
+ bSizer260->Add( m_checkBoxLogFilesMaxAge, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 );
+
+ m_spinCtrlLogFilesMaxAge = new wxSpinCtrl( m_panel39, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 );
+ bSizer260->Add( m_spinCtrlLogFilesMaxAge, 0, wxALIGN_CENTER_VERTICAL, 5 );
+
+
+ bSizer259->Add( bSizer260, 0, wxBOTTOM|wxRIGHT|wxLEFT, 5 );
+
+
+ bSizer166->Add( bSizer259, 0, wxALL, 5 );
+
+ m_staticline361 = new wxStaticLine( m_panel39, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL );
+ bSizer166->Add( m_staticline361, 0, wxEXPAND, 5 );
+
wxBoxSizer* bSizer181;
bSizer181 = new wxBoxSizer( wxVERTICAL );
@@ -4123,6 +4311,8 @@ OptionsDlgGenerated::OptionsDlgGenerated( wxWindow* parent, wxWindowID id, const
// Connect Events
this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( OptionsDlgGenerated::OnClose ) );
m_buttonResetDialogs->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::OnResetDialogs ), NULL, this );
+ m_hyperlinkLogFolder->Connect( wxEVT_COMMAND_HYPERLINK, wxHyperlinkEventHandler( OptionsDlgGenerated::OnShowLogFolder ), NULL, this );
+ m_checkBoxLogFilesMaxAge->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::OnToggleLogfilesLimit ), NULL, this );
m_bpButtonAddRow->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::OnAddRow ), NULL, this );
m_bpButtonRemoveRow->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( OptionsDlgGenerated::OnRemoveRow ), NULL, this );
m_hyperlink17->Connect( wxEVT_COMMAND_HYPERLINK, wxHyperlinkEventHandler( OptionsDlgGenerated::OnHelpShowExamples ), NULL, this );
@@ -4337,7 +4527,7 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS
wxBoxSizer* bSizer178;
bSizer178 = new wxBoxSizer( wxVERTICAL );
- m_staticTextDonate = new wxStaticText( m_panel39, wxID_ANY, _("If you like FreeFileSync:"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE );
+ m_staticTextDonate = new wxStaticText( m_panel39, wxID_ANY, _("If you like FreeFileSync:"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextDonate->Wrap( -1 );
m_staticTextDonate->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) );
m_staticTextDonate->SetForegroundColour( wxColour( 0, 0, 0 ) );
@@ -4386,7 +4576,7 @@ AboutDlgGenerated::AboutDlgGenerated( wxWindow* parent, wxWindowID id, const wxS
m_bitmapThanks = new wxStaticBitmap( m_panel391, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 );
bSizer1841->Add( m_bitmapThanks, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 );
- m_staticTextThanks = new wxStaticText( m_panel391, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE );
+ m_staticTextThanks = new wxStaticText( m_panel391, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextThanks->Wrap( -1 );
m_staticTextThanks->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) );
m_staticTextThanks->SetForegroundColour( wxColour( 0, 0, 0 ) );
@@ -4632,7 +4822,7 @@ DownloadProgressDlgGenerated::DownloadProgressDlgGenerated( wxWindow* parent, wx
m_gaugeProgress->SetValue( 0 );
bSizer212->Add( m_gaugeProgress, 0, wxEXPAND|wxRIGHT|wxLEFT, 5 );
- m_staticTextDetails = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE );
+ m_staticTextDetails = new wxStaticText( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 );
m_staticTextDetails->Wrap( -1 );
bSizer212->Add( m_staticTextDetails, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 );
diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h
index dbed969d..7f6b8843 100755
--- a/FreeFileSync/Source/ui/gui_generated.h
+++ b/FreeFileSync/Source/ui/gui_generated.h
@@ -66,7 +66,7 @@ class MainDialogGenerated : public wxFrame
private:
protected:
- wxMenuBar* m_menubar1;
+ wxMenuBar* m_menubar;
wxMenu* m_menuFile;
wxMenuItem* m_menuItemNew;
wxMenuItem* m_menuItemLoad;
@@ -74,6 +74,7 @@ protected:
wxMenuItem* m_menuItemSaveAs;
wxMenuItem* m_menuItemSaveAsBatch;
wxMenu* m_menu4;
+ wxMenuItem* m_menuItemShowLog;
wxMenuItem* m_menuItemCompare;
wxMenuItem* m_menuItemCompSettings;
wxMenuItem* m_menuItemFilter;
@@ -149,6 +150,10 @@ protected:
wxStaticText* m_staticText101;
wxTextCtrl* m_textCtrlSearchTxt;
wxCheckBox* m_checkBoxMatchCase;
+ wxPanel* m_panelLog;
+ wxBoxSizer* bSizerLog;
+ wxBoxSizer* bSizer42;
+ wxStaticLine* m_staticline70;
wxPanel* m_panelConfig;
wxBoxSizer* bSizerConfig;
wxBoxSizer* bSizerCfgHistoryButtons;
@@ -164,6 +169,7 @@ protected:
zen::Grid* m_gridCfgHistory;
wxPanel* m_panelViewFilter;
wxBoxSizer* bSizerViewFilter;
+ wxBitmapButton* m_bpButtonShowLog;
wxStaticText* m_staticTextViewType;
zen::ToggleButton* m_bpButtonViewTypeSyncAction;
zen::ToggleButton* m_bpButtonShowExcluded;
@@ -208,6 +214,7 @@ protected:
virtual void OnConfigSaveAs( wxCommandEvent& event ) { event.Skip(); }
virtual void OnSaveAsBatchJob( wxCommandEvent& event ) { event.Skip(); }
virtual void OnMenuQuit( wxCommandEvent& event ) { event.Skip(); }
+ virtual void OnShowLog( wxCommandEvent& event ) { event.Skip(); }
virtual void OnCompare( wxCommandEvent& event ) { event.Skip(); }
virtual void OnCmpSettings( wxCommandEvent& event ) { event.Skip(); }
virtual void OnConfigureFilter( wxCommandEvent& event ) { event.Skip(); }
@@ -251,6 +258,15 @@ public:
wxPanel* m_panelTopRight;
fff::FolderHistoryBox* m_folderPathRight;
wxBitmapButton* m_bpButtonSelectAltFolderRight;
+ wxStaticBitmap* m_bitmapLogStatus;
+ wxStaticText* m_staticTextLogStatus;
+ wxPanel* m_panelItemsProcessed;
+ wxStaticText* m_staticTextItemsProcessed;
+ wxStaticText* m_staticTextBytesProcessed;
+ wxPanel* m_panelItemsRemaining;
+ wxStaticText* m_staticTextItemsRemaining;
+ wxStaticText* m_staticTextBytesRemaining;
+ wxStaticText* m_staticTextTotalTime;
wxBoxSizer* bSizerStatistics;
wxBoxSizer* bSizerData;
@@ -785,7 +801,6 @@ protected:
zen::ToggleButton* m_bpButtonWarnings;
zen::ToggleButton* m_bpButtonInfo;
wxStaticLine* m_staticline13;
- zen::Grid* m_gridMessages;
// Virtual event handlers, overide them in your derived class
virtual void OnErrors( wxCommandEvent& event ) { event.Skip(); }
@@ -794,6 +809,7 @@ protected:
public:
+ zen::Grid* m_gridMessages;
LogPanelGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxTAB_TRAVERSAL );
~LogPanelGenerated();
@@ -959,9 +975,14 @@ protected:
wxStaticText* m_staticText93;
wxStaticText* m_staticText932;
wxStaticLine* m_staticline39;
- zen::BitmapTextButton* m_buttonResetDialogs;
wxStaticText* m_staticTextResetDialogs;
+ zen::BitmapTextButton* m_buttonResetDialogs;
wxStaticLine* m_staticline191;
+ wxStaticBitmap* m_bitmapLogFile;
+ wxHyperlinkCtrl* m_hyperlinkLogFolder;
+ wxCheckBox* m_checkBoxLogFilesMaxAge;
+ wxSpinCtrl* m_spinCtrlLogFilesMaxAge;
+ wxStaticLine* m_staticline361;
wxStaticText* m_staticText85;
wxGrid* m_gridCustomCommand;
wxBitmapButton* m_bpButtonAddRow;
@@ -976,6 +997,8 @@ protected:
// Virtual event handlers, overide them in your derived class
virtual void OnClose( wxCloseEvent& event ) { event.Skip(); }
virtual void OnResetDialogs( wxCommandEvent& event ) { event.Skip(); }
+ virtual void OnShowLogFolder( wxHyperlinkEvent& event ) { event.Skip(); }
+ virtual void OnToggleLogfilesLimit( wxCommandEvent& event ) { event.Skip(); }
virtual void OnAddRow( wxCommandEvent& event ) { event.Skip(); }
virtual void OnRemoveRow( wxCommandEvent& event ) { event.Skip(); }
virtual void OnHelpShowExamples( wxHyperlinkEvent& event ) { event.Skip(); }
diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp
index 312ee7db..5abf3931 100755
--- a/FreeFileSync/Source/ui/gui_status_handler.cpp
+++ b/FreeFileSync/Source/ui/gui_status_handler.cpp
@@ -9,21 +9,30 @@
#include <zen/shutdown.h>
#include <wx/app.h>
#include <wx/wupdlock.h>
-#include <wx+/bitmap_button.h>
+//#include <wx+/bitmap_button.h>
#include <wx+/popup_dlg.h>
#include "main_dlg.h"
#include "../base/generate_logfile.h"
#include "../base/resolve_path.h"
-#include "../base/status_handler_impl.h"
+//#include "../base/status_handler_impl.h"
+#include "../fs/concrete.h"
using namespace zen;
using namespace fff;
-StatusHandlerTemporaryPanel::StatusHandlerTemporaryPanel(MainDialog& dlg) : mainDlg_(dlg)
+StatusHandlerTemporaryPanel::StatusHandlerTemporaryPanel(MainDialog& dlg,
+ const std::chrono::system_clock::time_point& startTime,
+ bool ignoreErrors,
+ size_t automaticRetryCount,
+ size_t automaticRetryDelay) :
+ mainDlg_(dlg),
+ automaticRetryCount_(automaticRetryCount),
+ automaticRetryDelay_(automaticRetryDelay),
+ startTime_(startTime)
{
{
- mainDlg_.compareStatus_->init(*this, false /*ignoreErrors*/, 0 /*automaticRetryCount*/); //clear old values before showing panel
+ mainDlg_.compareStatus_->init(*this, ignoreErrors, automaticRetryCount); //clear old values before showing panel
//------------------------------------------------------------------
const wxAuiPaneInfo& topPanel = mainDlg_.auiMgr_.GetPane(mainDlg_.m_panelTopButtons);
@@ -54,8 +63,8 @@ StatusHandlerTemporaryPanel::StatusHandlerTemporaryPanel(MainDialog& dlg) : main
{
for (size_t i = 0; i < paneArray.size(); ++i)
{
- wxAuiPaneInfo& paneInfo = paneArray[i];
-
+ const wxAuiPaneInfo& paneInfo = paneArray[i];
+ //doesn't matter if paneInfo.IsShown() or not! => move down in either case!
if (&paneInfo != &statusPanel &&
paneInfo.dock_layer == statusPanel.dock_layer &&
paneInfo.dock_direction == statusPanel.dock_direction &&
@@ -97,22 +106,56 @@ StatusHandlerTemporaryPanel::~StatusHandlerTemporaryPanel()
mainDlg_.Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(StatusHandlerTemporaryPanel::OnKeyPressed), nullptr, this);
mainDlg_.m_buttonCancel->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusHandlerTemporaryPanel::OnAbortCompare), nullptr, this);
+ //Workaround wxAuiManager crash when starting panel resizing during comparison and holding button until after comparison has finished:
+ //- unlike regular window resizing, wxAuiManager does not run a dedicated event loop while the mouse button is held
+ //- wxAuiManager internally stores the panel index that is currently resized
+ //- our previous hiding of the compare status panel invalidates this index
+ // => the next mouse move will have wxAuiManager crash => another fine piece of "wxQuality" code
+ // => mitigate:
+ wxMouseCaptureLostEvent dummy;
+ mainDlg_.auiMgr_.ProcessEvent(dummy); //should be no-op if no mouse buttons are pressed
+
mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).Hide();
mainDlg_.auiMgr_.Update();
mainDlg_.compareStatus_->teardown();
+
+ if (!errorLog_.empty()) //reportFinalStatus() was not called!
+ std::abort();
}
-void StatusHandlerTemporaryPanel::OnKeyPressed(wxKeyEvent& event)
+StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::reportFinalStatus() //noexcept!!
{
- const int keyCode = event.GetKeyCode();
- if (keyCode == WXK_ESCAPE)
+ const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_);
+
+ //determine post-sync status irrespective of further errors during tear-down
+ const SyncResult finalStatus = [&]
{
- wxCommandEvent dummy;
- OnAbortCompare(dummy);
- }
+ if (getAbortStatus())
+ return SyncResult::ABORTED;
+ else if (errorLog_.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR) > 0)
+ return SyncResult::FINISHED_WITH_ERROR;
+ else if (errorLog_.getItemCount(MSG_TYPE_WARNING) > 0)
+ return SyncResult::FINISHED_WITH_WARNINGS;
+ else
+ return SyncResult::FINISHED_WITH_SUCCESS;
+ }();
- event.Skip();
+ errorLog_.logMsg(getFinalStatusLabel(finalStatus), getFinalMsgType(finalStatus));
+
+ ProcessSummary summary
+ {
+ finalStatus, {} /*jobName*/,
+ getStatsCurrent(currentPhase()),
+ getStatsTotal (currentPhase()),
+ totalTime
+ };
+
+
+ auto errorLogFinal = std::make_shared<const ErrorLog>(std::move(errorLog_));
+ errorLog_ = ErrorLog(); //see check in ~StatusHandlerTemporaryPanel()
+
+ return { summary, errorLogFinal };
}
@@ -132,20 +175,58 @@ void StatusHandlerTemporaryPanel::logInfo(const std::wstring& msg)
}
-ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::wstring& errorMessage, size_t retryNumber)
+void StatusHandlerTemporaryPanel::reportWarning(const std::wstring& msg, bool& warningActive)
{
- //no need to implement auto-retry here: 1. user is watching 2. comparison is fast
- //=> similar behavior like "ignoreErrors" which is also not used for the comparison phase in GUI mode
+ PauseTimers dummy(*mainDlg_.compareStatus_);
+
+ errorLog_.logMsg(msg, MSG_TYPE_WARNING);
+
+ if (!warningActive) //if errors are ignored, then warnings should also
+ return;
+
+ if (!mainDlg_.compareStatus_->getOptionIgnoreErrors())
+ {
+ forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
+
+ bool dontWarnAgain = false;
+ switch (showConfirmationDialog(&mainDlg_, DialogInfoType::WARNING,
+ PopupDialogCfg().setDetailInstructions(msg).
+ setCheckBox(dontWarnAgain, _("&Don't show this warning again")),
+ _("&Ignore")))
+ {
+ case ConfirmationButton::ACCEPT:
+ warningActive = !dontWarnAgain;
+ break;
+ case ConfirmationButton::CANCEL:
+ userAbortProcessNow(); //throw AbortProcess
+ break;
+ }
+ }
+ //else: if errors are ignored, then warnings should also
+}
+
+
+ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::wstring& msg, size_t retryNumber)
+{
+ PauseTimers dummy(*mainDlg_.compareStatus_);
+
+ //auto-retry
+ if (retryNumber < automaticRetryCount_)
+ {
+ errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO);
+ delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); });
+ return ProcessCallback::RETRY;
+ }
//always, except for "retry":
- auto guardWriteLog = zen::makeGuard<ScopeGuardRunMode::ON_EXIT>([&] { errorLog_.logMsg(errorMessage, MSG_TYPE_ERROR); });
+ auto guardWriteLog = zen::makeGuard<ScopeGuardRunMode::ON_EXIT>([&] { errorLog_.logMsg(msg, MSG_TYPE_ERROR); });
if (!mainDlg_.compareStatus_->getOptionIgnoreErrors())
{
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
- switch (showConfirmationDialog(&mainDlg_, DialogInfoType::ERROR2, PopupDialogCfg().
- setDetailInstructions(errorMessage),
+ switch (showConfirmationDialog(&mainDlg_, DialogInfoType::ERROR2,
+ PopupDialogCfg().setDetailInstructions(msg),
_("&Ignore"), _("Ignore &all"), _("&Retry")))
{
case ConfirmationButton3::ACCEPT: //ignore
@@ -157,7 +238,7 @@ ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::ws
case ConfirmationButton3::DECLINE: //retry
guardWriteLog.dismiss();
- errorLog_.logMsg(errorMessage + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO); //explain why there are duplicate "doing operation X" info messages in the log!
+ errorLog_.logMsg(msg + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO); //explain why there are duplicate "doing operation X" info messages in the log!
return ProcessCallback::RETRY;
case ConfirmationButton3::CANCEL:
@@ -173,41 +254,33 @@ ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::ws
}
-void StatusHandlerTemporaryPanel::reportFatalError(const std::wstring& errorMessage)
+void StatusHandlerTemporaryPanel::reportFatalError(const std::wstring& msg)
{
- errorLog_.logMsg(errorMessage, MSG_TYPE_FATAL_ERROR);
-
- forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
- showNotificationDialog(&mainDlg_, DialogInfoType::ERROR2, PopupDialogCfg().setTitle(_("Serious Error")).setDetailInstructions(errorMessage));
-}
+ PauseTimers dummy(*mainDlg_.compareStatus_);
-
-void StatusHandlerTemporaryPanel::reportWarning(const std::wstring& warningMessage, bool& warningActive)
-{
- errorLog_.logMsg(warningMessage, MSG_TYPE_WARNING);
-
- if (!warningActive) //if errors are ignored, then warnings should also
- return;
+ errorLog_.logMsg(msg, MSG_TYPE_FATAL_ERROR);
if (!mainDlg_.compareStatus_->getOptionIgnoreErrors())
{
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
- bool dontWarnAgain = false;
- switch (showConfirmationDialog(&mainDlg_, DialogInfoType::WARNING,
- PopupDialogCfg().setDetailInstructions(warningMessage).
- setCheckBox(dontWarnAgain, _("&Don't show this warning again")),
- _("&Ignore")))
+ switch (showConfirmationDialog(&mainDlg_, DialogInfoType::ERROR2,
+ PopupDialogCfg().setTitle(_("Serious Error")).
+ setDetailInstructions(msg),
+ _("&Ignore"), _("Ignore &all")))
{
- case ConfirmationButton::ACCEPT:
- warningActive = !dontWarnAgain;
+ case ConfirmationButton2::ACCEPT: //ignore
break;
- case ConfirmationButton::CANCEL:
+
+ case ConfirmationButton2::ACCEPT_ALL: //ignore all
+ mainDlg_.compareStatus_->setOptionIgnoreErrors(true);
+ break;
+
+ case ConfirmationButton2::CANCEL:
userAbortProcessNow(); //throw AbortProcess
break;
}
}
- //else: if errors are ignored, then warnings should also
}
@@ -217,6 +290,19 @@ void StatusHandlerTemporaryPanel::forceUiRefreshNoThrow()
}
+void StatusHandlerTemporaryPanel::OnKeyPressed(wxKeyEvent& event)
+{
+ const int keyCode = event.GetKeyCode();
+ if (keyCode == WXK_ESCAPE)
+ {
+ wxCommandEvent dummy;
+ OnAbortCompare(dummy);
+ }
+
+ event.Skip();
+}
+
+
void StatusHandlerTemporaryPanel::OnAbortCompare(wxCommandEvent& event)
{
userRequestAbort();
@@ -226,7 +312,6 @@ void StatusHandlerTemporaryPanel::OnAbortCompare(wxCommandEvent& event)
StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg,
const std::chrono::system_clock::time_point& startTime,
- size_t lastSyncsLogFileSizeMax,
bool ignoreErrors,
size_t automaticRetryCount,
size_t automaticRetryDelay,
@@ -234,59 +319,59 @@ StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg,
const Zstring& soundFileSyncComplete,
const Zstring& postSyncCommand,
PostSyncCondition postSyncCondition,
- bool& exitAfterSync,
bool& autoCloseDialog) :
progressDlg_(createProgressDialog(*this, [this] { this->onProgressDialogTerminate(); }, *this, parentDlg, true /*showProgress*/, autoCloseDialog,
jobName, soundFileSyncComplete, ignoreErrors, automaticRetryCount, PostSyncAction2::NONE)),
- lastSyncsLogFileSizeMax_(lastSyncsLogFileSizeMax),
automaticRetryCount_(automaticRetryCount),
automaticRetryDelay_(automaticRetryDelay),
jobName_(jobName),
startTime_(startTime),
postSyncCommand_(postSyncCommand),
postSyncCondition_(postSyncCondition),
- exitAfterSync_(exitAfterSync),
- autoCloseDialogOut_(autoCloseDialog)
+autoCloseDialogOut_(autoCloseDialog) {}
+
+
+StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
{
- assert(!exitAfterSync);
+ if (progressDlg_) //reportFinalStatus() was not called!
+ std::abort();
}
-StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
+StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::reportFinalStatus(int logfilesMaxAgeDays, const std::set<Zstring, LessFilePath>& logFilePathsToKeep)
{
- const int totalErrors = errorLog_.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR); //evaluate before finalizing log
- const int totalWarnings = errorLog_.getItemCount(MSG_TYPE_WARNING);
+ const auto totalTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime_);
- //finalize error log
- SyncProgressDialog::SyncResult finalStatus = SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS;
- std::wstring finalStatusMsg;
- if (getAbortStatus())
- {
- finalStatus = SyncProgressDialog::RESULT_ABORTED;
- finalStatusMsg = _("Stopped");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_ERROR);
- }
- else if (totalErrors > 0)
- {
- finalStatus = SyncProgressDialog::RESULT_FINISHED_WITH_ERROR;
- finalStatusMsg = _("Completed with errors");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_ERROR);
- }
- else if (totalWarnings > 0)
- {
- finalStatus = SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS;
- finalStatusMsg = _("Completed with warnings");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_WARNING); //give status code same warning priority as display category!
- }
- else
+ if (progressDlg_) progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep
+
+ //determine post-sync status irrespective of further errors during tear-down
+ const SyncResult finalStatus = [&]
{
- if (getItemsTotal(PHASE_SYNCHRONIZING) == 0 && //we're past "initNewPhase(PHASE_SYNCHRONIZING)" at this point!
- getBytesTotal(PHASE_SYNCHRONIZING) == 0)
- finalStatusMsg = _("Nothing to synchronize"); //even if "ignored conflicts" occurred!
+ if (getAbortStatus())
+ return SyncResult::ABORTED;
+ else if (errorLog_.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR) > 0)
+ return SyncResult::FINISHED_WITH_ERROR;
+ else if (errorLog_.getItemCount(MSG_TYPE_WARNING) > 0)
+ return SyncResult::FINISHED_WITH_WARNINGS;
else
- finalStatusMsg = _("Completed successfully");
- errorLog_.logMsg(finalStatusMsg, MSG_TYPE_INFO);
- }
+ return SyncResult::FINISHED_WITH_SUCCESS;
+ }();
+
+ assert(finalStatus == SyncResult::ABORTED || currentPhase() == PHASE_SYNCHRONIZING);
+
+ ProcessSummary summary
+ {
+ finalStatus, jobName_,
+ getStatsCurrent(currentPhase()),
+ getStatsTotal (currentPhase()),
+ totalTime
+ };
+
+ const std::wstring& finalStatusLabel = finalStatus == SyncResult::FINISHED_WITH_SUCCESS &&
+ summary.statsTotal.items == 0 &&
+ summary.statsTotal.bytes == 0 ? _("Nothing to synchronize") :
+ getFinalStatusLabel(finalStatus);
+ errorLog_.logMsg(finalStatusLabel, getFinalMsgType(finalStatus));
//post sync command
Zstring commandLine = [&]
@@ -299,13 +384,13 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
case PostSyncCondition::COMPLETION:
return postSyncCommand_;
case PostSyncCondition::ERRORS:
- if (finalStatus == SyncProgressDialog::RESULT_ABORTED ||
- finalStatus == SyncProgressDialog::RESULT_FINISHED_WITH_ERROR)
+ if (finalStatus == SyncResult::ABORTED ||
+ finalStatus == SyncResult::FINISHED_WITH_ERROR)
return postSyncCommand_;
break;
case PostSyncCondition::SUCCESS:
- if (finalStatus == SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS ||
- finalStatus == SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS)
+ if (finalStatus == SyncResult::FINISHED_WITH_WARNINGS ||
+ finalStatus == SyncResult::FINISHED_WITH_SUCCESS)
return postSyncCommand_;
break;
}
@@ -316,26 +401,13 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
if (!commandLine.empty())
errorLog_.logMsg(replaceCpy(_("Executing command %x"), L"%x", fmtPath(commandLine)), MSG_TYPE_INFO);
- //----------------- write results into LastSyncs.log------------------------
- const LogSummary summary =
- {
- jobName_, finalStatusMsg,
- getItemsCurrent(PHASE_SYNCHRONIZING), getBytesCurrent(PHASE_SYNCHRONIZING),
- getItemsTotal (PHASE_SYNCHRONIZING), getBytesTotal (PHASE_SYNCHRONIZING),
- std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - startTime_).count()
- };
- if (progressDlg_) progressDlg_->timerSetStatus(false /*active*/); //keep correct summary window stats considering count down timer, system sleep
-
- //do NOT use tryReportingError()! saving log files should not be cancellable!
- auto notifyStatusNoThrow = [&](const std::wstring& msg)
- {
- try { reportStatus(msg); /*throw X*/ }
- catch (...) {}
- };
-
+ //----------------- always save log under %appdata%\FreeFileSync\Logs ------------------------
+ Zstring logFilePath;
try
{
- saveToLastSyncsLog(summary, errorLog_, lastSyncsLogFileSizeMax_, notifyStatusNoThrow); //throw FileError, (X)
+ //do NOT use tryReportingError()! saving log files should not be cancellable!
+ auto notifyStatusNoThrow = [&](const std::wstring& msg) { try { reportStatus(msg); /*throw X*/ } catch (...) {} };
+ logFilePath = saveLogFile(summary, errorLog_, startTime_, logfilesMaxAgeDays, logFilePathsToKeep, notifyStatusNoThrow /*throw (X)*/); //throw FileError
}
catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
@@ -343,6 +415,9 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
if (!commandLine.empty())
try
{
+ //----------------------------------------------------------------------
+ ::wxSetEnv(L"logfile_path", utfTo<wxString>(logFilePath));
+ //----------------------------------------------------------------------
//use ExecutionType::ASYNC until there is reason not to: https://freefilesync.org/forum/viewtopic.php?t=31
shellExecute(expandMacros(commandLine), ExecutionType::ASYNC); //throw FileError
}
@@ -374,6 +449,8 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
//post sync action
bool autoClose = false;
+ bool exitAfterSync = false;
+
if (getAbortStatus() && *getAbortStatus() == AbortTrigger::USER)
; //user cancelled => don't run post sync command!
else
@@ -383,7 +460,7 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
autoClose = progressDlg_->getOptionAutoCloseDialog();
break;
case PostSyncAction2::EXIT:
- autoClose = exitAfterSync_ = true; //program exit must be handled by calling context!
+ autoClose = exitAfterSync = true; //program exit must be handled by calling context!
break;
case PostSyncAction2::SLEEP:
if (mayRunAfterCountDown(_("System: Sleep")))
@@ -399,17 +476,19 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
try
{
shutdownSystem(); //throw FileError
- autoClose = exitAfterSync_ = true;
+ autoClose = exitAfterSync = true;
}
catch (const FileError& e) { errorLog_.logMsg(e.toString(), MSG_TYPE_ERROR); }
break;
}
+ auto errorLogFinal = std::make_shared<const ErrorLog>(std::move(errorLog_));
+
//close progress dialog
if (autoClose)
- progressDlg_->closeDirectly(!exitAfterSync_ /*restoreParentFrame*/);
+ progressDlg_->closeDirectly(!exitAfterSync /*restoreParentFrame*/);
else
- progressDlg_->showSummary(finalStatus, errorLog_);
+ progressDlg_->showSummary(finalStatus, errorLogFinal);
//wait until progress dialog notified shutdown via onProgressDialogTerminate()
//-> required since it has our "this" pointer captured in lambda "notifyWindowTerminate"!
@@ -420,7 +499,11 @@ StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog()
if (!progressDlg_) break;
std::this_thread::sleep_for(UI_UPDATE_INTERVAL);
}
+
+ return { summary, errorLogFinal, exitAfterSync, logFilePath };
}
+ else
+ return { summary, std::make_shared<const ErrorLog>(std::move(errorLog_)), false /*exitAfterSync */, logFilePath };
}
@@ -435,43 +518,66 @@ void StatusHandlerFloatingDialog::initNewPhase(int itemsTotal, int64_t bytesTota
}
-void StatusHandlerFloatingDialog::updateDataProcessed(int itemsDelta, int64_t bytesDelta)
+void StatusHandlerFloatingDialog::logInfo(const std::wstring& msg)
{
- StatusHandler::updateDataProcessed(itemsDelta, bytesDelta);
- if (progressDlg_)
- progressDlg_->notifyProgressChange(); //noexcept
- //note: this method should NOT throw in order to properly allow undoing setting of statistics!
+ errorLog_.logMsg(msg, MSG_TYPE_INFO);
}
-void StatusHandlerFloatingDialog::logInfo(const std::wstring& msg)
+void StatusHandlerFloatingDialog::reportWarning(const std::wstring& msg, bool& warningActive)
{
- errorLog_.logMsg(msg, MSG_TYPE_INFO);
+ if (!progressDlg_) abortProcessNow();
+ PauseTimers dummy(*progressDlg_);
+
+ errorLog_.logMsg(msg, MSG_TYPE_WARNING);
+
+ if (!warningActive)
+ return;
+
+ if (!progressDlg_->getOptionIgnoreErrors())
+ {
+ forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
+
+ bool dontWarnAgain = false;
+ switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::WARNING,
+ PopupDialogCfg().setDetailInstructions(msg).
+ setCheckBox(dontWarnAgain, _("&Don't show this warning again")),
+ _("&Ignore")))
+ {
+ case ConfirmationButton::ACCEPT:
+ warningActive = !dontWarnAgain;
+ break;
+ case ConfirmationButton::CANCEL:
+ userAbortProcessNow(); //throw AbortProcess
+ break;
+ }
+ }
+ //else: if errors are ignored, then warnings should be, too
}
-ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::wstring& errorMessage, size_t retryNumber)
+ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::wstring& msg, size_t retryNumber)
{
if (!progressDlg_) abortProcessNow();
+ PauseTimers dummy(*progressDlg_);
//auto-retry
if (retryNumber < automaticRetryCount_)
{
- errorLog_.logMsg(errorMessage + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO);
- delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& msg) { this->reportStatus(_("Error") + L": " + msg); });
+ errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO);
+ delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); });
return ProcessCallback::RETRY;
}
//always, except for "retry":
- auto guardWriteLog = zen::makeGuard<ScopeGuardRunMode::ON_EXIT>([&] { errorLog_.logMsg(errorMessage, MSG_TYPE_ERROR); });
+ auto guardWriteLog = zen::makeGuard<ScopeGuardRunMode::ON_EXIT>([&] { errorLog_.logMsg(msg, MSG_TYPE_ERROR); });
if (!progressDlg_->getOptionIgnoreErrors())
{
- PauseTimers dummy(*progressDlg_);
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
- switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::ERROR2, PopupDialogCfg().
- setDetailInstructions(errorMessage),
+ switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::ERROR2,
+ PopupDialogCfg().setDetailInstructions(msg),
_("&Ignore"), _("Ignore &all"), _("&Retry")))
{
case ConfirmationButton3::ACCEPT: //ignore
@@ -483,7 +589,7 @@ ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::ws
case ConfirmationButton3::DECLINE: //retry
guardWriteLog.dismiss();
- errorLog_.logMsg(errorMessage + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO); //explain why there are duplicate "doing operation X" info messages in the log!
+ errorLog_.logMsg(msg + L"\n-> " + _("Retrying operation..."), MSG_TYPE_INFO); //explain why there are duplicate "doing operation X" info messages in the log!
return ProcessCallback::RETRY;
case ConfirmationButton3::CANCEL:
@@ -499,20 +605,20 @@ ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::ws
}
-void StatusHandlerFloatingDialog::reportFatalError(const std::wstring& errorMessage)
+void StatusHandlerFloatingDialog::reportFatalError(const std::wstring& msg)
{
if (!progressDlg_) abortProcessNow();
+ PauseTimers dummy(*progressDlg_);
- errorLog_.logMsg(errorMessage, MSG_TYPE_FATAL_ERROR);
+ errorLog_.logMsg(msg, MSG_TYPE_FATAL_ERROR);
if (!progressDlg_->getOptionIgnoreErrors())
{
- PauseTimers dummy(*progressDlg_);
forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::ERROR2,
PopupDialogCfg().setTitle(_("Serious Error")).
- setDetailInstructions(errorMessage),
+ setDetailInstructions(msg),
_("&Ignore"), _("Ignore &all")))
{
case ConfirmationButton2::ACCEPT:
@@ -530,35 +636,13 @@ void StatusHandlerFloatingDialog::reportFatalError(const std::wstring& errorMess
}
-void StatusHandlerFloatingDialog::reportWarning(const std::wstring& warningMessage, bool& warningActive)
+void StatusHandlerFloatingDialog::updateDataProcessed(int itemsDelta, int64_t bytesDelta)
{
- if (!progressDlg_) abortProcessNow();
-
- errorLog_.logMsg(warningMessage, MSG_TYPE_WARNING);
-
- if (!warningActive)
- return;
-
- if (!progressDlg_->getOptionIgnoreErrors())
- {
- PauseTimers dummy(*progressDlg_);
- forceUiRefreshNoThrow(); //noexcept! => don't throw here when error occurs during clean up!
+ StatusHandler::updateDataProcessed(itemsDelta, bytesDelta);
- bool dontWarnAgain = false;
- switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::WARNING,
- PopupDialogCfg().setDetailInstructions(warningMessage).
- setCheckBox(dontWarnAgain, _("&Don't show this warning again")),
- _("&Ignore")))
- {
- case ConfirmationButton::ACCEPT:
- warningActive = !dontWarnAgain;
- break;
- case ConfirmationButton::CANCEL:
- userAbortProcessNow(); //throw AbortProcess
- break;
- }
- }
- //else: if errors are ignored, then warnings should be, too
+ //note: this method should NOT throw in order to properly allow undoing setting of statistics!
+ if (progressDlg_) progressDlg_->notifyProgressChange(); //noexcept
+ //for "curveDataBytes_->addRecord()"
}
diff --git a/FreeFileSync/Source/ui/gui_status_handler.h b/FreeFileSync/Source/ui/gui_status_handler.h
index 767b1a31..73942f0d 100755
--- a/FreeFileSync/Source/ui/gui_status_handler.h
+++ b/FreeFileSync/Source/ui/gui_status_handler.h
@@ -19,22 +19,26 @@ namespace fff
//classes handling sync and compare errors as well as status feedback
//StatusHandlerTemporaryPanel(CompareProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks!
-class StatusHandlerTemporaryPanel : private wxEvtHandler, public StatusHandler //throw AbortProcess
+class StatusHandlerTemporaryPanel : private wxEvtHandler, public StatusHandler
{
public:
- StatusHandlerTemporaryPanel(MainDialog& dlg);
+ StatusHandlerTemporaryPanel(MainDialog& dlg, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, size_t automaticRetryCount, size_t automaticRetryDelay);
~StatusHandlerTemporaryPanel();
- void initNewPhase(int itemsTotal, int64_t bytesTotal, Phase phaseID) override;
-
- void logInfo (const std::wstring& msg) override;
- Response reportError (const std::wstring& text, size_t retryNumber) override;
- void reportFatalError(const std::wstring& errorMessage) override;
- void reportWarning (const std::wstring& warningMessage, bool& warningActive) override;
+ void initNewPhase (int itemsTotal, int64_t bytesTotal, Phase phaseID) override; //
+ void logInfo (const std::wstring& msg) override; //
+ void reportWarning (const std::wstring& msg, bool& warningActive) override; //throw AbortProcess
+ Response reportError (const std::wstring& msg, size_t retryNumber) override; //
+ void reportFatalError(const std::wstring& msg) override; //
void forceUiRefreshNoThrow() override;
- zen::ErrorLog getErrorLog() const { return errorLog_; }
+ struct Result
+ {
+ ProcessSummary summary;
+ std::shared_ptr<const zen::ErrorLog> errorLog;
+ };
+ Result reportFinalStatus(); //noexcept!!
private:
void OnKeyPressed(wxKeyEvent& event);
@@ -42,6 +46,9 @@ private:
MainDialog& mainDlg_;
zen::ErrorLog errorLog_;
+ const size_t automaticRetryCount_;
+ const size_t automaticRetryDelay_;
+ const std::chrono::system_clock::time_point startTime_;
};
@@ -51,7 +58,6 @@ class StatusHandlerFloatingDialog : public StatusHandler //throw AbortProcess
public:
StatusHandlerFloatingDialog(wxFrame* parentDlg,
const std::chrono::system_clock::time_point& startTime,
- size_t lastSyncsLogFileSizeMax,
bool ignoreErrors,
size_t automaticRetryCount,
size_t automaticRetryDelay,
@@ -59,25 +65,31 @@ public:
const Zstring& soundFileSyncComplete,
const Zstring& postSyncCommand,
PostSyncCondition postSyncCondition,
- bool& exitAfterSync,
bool& autoCloseDialog);
~StatusHandlerFloatingDialog();
- void initNewPhase (int itemsTotal, int64_t bytesTotal, Phase phaseID) override;
- void updateDataProcessed(int itemsDelta, int64_t bytesDelta ) override;
+ void initNewPhase (int itemsTotal, int64_t bytesTotal, Phase phaseID) override; //
+ void logInfo (const std::wstring& msg) override; //
+ void reportWarning (const std::wstring& msg, bool& warningActive) override; //throw AbortProcess
+ Response reportError (const std::wstring& msg, size_t retryNumber) override; //
+ void reportFatalError(const std::wstring& msg) override; //
- void logInfo (const std::wstring& msg ) override;
- Response reportError (const std::wstring& text, size_t retryNumber ) override;
- void reportFatalError(const std::wstring& errorMessage ) override;
- void reportWarning (const std::wstring& warningMessage, bool& warningActive) override;
+ void updateDataProcessed(int itemsDelta, int64_t bytesDelta) override; //noexcept!!
+ void forceUiRefreshNoThrow() override; //
- void forceUiRefreshNoThrow() override;
+ struct Result
+ {
+ ProcessSummary summary;
+ std::shared_ptr<const zen::ErrorLog> errorLog;
+ bool exitAfterSync;
+ Zstring logFilePath;
+ };
+ Result reportFinalStatus(int logfilesMaxAgeDays, const std::set<Zstring, LessFilePath>& logFilePathsToKeep); //noexcept!!
private:
void onProgressDialogTerminate();
SyncProgressDialog* progressDlg_; //managed to have shorter lifetime than this handler!
- const size_t lastSyncsLogFileSizeMax_;
zen::ErrorLog errorLog_;
const size_t automaticRetryCount_;
const size_t automaticRetryDelay_;
@@ -85,7 +97,6 @@ private:
const std::chrono::system_clock::time_point startTime_;
const Zstring postSyncCommand_;
const PostSyncCondition postSyncCondition_;
- bool& exitAfterSync_;
bool& autoCloseDialogOut_; //owned by SyncProgressDialog
};
}
diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp
new file mode 100755
index 00000000..545bc594
--- /dev/null
+++ b/FreeFileSync/Source/ui/log_panel.cpp
@@ -0,0 +1,566 @@
+// *****************************************************************************
+// * 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 "log_panel.h"
+#include <wx/clipbrd.h>
+#include <wx+/focus.h>
+#include <wx+/image_resources.h>
+#include <wx+/rtl.h>
+#include <wx+/context_menu.h>
+#include <wx+/popup_dlg.h>
+
+using namespace zen;
+using namespace fff;
+
+
+namespace
+{
+inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey
+
+
+inline
+wxBitmap getImageButtonPressed(const wchar_t* name)
+{
+ return layOver(getResourceImage(L"msg_button_pressed"), getResourceImage(name));
+}
+
+
+inline
+wxBitmap getImageButtonReleased(const wchar_t* name)
+{
+ return greyScale(getResourceImage(name)).ConvertToImage();
+ //getResourceImage(utfTo<wxString>(name)).ConvertToImage().ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally!
+ //brighten(output, 30);
+
+ //moveImage(output, 1, 0); //move image right one pixel
+ //return output;
+}
+
+
+enum class ColumnTypeMsg
+{
+ TIME,
+ CATEGORY,
+ TEXT,
+};
+}
+
+
+//a vector-view on ErrorLog considering multi-line messages: prepare consumption by Grid
+class fff::MessageView
+{
+public:
+ MessageView(const std::shared_ptr<const ErrorLog>& log /*bound*/) : log_(log) {}
+
+ size_t rowsOnView() const { return viewRef_.size(); }
+
+ struct LogEntryView
+ {
+ time_t time = 0;
+ MessageType type = MSG_TYPE_INFO;
+ Zstringw messageLine;
+ bool firstLine = false; //if LogEntry::message spans multiple rows
+ };
+
+ Opt<LogEntryView> getEntry(size_t row) const
+ {
+ if (row < viewRef_.size())
+ {
+ const Line& line = viewRef_[row];
+
+ LogEntryView output;
+ output.time = line.logIt_->time;
+ output.type = line.logIt_->type;
+ output.messageLine = extractLine(line.logIt_->message, line.rowNumber_);
+ output.firstLine = line.rowNumber_ == 0; //this is virtually always correct, unless first line of the original message is empty!
+ return output;
+ }
+ return NoValue();
+ }
+
+ void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, ect. see error_log.h
+ {
+ viewRef_.clear();
+
+ for (auto it = log_->begin(); it != log_->end(); ++it)
+ if (it->type & includedTypes)
+ {
+ static_assert(std::is_same_v<GetCharTypeT<Zstringw>, wchar_t>);
+ assert(!startsWith(it->message, L'\n'));
+
+ size_t rowNumber = 0;
+ bool lastCharNewline = true;
+ for (const wchar_t c : it->message)
+ if (c == L'\n')
+ {
+ if (!lastCharNewline) //do not reference empty lines!
+ viewRef_.emplace_back(it, rowNumber);
+ ++rowNumber;
+ lastCharNewline = true;
+ }
+ else
+ lastCharNewline = false;
+
+ if (!lastCharNewline)
+ viewRef_.emplace_back(it, rowNumber);
+ }
+ }
+
+private:
+ static Zstringw extractLine(const Zstringw& message, size_t textRow)
+ {
+ auto it1 = message.begin();
+ for (;;)
+ {
+ auto it2 = std::find_if(it1, message.end(), [](wchar_t c) { return c == L'\n'; });
+ if (textRow == 0)
+ return it1 == message.end() ? Zstringw() : Zstringw(&*it1, it2 - it1); //must not dereference iterator pointing to "end"!
+
+ if (it2 == message.end())
+ {
+ assert(false);
+ return Zstringw();
+ }
+
+ it1 = it2 + 1; //skip newline
+ --textRow;
+ }
+ }
+
+ struct Line
+ {
+ Line(ErrorLog::const_iterator logIt, size_t rowNumber) : logIt_(logIt), rowNumber_(rowNumber) {}
+
+ ErrorLog::const_iterator logIt_; //always bound!
+ size_t rowNumber_; //LogEntry::message may span multiple rows
+ };
+
+ std::vector<Line> viewRef_; //partial view on log_
+ /* /|\
+ | updateView()
+ | */
+ const std::shared_ptr<const ErrorLog> log_;
+};
+
+//-----------------------------------------------------------------------------
+namespace
+{
+//Grid data implementation referencing MessageView
+class GridDataMessages : public GridData
+{
+public:
+ GridDataMessages(const std::shared_ptr<const ErrorLog>& log /*bound!*/) : msgView_(log) {}
+
+ MessageView& getDataView() { return msgView_; }
+
+ size_t getRowCount() const override { return msgView_.rowsOnView(); }
+
+ std::wstring getValue(size_t row, ColumnType colType) const override
+ {
+ if (Opt<MessageView::LogEntryView> entry = msgView_.getEntry(row))
+ switch (static_cast<ColumnTypeMsg>(colType))
+ {
+ case ColumnTypeMsg::TIME:
+ if (entry->firstLine)
+ return formatTime<std::wstring>(FORMAT_TIME, getLocalTime(entry->time));
+ break;
+
+ case ColumnTypeMsg::CATEGORY:
+ if (entry->firstLine)
+ switch (entry->type)
+ {
+ case MSG_TYPE_INFO:
+ return _("Info");
+ case MSG_TYPE_WARNING:
+ return _("Warning");
+ case MSG_TYPE_ERROR:
+ return _("Error");
+ case MSG_TYPE_FATAL_ERROR:
+ return _("Serious Error");
+ }
+ break;
+
+ case ColumnTypeMsg::TEXT:
+ return copyStringTo<std::wstring>(entry->messageLine);
+ }
+ 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;
+
+ //-------------- draw item separation line -----------------
+ {
+ wxDCPenChanger dummy2(dc, getColorGridLine());
+ const bool drawBottomLine = [&] //don't separate multi-line messages
+ {
+ if (Opt<MessageView::LogEntryView> nextEntry = msgView_.getEntry(row + 1))
+ return nextEntry->firstLine;
+ return true;
+ }();
+
+ if (drawBottomLine)
+ {
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
+ --rectTmp.height;
+ }
+ }
+ //--------------------------------------------------------
+
+ if (Opt<MessageView::LogEntryView> entry = msgView_.getEntry(row))
+ switch (static_cast<ColumnTypeMsg>(colType))
+ {
+ case ColumnTypeMsg::TIME:
+ drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER);
+ break;
+
+ case ColumnTypeMsg::CATEGORY:
+ if (entry->firstLine)
+ {
+ wxBitmap msgTypeIcon = [&]
+ {
+ switch (entry->type)
+ {
+ case MSG_TYPE_INFO:
+ return getResourceImage(L"msg_info_sicon");
+ case MSG_TYPE_WARNING:
+ return getResourceImage(L"msg_warning_sicon");
+ case MSG_TYPE_ERROR:
+ case MSG_TYPE_FATAL_ERROR:
+ return getResourceImage(L"msg_error_sicon");
+ }
+ assert(false);
+ return wxNullBitmap;
+ }();
+ drawBitmapRtlNoMirror(dc, enabled ? msgTypeIcon : msgTypeIcon.ConvertToDisabled(), rectTmp, wxALIGN_CENTER);
+ }
+ break;
+
+ case ColumnTypeMsg::TEXT:
+ rectTmp.x += getColumnGapLeft();
+ rectTmp.width -= getColumnGapLeft();
+ drawCellText(dc, rectTmp, getValue(row, colType));
+ break;
+ }
+ }
+
+ void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) override
+ {
+ GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, enabled && selected);
+ }
+
+ int getBestSize(wxDC& dc, size_t row, ColumnType colType) override
+ {
+ // -> synchronize renderCell() <-> getBestSize()
+
+ if (msgView_.getEntry(row))
+ switch (static_cast<ColumnTypeMsg>(colType))
+ {
+ case ColumnTypeMsg::TIME:
+ return 2 * getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
+
+ case ColumnTypeMsg::CATEGORY:
+ return getResourceImage(L"msg_info_sicon").GetWidth();
+
+ case ColumnTypeMsg::TEXT:
+ return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
+ }
+ return 0;
+ }
+
+ static int getColumnTimeDefaultWidth(Grid& grid)
+ {
+ wxClientDC dc(&grid.getMainWin());
+ dc.SetFont(grid.getMainWin().GetFont());
+ return 2 * getColumnGapLeft() + dc.GetTextExtent(formatTime<wxString>(FORMAT_TIME)).GetWidth();
+ }
+
+ static int getColumnCategoryDefaultWidth()
+ {
+ return getResourceImage(L"msg_info_sicon").GetWidth();
+ }
+
+ static int getRowDefaultHeight(const Grid& grid)
+ {
+ return std::max(getResourceImage(L"msg_info_sicon").GetHeight(), grid.getMainWin().GetCharHeight() + fastFromDIP(2)) + 1; //+ some space + bottom border
+ }
+
+ std::wstring getToolTip(size_t row, ColumnType colType) const override
+ {
+ switch (static_cast<ColumnTypeMsg>(colType))
+ {
+ case ColumnTypeMsg::TIME:
+ case ColumnTypeMsg::TEXT:
+ break;
+
+ case ColumnTypeMsg::CATEGORY:
+ return getValue(row, colType);
+ }
+ return std::wstring();
+ }
+
+ std::wstring getColumnLabel(ColumnType colType) const override { return std::wstring(); }
+
+private:
+ MessageView msgView_;
+};
+}
+
+//########################################################################################
+
+void LogPanel::setLog(const std::shared_ptr<const ErrorLog>& log)
+{
+ std::shared_ptr<const zen::ErrorLog> newLog = log;
+ if (!newLog)
+ {
+ auto placeHolderLog = std::make_shared<ErrorLog>();
+ placeHolderLog->logMsg(_("No log entries"), MSG_TYPE_INFO);
+ newLog = placeHolderLog;
+ }
+
+ const int errorCount = newLog->getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR);
+ const int warningCount = newLog->getItemCount(MSG_TYPE_WARNING);
+ const int infoCount = newLog->getItemCount(MSG_TYPE_INFO);
+
+ auto initButton = [](ToggleButton& btn, const wchar_t* imgName, const wxString& tooltip)
+ {
+ btn.init(getImageButtonPressed(imgName), getImageButtonReleased(imgName));
+ btn.SetToolTip(tooltip);
+ };
+
+ initButton(*m_bpButtonErrors, L"msg_error", _("Error" ) + L" (" + formatNumber(errorCount) + L")");
+ initButton(*m_bpButtonWarnings, L"msg_warning", _("Warning") + L" (" + formatNumber(warningCount) + L")");
+ initButton(*m_bpButtonInfo, L"msg_info", _("Info" ) + L" (" + formatNumber(infoCount) + L")");
+
+ m_bpButtonErrors ->setActive(true);
+ m_bpButtonWarnings->setActive(true);
+ m_bpButtonInfo ->setActive(errorCount + warningCount == 0);
+
+ m_bpButtonErrors ->Show(errorCount != 0);
+ m_bpButtonWarnings->Show(warningCount != 0);
+ m_bpButtonInfo ->Show(infoCount != 0);
+
+ //init grid, determine default sizes
+ const int rowHeight = GridDataMessages::getRowDefaultHeight(*m_gridMessages);
+ const int colMsgTimeWidth = GridDataMessages::getColumnTimeDefaultWidth(*m_gridMessages);
+ const int colMsgCategoryWidth = GridDataMessages::getColumnCategoryDefaultWidth();
+
+ m_gridMessages->setDataProvider(std::make_shared<GridDataMessages>(newLog));
+ m_gridMessages->setColumnLabelHeight(0);
+ m_gridMessages->showRowLabel(false);
+ m_gridMessages->setRowHeight(rowHeight);
+ m_gridMessages->setColumnConfig(
+ {
+ { static_cast<ColumnType>(ColumnTypeMsg::TIME ), colMsgTimeWidth, 0, true },
+ { static_cast<ColumnType>(ColumnTypeMsg::CATEGORY), colMsgCategoryWidth, 0, true },
+ { static_cast<ColumnType>(ColumnTypeMsg::TEXT ), -colMsgTimeWidth - colMsgCategoryWidth, 1, true },
+ });
+
+ //support for CTRL + C
+ m_gridMessages->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(LogPanel::onGridButtonEvent), nullptr, this);
+
+ m_gridMessages->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(LogPanel::onMsgGridContext), nullptr, this);
+
+ //enable dialog-specific key events
+ Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(LogPanel::onLocalKeyEvent), nullptr, this);
+
+ updateGrid();
+}
+
+
+MessageView& LogPanel::getDataView()
+{
+ if (auto* prov = dynamic_cast<GridDataMessages*>(m_gridMessages->getDataProvider()))
+ return prov->getDataView();
+ throw std::runtime_error(std::string(__FILE__) + "[" + numberTo<std::string>(__LINE__) + "] m_gridMessages was not initialized.");
+}
+
+
+
+void LogPanel::updateGrid()
+{
+ int includedTypes = 0;
+ if (m_bpButtonErrors->isActive())
+ includedTypes |= MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR;
+
+ if (m_bpButtonWarnings->isActive())
+ includedTypes |= MSG_TYPE_WARNING;
+
+ if (m_bpButtonInfo->isActive())
+ includedTypes |= MSG_TYPE_INFO;
+
+ getDataView().updateView(includedTypes); //update MVC "model"
+ m_gridMessages->Refresh(); //update MVC "view"
+}
+
+void LogPanel::OnErrors(wxCommandEvent& event)
+{
+ m_bpButtonErrors->toggle();
+ updateGrid();
+}
+
+
+void LogPanel::OnWarnings(wxCommandEvent& event)
+{
+ m_bpButtonWarnings->toggle();
+ updateGrid();
+}
+
+
+void LogPanel::OnInfo(wxCommandEvent& event)
+{
+ m_bpButtonInfo->toggle();
+ updateGrid();
+}
+
+
+void LogPanel::onGridButtonEvent(wxKeyEvent& event)
+{
+ int keyCode = event.GetKeyCode();
+
+ if (event.ControlDown())
+ switch (keyCode)
+ {
+ //case 'A': -> "select all" is already implemented by Grid!
+
+ case 'C':
+ case WXK_INSERT: //CTRL + C || CTRL + INS
+ copySelectionToClipboard();
+ return; // -> swallow event! don't allow default grid commands!
+ }
+
+ //else
+ //switch (keyCode)
+ //{
+ // case WXK_RETURN:
+ // case WXK_NUMPAD_ENTER:
+ // return;
+ //}
+
+ event.Skip(); //unknown keypress: propagate
+}
+
+
+void LogPanel::onMsgGridContext(GridClickEvent& event)
+{
+ const std::vector<size_t> selection = m_gridMessages->getSelectedRows();
+
+ const size_t rowCount = [&]() -> size_t
+ {
+ if (auto prov = m_gridMessages->getDataProvider())
+ return prov->getRowCount();
+ return 0;
+ }();
+
+ ContextMenu menu;
+ menu.addItem(_("Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, nullptr, !selection.empty());
+ menu.addSeparator();
+
+ menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridMessages->selectAllRows(GridEventPolicy::ALLOW); }, nullptr, rowCount > 0);
+ menu.popup(*this);
+}
+
+
+void LogPanel::onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
+{
+ if (processingKeyEventHandler_) //avoid recursion
+ {
+ event.Skip();
+ return;
+ }
+ processingKeyEventHandler_ = true;
+ ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false);
+
+
+ const int keyCode = event.GetKeyCode();
+
+ if (event.ControlDown())
+ switch (keyCode)
+ {
+ case 'A':
+ m_gridMessages->SetFocus();
+ m_gridMessages->selectAllRows(GridEventPolicy::ALLOW);
+ return; // -> swallow event! don't allow default grid commands!
+
+ //case 'C': -> already implemented by "Grid" class
+ }
+ else
+ switch (keyCode)
+ {
+ //redirect certain (unhandled) keys directly to grid!
+ case WXK_UP:
+ case WXK_DOWN:
+ case WXK_LEFT:
+ case WXK_RIGHT:
+ case WXK_PAGEUP:
+ case WXK_PAGEDOWN:
+ case WXK_HOME:
+ case WXK_END:
+
+ case WXK_NUMPAD_UP:
+ case WXK_NUMPAD_DOWN:
+ case WXK_NUMPAD_LEFT:
+ case WXK_NUMPAD_RIGHT:
+ case WXK_NUMPAD_PAGEUP:
+ case WXK_NUMPAD_PAGEDOWN:
+ case WXK_NUMPAD_HOME:
+ case WXK_NUMPAD_END:
+ if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus
+ m_gridMessages->IsEnabled())
+ if (wxEvtHandler* evtHandler = m_gridMessages->getMainWin().GetEventHandler())
+ {
+ m_gridMessages->SetFocus();
+
+ event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
+ evtHandler->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it...
+ event.Skip(false); //definitively handled now!
+ return;
+ }
+ break;
+ }
+
+ event.Skip();
+}
+
+
+void LogPanel::copySelectionToClipboard()
+{
+ try
+ {
+ Zstringw clipboardString; //guaranteed exponential growth, unlike wxString
+
+ if (auto prov = m_gridMessages->getDataProvider())
+ {
+ std::vector<Grid::ColAttributes> 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::ColAttributes& ca)
+ {
+ clipboardString += copyStringTo<Zstringw>(prov->getValue(row, ca.type));
+ clipboardString += L'\t';
+ });
+ clipboardString += copyStringTo<Zstringw>(prov->getValue(row, colAttr.back().type));
+ clipboardString += L'\n';
+ }
+ }
+
+ //finally write to clipboard
+ if (!clipboardString.empty())
+ if (wxClipboard::Get()->Open())
+ {
+ ZEN_ON_SCOPE_EXIT(wxClipboard::Get()->Close());
+ wxClipboard::Get()->SetData(new wxTextDataObject(copyStringTo<wxString>(clipboardString))); //ownership passed
+ }
+ }
+ catch (const std::bad_alloc& e)
+ {
+ showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L" " + utfTo<std::wstring>(e.what())));
+ }
+}
diff --git a/FreeFileSync/Source/ui/log_panel.h b/FreeFileSync/Source/ui/log_panel.h
new file mode 100755
index 00000000..47f9a84a
--- /dev/null
+++ b/FreeFileSync/Source/ui/log_panel.h
@@ -0,0 +1,43 @@
+// *****************************************************************************
+// * 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 LOG_PANEL_3218470817450193
+#define LOG_PANEL_3218470817450193
+
+#include <zen/error_log.h>
+#include "gui_generated.h"
+#include <wx+/grid.h>
+
+
+namespace fff
+{
+class MessageView;
+
+class LogPanel : public LogPanelGenerated
+{
+public:
+ LogPanel(wxWindow* parent) : LogPanelGenerated(parent) { setLog(nullptr); }
+
+ void setLog(const std::shared_ptr<const zen::ErrorLog>& log);
+
+private:
+ MessageView& getDataView();
+ void updateGrid();
+
+ void OnErrors (wxCommandEvent& event) override;
+ void OnWarnings(wxCommandEvent& event) override;
+ void OnInfo (wxCommandEvent& event) override;
+ void onGridButtonEvent(wxKeyEvent& event);
+ void onMsgGridContext (zen::GridClickEvent& event);
+ void onLocalKeyEvent (wxKeyEvent& event);
+
+ void copySelectionToClipboard();
+
+ bool processingKeyEventHandler_ = false;
+};
+}
+
+#endif //LOG_PANEL_3218470817450193
diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp
index e0c934e3..5a78c05e 100755
--- a/FreeFileSync/Source/ui/main_dlg.cpp
+++ b/FreeFileSync/Source/ui/main_dlg.cpp
@@ -370,9 +370,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
const XmlGlobalSettings& globalSettings,
bool startComparison) :
MainDialogGenerated(nullptr),
- globalConfigFilePath_(globalConfigFilePath),
- lastRunConfigPath_(getLastRunConfigPath())
-
+ globalConfigFilePath_(globalConfigFilePath)
{
m_folderPathLeft ->init(folderHistoryLeft_);
m_folderPathRight->init(folderHistoryRight_);
@@ -403,11 +401,20 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
m_bpButtonAddPair ->SetBitmapLabel(getResourceImage(L"item_add"));
m_bpButtonHideSearch ->SetBitmapLabel(getResourceImage(L"close_panel"));
+ m_bpButtonShowLog ->SetBitmapLabel(getResourceImage(L"log_file_small"));
m_textCtrlSearchTxt->SetMinSize(wxSize(fastFromDIP(220), -1));
initViewFilterButtons();
+ //init log panel
+ setRelativeFontSize(*m_staticTextLogStatus, 1.5);
+
+ logPanel_ = new LogPanel(m_panelLog); //pass ownership
+ bSizerLog->Add(logPanel_, 1, wxEXPAND);
+
+ setLastOperationLog(ProcessSummary(), nullptr /*errorLog*/);
+
//we have to use the OS X naming convention by default, because wxMac permanently populates the display menu when the wxMenuItem is created for the first time!
//=> other wx ports are not that badly programmed; therefore revert:
assert(m_menuItemOptions->GetItemLabel() == _("&Preferences") + L"\tCtrl+,"); //"Ctrl" is automatically mapped to command button!
@@ -415,6 +422,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
//---------------- support for dockable gui style --------------------------------
bSizerPanelHolder->Detach(m_panelTopButtons);
+ bSizerPanelHolder->Detach(m_panelLog);
bSizerPanelHolder->Detach(m_panelDirectoryPairs);
bSizerPanelHolder->Detach(m_gridOverview);
bSizerPanelHolder->Detach(m_panelCenter);
@@ -424,38 +432,51 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
auiMgr_.SetManagedWindow(this);
auiMgr_.SetFlags(wxAUI_MGR_DEFAULT | wxAUI_MGR_LIVE_RESIZE);
+ auiMgr_.Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& event)
+ {
+ if (wxAuiPaneInfo* pi = event.GetPane())
+ if (pi->IsMaximized()) //wxBugs: restored size is lost with wxAuiManager::ClosePane()
+ {
+ auiMgr_.RestorePane(*pi); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?)
+ auiMgr_.Update();
+ }
+ });
+
compareStatus_ = std::make_unique<CompareProgressDialog>(*this); //integrate the compare status panel (in hidden state)
//caption required for all panes that can be manipulated by the users => used by context menu
auiMgr_.AddPane(m_panelCenter,
wxAuiPaneInfo().Name(L"CenterPanel").CenterPane().PaneBorder(false));
- {
- //set comparison button label tentatively for m_panelTopButtons to receive final height:
- updateTopButton(*m_buttonCompare, getResourceImage(L"compare"), L"Dummy", false /*makeGrey*/);
- m_panelTopButtons->GetSizer()->SetSizeHints(m_panelTopButtons); //~=Fit() + SetMinSize()
- setBitmapTextLabel(*m_buttonCancel, wxImage(), m_buttonCancel->GetLabel()); //we can't use a wxButton for cancel: it's rendered smaller on OS X than a wxBitmapButton!
- m_buttonCancel->SetMinSize(wxSize(std::max(m_buttonCancel->GetSize().x, fastFromDIP(TOP_BUTTON_OPTIMAL_WIDTH_DIP)),
- std::max(m_buttonCancel->GetSize().y, m_buttonCompare->GetSize().y)));
+ //set comparison button label tentatively for m_panelTopButtons to receive final height:
+ updateTopButton(*m_buttonCompare, getResourceImage(L"compare"), L"Dummy", false /*makeGrey*/);
+ m_panelTopButtons->GetSizer()->SetSizeHints(m_panelTopButtons); //~=Fit() + SetMinSize()
- auiMgr_.AddPane(m_panelTopButtons,
- wxAuiPaneInfo().Name(L"TopPanel").Layer(2).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false).
- PaneBorder(false).Gripper().MinSize(fastFromDIP(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight()));
- //note: min height is calculated incorrectly by wxAuiManager if panes with and without caption are in the same row => use smaller min-size
+ setBitmapTextLabel(*m_buttonCancel, wxImage(), m_buttonCancel->GetLabel()); //we can't use a wxButton for cancel: it's rendered smaller on OS X than a wxBitmapButton!
+ m_buttonCancel->SetMinSize(wxSize(std::max(m_buttonCancel->GetSize().x, fastFromDIP(TOP_BUTTON_OPTIMAL_WIDTH_DIP)),
+ std::max(m_buttonCancel->GetSize().y, m_buttonCompare->GetSize().y)));
- auiMgr_.AddPane(compareStatus_->getAsWindow(),
- wxAuiPaneInfo().Name(L"ProgressPanel").Layer(2).Top().Row(2).CaptionVisible(false).PaneBorder(false).Hide().
- //wxAui does not consider the progress panel's wxRAISED_BORDER and set's too small a panel height! => use correct value from wxWindow::GetSize()
- MinSize(-1, compareStatus_->getAsWindow()->GetSize().GetHeight())); //bonus: minimal height isn't a bad idea anyway
- }
+ auiMgr_.AddPane(m_panelTopButtons,
+ wxAuiPaneInfo().Name(L"TopPanel").Layer(2).Top().Row(1).Caption(_("Main Bar")).CaptionVisible(false).
+ PaneBorder(false).Gripper().MinSize(fastFromDIP(TOP_BUTTON_OPTIMAL_WIDTH_DIP), m_panelTopButtons->GetSize().GetHeight()));
+ //note: min height is calculated incorrectly by wxAuiManager if panes with and without caption are in the same row => use smaller min-size
+
+ auiMgr_.AddPane(compareStatus_->getAsWindow(),
+ wxAuiPaneInfo().Name(L"ProgressPanel").Layer(2).Top().Row(2).CaptionVisible(false).PaneBorder(false).Hide().
+ //wxAui does not consider the progress panel's wxRAISED_BORDER and set's too small a panel height! => use correct value from wxWindow::GetSize()
+ MinSize(-1, compareStatus_->getAsWindow()->GetSize().GetHeight())); //bonus: minimal height isn't a bad idea anyway
auiMgr_.AddPane(m_panelDirectoryPairs,
wxAuiPaneInfo().Name(L"FoldersPanel").Layer(2).Top().Row(3).Caption(_("Folder Pairs")).CaptionVisible(false).PaneBorder(false).Gripper());
auiMgr_.AddPane(m_panelSearch,
- wxAuiPaneInfo().Name(L"SearchPanel").Layer(2).Bottom().Row(2).Caption(_("Find")).CaptionVisible(false).PaneBorder(false).Gripper().
+ wxAuiPaneInfo().Name(L"SearchPanel").Layer(2).Bottom().Row(3).Caption(_("Find")).CaptionVisible(false).PaneBorder(false).Gripper().
MinSize(fastFromDIP(100), m_panelSearch->GetSize().y).Hide());
+ auiMgr_.AddPane(m_panelLog,
+ wxAuiPaneInfo().Name(L"LogPanel").Layer(2).Bottom().Row(2).Caption(_("Log")).MaximizeButton().Hide()
+ .BestSize(fastFromDIP(600), fastFromDIP(300))); //no use setting MinSize(): wxAUI does not update size of hidden panels
+
m_panelViewFilter->GetSizer()->SetSizeHints(m_panelViewFilter); //~=Fit() + SetMinSize()
auiMgr_.AddPane(m_panelViewFilter,
wxAuiPaneInfo().Name(L"ViewFilterPanel").Layer(2).Bottom().Row(1).Caption(_("View Settings")).CaptionVisible(false).
@@ -480,14 +501,14 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
artProvider->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, font.GetPixelSize().GetHeight() + fastFromDIP(2 + 2));
//- fix wxWidgets 3.1.0 insane color scheme
- artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, wxColor(220, 220, 220)); //light grey
- artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_GRADIENT_COLOUR, wxColor(220, 220, 220)); //
- artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxBLACK); //accessibility: always set both foreground AND background colors!
+ artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); //accessibility: always set both foreground AND background colors!
+ artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, wxColor(51, 147, 223)); //medium blue
+ artProvider->SetColor(wxAUI_DOCKART_INACTIVE_CAPTION_GRADIENT_COLOUR, wxColor( 0, 120, 215)); //
//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT) -> better than wxBLACK, but which background to use?
}
auiMgr_.GetPane(m_gridOverview).MinSize(-1, -1); //we successfully tricked wxAuiManager into setting an initial Window size :> incomplete API anyone??
- auiMgr_.Update(); //
+ auiMgr_.Update(); //
defaultPerspective_ = auiMgr_.SavePerspective();
//----------------------------------------------------------------------------------
@@ -524,7 +545,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
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_RIGHT, GridLabelClickEventHandler(MainDialog::onCfgGridLabelContext), nullptr, this);
m_gridCfgHistory->Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEventHandler(MainDialog::onCfgGridLabelLeftClick), nullptr, this);
//----------------------------------------------------------------------------------
@@ -537,6 +558,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
m_bpButtonSaveAs ->SetToolTip(replaceCpy(_("Save &as..."), L"&", L"")); //
m_bpButtonSaveAsBatch->SetToolTip(replaceCpy(_("Save as &batch job..."), L"&", L"")); //
+ m_bpButtonShowLog ->SetToolTip(replaceCpy(_("Show &log"), L"&", L"") + L" (F4)"); //
m_buttonCompare ->SetToolTip(replaceCpy(_("Start &comparison"), L"&", L"") + L" (F5)"); //
m_bpButtonCmpConfig ->SetToolTip(replaceCpy(_("C&omparison settings"), L"&", L"") + L" (F6)"); //
m_bpButtonSyncConfig->SetToolTip(replaceCpy(_("S&ynchronization settings"), L"&", L"") + L" (F8)"); //
@@ -561,6 +583,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
m_menuItemSave ->SetBitmap(getResourceImage(L"file_save_sicon"));
m_menuItemSaveAsBatch->SetBitmap(getResourceImage(L"file_batch_sicon"));
+ m_menuItemShowLog ->SetBitmap(getResourceImage(L"log_file_sicon"));
m_menuItemCompare ->SetBitmap(getResourceImage(L"compare_sicon"));
m_menuItemCompSettings->SetBitmap(getResourceImage(L"cfg_compare_sicon"));
m_menuItemFilter ->SetBitmap(getResourceImage(L"cfg_filter_sicon"));
@@ -620,7 +643,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath,
wxMenuItem* newItem = new wxMenuItem(menu, wxID_ANY, _("&Show details"));
this->Connect(newItem->GetId(), wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MainDialog::OnMenuUpdateAvailable));
menu->Append(newItem); //pass ownership
- m_menubar1->Append(menu, L"\u2605 " + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(globalSettings.gui.lastOnlineVersion)) + L" \u2605"); //"BLACK STAR"
+ m_menubar->Append(menu, L"\u2605 " + replaceCpy(_("FreeFileSync %x is available!"), L"%x", utfTo<std::wstring>(globalSettings.gui.lastOnlineVersion)) + L" \u2605"); //"BLACK STAR"
}
//notify about (logical) application main window => program won't quit, but stay on this dialog
@@ -773,17 +796,13 @@ MainDialog::~MainDialog()
{
writeConfig(getGlobalCfgBeforeExit(), globalConfigFilePath_); //throw FileError
}
- catch (const FileError& e) { firstError = e; }
+ catch (const FileError& e) { if (!firstError) firstError = e; }
try //save "LastRun.ffs_gui"
{
writeConfig(getConfig(), lastRunConfigPath_); //throw FileError
}
- catch (const FileError& e)
- {
- if (!firstError)
- 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)
@@ -900,21 +919,7 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings)
//--------------------------------------------------------------------------------
//load list of configuration files
- std::vector<Zstring> cfgFilePaths;
- std::vector<std::pair<Zstring, time_t>> lastSyncTimes;
- //list is stored with last used files first in XML, however m_gridCfgHistory expects them last!!!
- std::for_each(globalSettings.gui.mainDlg.cfgFileHistory.crbegin(),
- globalSettings.gui.mainDlg.cfgFileHistory.crend(),
- [&](const ConfigFileItem& item)
- {
- cfgFilePaths.push_back(item.filePath);
- lastSyncTimes.emplace_back(item.filePath, item.lastSyncTime);
- });
- cfgFilePaths.push_back(lastRunConfigPath_); //make sure <Last session> is always part of history list (if existing)
-
- cfggrid::getDataView(*m_gridCfgHistory).addCfgFiles(cfgFilePaths);
- cfggrid::getDataView(*m_gridCfgHistory).setLastSyncTime(lastSyncTimes);
- m_gridCfgHistory->Refresh();
+ cfggrid::getDataView(*m_gridCfgHistory).set(globalSettings.gui.mainDlg.cfgFileHistory);
//globalSettings.gui.mainDlg.cfgGridTopRowPos => defer evaluation until later within MainDialog constructor
m_gridCfgHistory->setColumnConfig(convertColAttributes(globalSettings.gui.mainDlg.cfgGridColumnAttribs, getCfgGridDefaultColAttribs()));
@@ -922,7 +927,12 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings)
cfggrid::setSyncOverdueDays(*m_gridCfgHistory, globalSettings.gui.mainDlg.cfgGridSyncOverdueDays);
//m_gridCfgHistory->Refresh(); <- implicit in last call
- cfgHistoryRemoveObsolete(cfgFilePaths); //remove non-existent items (we need this only on startup)
+ //remove non-existent items (we need this only on startup)
+ std::vector<Zstring> cfgFilePaths;
+ for (const ConfigFileItem& item : globalSettings.gui.mainDlg.cfgFileHistory)
+ cfgFilePaths.push_back(item.cfgFilePath);
+
+ cfgHistoryRemoveObsolete(cfgFilePaths);
//--------------------------------------------------------------------------------
//load list of last used folders
@@ -955,6 +965,7 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings)
auiMgr_.GetPane(compareStatus_->getAsWindow()).Hide();
auiMgr_.GetPane(m_panelSearch).Hide(); //no need to show it on startup
+ auiMgr_.GetPane(m_panelLog ).Hide(); //
m_menuItemCheckVersionAuto->Check(updateCheckActive(globalCfg_.gui.lastUpdateCheck));
@@ -984,16 +995,7 @@ XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit()
//--------------------------------------------------------------------------------
//write list of configuration files
- std::map<int, ConfigFileItem, std::greater<>> cfgItemsSorted; //sort by last use; put most recent items *first* (looks better in XML than reverted)
- 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, ConfigFileItem{ cfg->filePath, cfg->lastSyncTime });
- else
- assert(false);
-
- std::vector<ConfigFileItem> cfgHistory;
- for (const auto& item : cfgItemsSorted)
- cfgHistory.emplace_back(item.second);
+ std::vector<ConfigFileItem> cfgHistory = cfggrid::getDataView(*m_gridCfgHistory).get();
if (cfgHistory.size() > globalSettings.gui.mainDlg.cfgHistItemsMax) //erase oldest elements
cfgHistory.resize(globalSettings.gui.mainDlg.cfgHistItemsMax);
@@ -1015,6 +1017,18 @@ XmlGlobalSettings MainDialog::getGlobalCfgBeforeExit()
globalSettings.gui.mainDlg.textSearchRespectCase = m_checkBoxMatchCase->GetValue();
+ wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog);
+ if (logPane.IsShown())
+ {
+ if (logPane.IsMaximized()) //wxBugs: restored size is lost with wxAuiManager::ClosePane()
+ {
+ auiMgr_.RestorePane(logPane); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?)
+ auiMgr_.Update();
+ }
+ }
+ else //wxAUI does not store size of hidden panels => show it (properly!)
+ showLogPanel(true /*show*/);
+
globalSettings.gui.mainDlg.guiPerspectiveLast = auiMgr_.SavePerspective();
//we need to portably retrieve non-iconized, non-maximized size and position (non-portable: GetWindowPlacement())
@@ -1195,20 +1209,30 @@ void MainDialog::copyToAlternateFolder(const std::vector<FileSystemObject*>& sel
auto app = wxTheApp; //fix lambda/wxWigets/VC fuck up
ZEN_ON_SCOPE_EXIT(app->Yield(); enableAllElements()); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks
+ const auto& guiCfg = getConfig();
+ const std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
+
+ StatusHandlerTemporaryPanel statusHandler(*this, startTime,
+ false /*ignoreErrors*/,
+ guiCfg.mainCfg.automaticRetryCount,
+ guiCfg.mainCfg.automaticRetryDelay); //handle status display and error messages
try
{
- StatusHandlerTemporaryPanel statusHandler(*this); //handle status display and error messages
-
fff::copyToAlternateFolder(rowsLeftTmp, rowsRightTmp,
globalCfg_.gui.mainDlg.copyToCfg.lastUsedPath,
globalCfg_.gui.mainDlg.copyToCfg.keepRelPaths,
globalCfg_.gui.mainDlg.copyToCfg.overwriteIfExists,
globalCfg_.warnDlgs,
- statusHandler);
+ statusHandler); //throw AbortProcess
+
//"clearSelection" not needed/desired
}
catch (AbortProcess&) {}
+ StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+
+ setLastOperationLog(r.summary, r.errorLog);
+
//updateGui(); -> not needed
}
@@ -1234,25 +1258,38 @@ void MainDialog::deleteSelectedFiles(const std::vector<FileSystemObject*>& selec
auto app = wxTheApp; //fix lambda/wxWigets/VC fuck up
ZEN_ON_SCOPE_EXIT(app->Yield(); enableAllElements()); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks
+ const auto& guiCfg = getConfig();
+ const std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
+
//wxBusyCursor dummy; -> redundant: progress already shown in status bar!
+
+ StatusHandlerTemporaryPanel statusHandler(*this, startTime,
+ false /*ignoreErrors*/,
+ guiCfg.mainCfg.automaticRetryCount,
+ guiCfg.mainCfg.automaticRetryDelay); //handle status display and error messages
try
{
- StatusHandlerTemporaryPanel statusHandler(*this); //handle status display and error messages
-
deleteFromGridAndHD(rowsLeftTmp, rowsRightTmp,
folderCmp_,
extractDirectionCfg(getConfig().mainCfg),
moveToRecycler,
globalCfg_.warnDlgs.warnRecyclerMissing,
- statusHandler);
+ statusHandler); //throw AbortProcess
+ }
+ catch (AbortProcess&) {}
+
+ StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
- m_gridMainL->clearSelection(ALLOW_GRID_EVENT);
- m_gridMainC->clearSelection(ALLOW_GRID_EVENT);
- m_gridMainR->clearSelection(ALLOW_GRID_EVENT);
+ setLastOperationLog(r.summary, r.errorLog);
- m_gridOverview->clearSelection(ALLOW_GRID_EVENT);
+ if (r.summary.finalStatus != SyncResult::ABORTED)
+ {
+ m_gridMainL->clearSelection(GridEventPolicy::ALLOW);
+ m_gridMainC->clearSelection(GridEventPolicy::ALLOW);
+ m_gridMainR->clearSelection(GridEventPolicy::ALLOW);
+
+ m_gridOverview->clearSelection(GridEventPolicy::ALLOW);
}
- catch (AbortProcess&) {} //do not clear grids, if aborted!
//remove rows that are empty: just a beautification, invalid rows shouldn't cause issues
filegrid::getDataView(*m_gridMainC).removeInvalidRows();
@@ -1375,11 +1412,11 @@ void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool
return openExternalApplication(commandLinePhrase, leftSide, {}, { selectionRight[0] });
}
- auto openFolderInFileBrowser = [&](const AbstractPath& folderPath)
+ auto openFolderInFileBrowser = [this](const AbstractPath& folderPath)
{
try
{
- shellExecute("xdg-open \"" + utfTo<Zstring>(AFS::getDisplayPath(folderPath)) + "\"", ExecutionType::ASYNC); //
+ openWithDefaultApplication(utfTo<Zstring>(AFS::getDisplayPath(folderPath))); //throw FileError
}
catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); }
};
@@ -1438,20 +1475,32 @@ void MainDialog::openExternalApplication(const Zstring& commandLinePhrase, bool
//##################### create temporary files for non-native paths ######################
if (!nonNativeFiles.empty())
{
+ const auto& guiCfg = getConfig();
+ const std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
+
FocusPreserver fp;
disableAllElements(true); //StatusHandlerTemporaryPanel will internally process Window messages, so avoid unexpected callbacks!
auto app = wxTheApp; //fix lambda/wxWigets/VC fuck up
ZEN_ON_SCOPE_EXIT(app->Yield(); enableAllElements()); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks
+ StatusHandlerTemporaryPanel statusHandler(*this, startTime,
+ false /*ignoreErrors*/,
+ guiCfg.mainCfg.automaticRetryCount,
+ guiCfg.mainCfg.automaticRetryDelay); //handle status display and error messages
try
{
- StatusHandlerTemporaryPanel statusHandler(*this); //throw AbortProcess
-
- tempFileBuf_.createTempFiles(nonNativeFiles, statusHandler);
+ tempFileBuf_.createTempFiles(nonNativeFiles, statusHandler); //throw AbortProcess
//"clearSelection" not needed/desired
}
- catch (AbortProcess&) { return; }
+ catch (AbortProcess&) {}
+
+ StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+
+ setLastOperationLog(r.summary, r.errorLog);
+
+ if (r.summary.finalStatus == SyncResult::ABORTED)
+ return;
//updateGui(); -> not needed
}
@@ -1512,57 +1561,38 @@ void MainDialog::setStatusBarFileStatistics(size_t filesOnLeftView,
}
-//void MainDialog::setStatusBarFullText(const wxString& msg)
-//{
-// const bool needLayoutUpdate = !m_staticTextFullStatus->IsShown();
-// //select state
-// bSizerFileStatus->Show(false);
-// m_staticTextFullStatus->Show();
-//
-// //update status information
-// setText(*m_staticTextFullStatus, msg);
-// m_panelStatusBar->Layout();
-//
-// if (needLayoutUpdate)
-// auiMgr.Update(); //fix status bar height (needed on OS X)
-//}
-
-
void MainDialog::flashStatusInformation(const wxString& text)
{
oldStatusMsgs_.push_back(m_staticTextStatusCenter->GetLabel());
m_staticTextStatusCenter->SetLabel(text);
- m_staticTextStatusCenter->SetForegroundColour(wxColor(31, 57, 226)); //highlight_ color: blue
+ m_staticTextStatusCenter->SetForegroundColour(wxColor(31, 57, 226)); //highlight color: blue
m_staticTextStatusCenter->SetFont(m_staticTextStatusCenter->GetFont().Bold());
m_panelStatusBar->Layout();
//if (needLayoutUpdate) auiMgr.Update(); -> not needed here, this is called anyway in updateGui()
- guiQueue_.processAsync([] { std::this_thread::sleep_for(std::chrono::milliseconds(2500)); },
- [this] { this->restoreStatusInformation(); });
-}
-
-
-void MainDialog::restoreStatusInformation()
-{
- if (!oldStatusMsgs_.empty())
+ auto restoreStatusInformation = [this]
{
- wxString oldMsg = oldStatusMsgs_.back();
- oldStatusMsgs_.pop_back();
-
- if (oldStatusMsgs_.empty()) //restore original status text
+ if (!oldStatusMsgs_.empty())
{
- m_staticTextStatusCenter->SetLabel(oldMsg);
- m_staticTextStatusCenter->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //reset color
+ wxString oldMsg = oldStatusMsgs_.back();
+ oldStatusMsgs_.pop_back();
- wxFont fnt = m_staticTextStatusCenter->GetFont();
- fnt.SetWeight(wxFONTWEIGHT_NORMAL);
- m_staticTextStatusCenter->SetFont(fnt);
+ if (oldStatusMsgs_.empty()) //restore original status text
+ {
+ m_staticTextStatusCenter->SetLabel(oldMsg);
+ m_staticTextStatusCenter->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //reset color
+
+ wxFont font = m_staticTextStatusCenter->GetFont();
+ font.SetWeight(wxFONTWEIGHT_NORMAL);
+ m_staticTextStatusCenter->SetFont(font);
- m_panelStatusBar->Layout();
+ m_panelStatusBar->Layout();
+ }
}
- }
+ };
+ guiQueue_.processAsync([] { std::this_thread::sleep_for(std::chrono::milliseconds(2500)); }, restoreStatusInformation);
}
@@ -1583,82 +1613,79 @@ void MainDialog::disableAllElements(bool enableAbort)
localKeyEventsEnabled_ = false;
- for (size_t pos = 0; pos < m_menubar1->GetMenuCount(); ++pos)
- m_menubar1->EnableTop(pos, false);
- m_bpButtonCmpConfig ->Disable();
- m_bpButtonFilter ->Disable();
- m_bpButtonSyncConfig ->Disable();
- m_buttonSync ->Disable();
- m_panelDirectoryPairs->Disable();
- m_splitterMain ->Disable();
- m_gridMainL ->Disable(); //disabled state already covered by m_splitterMain,
- m_gridMainC ->Disable(); //however grid.cpp used IsThisEnabled() for rendering!
- m_gridMainR ->Disable(); //
- m_panelViewFilter ->Disable();
- m_panelConfig ->Disable();
- m_gridOverview ->Disable();
- m_gridCfgHistory ->Disable();
- m_panelSearch ->Disable();
- m_bpButtonCmpContext ->Disable();
- m_bpButtonSyncContext->Disable();
- m_bpButtonFilterContext->Disable();
+ for (size_t pos = 0; pos < m_menubar->GetMenuCount(); ++pos)
+ m_menubar->EnableTop(pos, false);
if (enableAbort)
{
- //show abort button
m_buttonCancel->Enable();
m_buttonCancel->Show();
//if (m_buttonCancel->IsShownOnScreen()) -> needed?
m_buttonCancel->SetFocus();
m_buttonCompare->Disable();
m_buttonCompare->Hide();
-
m_panelTopButtons->Layout();
+
+ m_bpButtonCmpConfig ->Disable();
+ m_bpButtonCmpContext ->Disable();
+ m_bpButtonFilter ->Disable();
+ m_bpButtonFilterContext->Disable();
+ m_bpButtonSyncConfig ->Disable();
+ m_bpButtonSyncContext->Disable();
+ m_buttonSync ->Disable();
}
else
m_panelTopButtons->Disable();
+
+ m_panelDirectoryPairs->Disable();
+ m_gridOverview ->Disable();
+ m_panelCenter ->Disable();
+ m_panelSearch ->Disable();
+ m_panelLog ->Disable();
+ m_panelConfig ->Disable();
+ m_panelViewFilter ->Disable();
+
+ Refresh(); //wxWidgets fails to do this automatically for child items of disabled windows
}
void MainDialog::enableAllElements()
{
- //wxGTK, yet another QOI issue: some stupid bug, keeps moving main dialog to top!!
+ //wxGTK, yet another QOI issue: some stupid bug keeps moving main dialog to top!!
EnableCloseButton(true);
allowMainDialogClose_ = true;
localKeyEventsEnabled_ = true;
- for (size_t pos = 0; pos < m_menubar1->GetMenuCount(); ++pos)
- m_menubar1->EnableTop(pos, true);
- m_bpButtonCmpConfig ->Enable();
- m_bpButtonFilter ->Enable();
- m_bpButtonSyncConfig ->Enable();
- m_buttonSync ->Enable();
- m_panelDirectoryPairs->Enable();
- m_splitterMain ->Enable();
- m_gridMainL ->Enable();
- m_gridMainC ->Enable();
- m_gridMainR ->Enable();
- m_panelViewFilter ->Enable();
- m_panelConfig ->Enable();
- m_gridOverview ->Enable();
- m_gridCfgHistory ->Enable();
- m_panelSearch ->Enable();
- m_bpButtonCmpContext ->Enable();
- m_bpButtonSyncContext->Enable();
- m_bpButtonFilterContext->Enable();
+ for (size_t pos = 0; pos < m_menubar->GetMenuCount(); ++pos)
+ m_menubar->EnableTop(pos, true);
- //show compare button
m_buttonCancel->Disable();
m_buttonCancel->Hide();
m_buttonCompare->Enable();
m_buttonCompare->Show();
+ m_panelTopButtons->Layout();
+
+ m_bpButtonCmpConfig ->Enable();
+ m_bpButtonCmpContext ->Enable();
+ m_bpButtonFilter ->Enable();
+ m_bpButtonFilterContext->Enable();
+ m_bpButtonSyncConfig ->Enable();
+ m_bpButtonSyncContext->Enable();
+ m_buttonSync ->Enable();
m_panelTopButtons->Enable();
- m_panelTopButtons->Layout();
- Refresh(); //at least wxWidgets on OS X fails to do this after enabling:
+ m_panelDirectoryPairs->Enable();
+ m_gridOverview ->Enable();
+ m_panelCenter ->Enable();
+ m_panelSearch ->Enable();
+ m_panelLog ->Enable();
+ m_panelConfig ->Enable();
+ m_panelViewFilter ->Enable();
+
+ Refresh(); //at least wxWidgets on macOS fails to do this after enabling
}
@@ -1960,10 +1987,8 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without
!isComponentOf(focus, m_gridOverview ) &&
!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_panelTopCenter) &&
- !isComponentOf(focus, m_panelTopRight ) &&
- !isComponentOf(focus, m_scrolledWindowFolderPairs) &&
+ !isComponentOf(focus, m_panelLog ) &&
+ !isComponentOf(focus, m_panelDirectoryPairs) && //don't propagate if changing directory fields
m_gridMainL->IsEnabled())
if (wxEvtHandler* evtHandler = m_gridMainL->getMainWin().GetEventHandler())
{
@@ -1976,6 +2001,18 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without
}
}
break;
+
+ case WXK_ESCAPE: //let's do something useful and hide the log panel
+ {
+ const wxWindow* focus = wxWindow::FindFocus();
+ if (!isComponentOf(focus, m_panelSearch) && //search panel also handles ESC!
+ m_panelLog->IsEnabled())
+ {
+ if (auiMgr_.GetPane(m_panelLog).IsShown()) //else: let it "ding"
+ return showLogPanel(false /*show*/);
+ }
+ }
+ break;
}
event.Skip();
@@ -2568,6 +2605,7 @@ void MainDialog::OnContextSetLayout(wxMouseEvent& event)
wxAuiPaneInfo& paneInfo = paneArray[i];
if (!paneInfo.IsShown() &&
paneInfo.window != compareStatus_->getAsWindow() &&
+ paneInfo.window != m_panelLog &&
paneInfo.window != m_panelSearch)
{
if (!addedSeparator)
@@ -3005,15 +3043,10 @@ void MainDialog::OnConfigLoad(wxCommandEvent& event)
void MainDialog::onCfgGridSelection(GridSelectEvent& event)
{
- 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<Zstring> filePaths;
for (size_t row : m_gridCfgHistory->getSelectedRows())
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row))
- filePaths.push_back(cfg->filePath);
+ filePaths.push_back(cfg->cfgItem.cfgFilePath);
else
assert(false);
@@ -3090,7 +3123,7 @@ void MainDialog::deleteSelectedCfgHistoryItems()
std::vector<Zstring> filePaths;
for (size_t row : selectedRows)
if (const ConfigView::Details* cfg = cfggrid::getDataView(*m_gridCfgHistory).getItem(row))
- filePaths.push_back(cfg->filePath);
+ filePaths.push_back(cfg->cfgItem.cfgFilePath);
else
assert(false);
@@ -3104,7 +3137,7 @@ void MainDialog::deleteSelectedCfgHistoryItems()
if (nextRow >= m_gridCfgHistory->getRowCount())
nextRow = m_gridCfgHistory->getRowCount() - 1;
- m_gridCfgHistory->selectRow(nextRow, GridEventPolicy::DENY_GRID_EVENT);
+ m_gridCfgHistory->selectRow(nextRow, GridEventPolicy::DENY);
}
}
}
@@ -3648,16 +3681,17 @@ void MainDialog::OnCompare(wxCommandEvent& event)
ZEN_ON_SCOPE_EXIT(app->Yield(); enableAllElements()); //ui update before enabling buttons again: prevent strange behaviour of delayed button clicks
const auto& guiCfg = getConfig();
+ const std::chrono::system_clock::time_point startTime = std::chrono::system_clock::now();
const std::map<AbstractPath, size_t>& deviceParallelOps = guiCfg.mainCfg.deviceParallelOps;
+ //handle status display and error messages
+ StatusHandlerTemporaryPanel statusHandler(*this, startTime,
+ guiCfg.mainCfg.ignoreErrors,
+ guiCfg.mainCfg.automaticRetryCount,
+ guiCfg.mainCfg.automaticRetryDelay);
try
{
- //handle status display and error messages
- StatusHandlerTemporaryPanel statusHandler(*this);
-
- const std::vector<FolderPairCfg> fpCfgList = extractCompareCfg(guiCfg.mainCfg);
-
//GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization
std::unique_ptr<LockHolder> dirLocks;
@@ -3669,25 +3703,30 @@ void MainDialog::OnCompare(wxCommandEvent& event)
globalCfg_.folderAccessTimeout,
globalCfg_.createLockFile,
dirLocks,
- fpCfgList,
+ extractCompareCfg(guiCfg.mainCfg),
deviceParallelOps,
statusHandler); //throw AbortProcess
}
- catch (AbortProcess&)
- {
- updateGui(); //refresh grid in ANY case! (also on abort)
- return;
- }
+ catch (AbortProcess&) {}
+
+ StatusHandlerTemporaryPanel::Result r = statusHandler.reportFinalStatus(); //noexcept
+ //---------------------------------------------------------------------------
+
+ setLastOperationLog(r.summary, r.errorLog);
+
+ if (r.summary.finalStatus == SyncResult::ABORTED)
+ return updateGui(); //refresh grid in ANY case! (also on abort)
- filegrid::getDataView(*m_gridMainC).setData(folderCmp_); //update view on data
+
+ filegrid::getDataView(*m_gridMainC ).setData(folderCmp_); //update view on data
treegrid::getDataView(*m_gridOverview).setData(folderCmp_); //
updateGui();
- m_gridMainL->clearSelection(ALLOW_GRID_EVENT);
- m_gridMainC->clearSelection(ALLOW_GRID_EVENT);
- m_gridMainR->clearSelection(ALLOW_GRID_EVENT);
+ m_gridMainL->clearSelection(GridEventPolicy::ALLOW);
+ m_gridMainC->clearSelection(GridEventPolicy::ALLOW);
+ m_gridMainR->clearSelection(GridEventPolicy::ALLOW);
- m_gridOverview->clearSelection(ALLOW_GRID_EVENT);
+ m_gridOverview->clearSelection(GridEventPolicy::ALLOW);
//play (optional) sound notification
if (!globalCfg_.soundFileCompareFinished.empty())
@@ -3714,7 +3753,8 @@ void MainDialog::OnCompare(wxCommandEvent& event)
flashStatusInformation(_("All files are in sync"));
//update last sync date for selected cfg files https://freefilesync.org/forum/viewtopic.php?t=4991
- updateLastSyncTimesToNow();
+ if (r.summary.finalStatus == SyncResult::FINISHED_WITH_SUCCESS)
+ updateConfigLastRunStats(std::chrono::system_clock::to_time_t(startTime), r.summary.finalStatus, Zstring() /*logFilePath*/);
}
}
@@ -3840,22 +3880,23 @@ void MainDialog::OnStartSync(wxCommandEvent& event)
}
const std::map<AbstractPath, size_t>& deviceParallelOps = guiCfg.mainCfg.deviceParallelOps;
+
+ std::set<Zstring, LessFilePath> logFilePathsToKeep;
+ for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get())
+ logFilePathsToKeep.insert(item.logFilePath);
+
+ const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
+ const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
bool exitAfterSync = false;
- try
{
- const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now();
-
- //PERF_START;
- const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring();
-
disableAllElements(false); //StatusHandlerFloatingDialog will internally process Window messages, so avoid unexpected callbacks!
ZEN_ON_SCOPE_EXIT(enableAllElements());
+ //run this->enableAllElements() BEFORE "exitAfterSync" buf AFTER StatusHandlerFloatingDialog::reportFinalStatus()
//class handling status updates and error messages
StatusHandlerFloatingDialog statusHandler(this, //throw AbortProcess
syncStartTime,
- globalCfg_.lastSyncsLogFileSizeMax,
guiCfg.mainCfg.ignoreErrors,
guiCfg.mainCfg.automaticRetryCount,
guiCfg.mainCfg.automaticRetryDelay,
@@ -3863,75 +3904,75 @@ void MainDialog::OnStartSync(wxCommandEvent& event)
globalCfg_.soundFileSyncFinished,
guiCfg.mainCfg.postSyncCommand,
guiCfg.mainCfg.postSyncCondition,
- exitAfterSync,
globalCfg_.autoCloseProgressDialog);
+ try
+ {
+ //PERF_START;
- //inform about (important) non-default global settings
- logNonDefaultSettings(globalCfg_, statusHandler); //let's report here rather than before comparison (user might have changed global settings in the meantime!)
+ //inform about (important) non-default global settings;
+ //let's report here rather than before comparison (user might have changed global settings in the meantime!)
+ logNonDefaultSettings(globalCfg_, statusHandler); //throw AbortProcess
- //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog!
+ //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog!
- //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization
- std::unique_ptr<LockHolder> dirLocks;
- if (globalCfg_.createLockFile)
- {
- std::set<Zstring, LessFilePath> availableDirPaths;
- for (auto it = begin(folderCmp_); it != end(folderCmp_); ++it)
+ //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization
+ std::unique_ptr<LockHolder> dirLocks;
+ if (globalCfg_.createLockFile)
{
- if (it->isAvailable<LEFT_SIDE>()) //do NOT check directory existence again!
- if (Opt<Zstring> nativeFolderPath = AFS::getNativeItemPath(it->getAbstractPath<LEFT_SIDE>())) //restrict directory locking to native paths until further
- availableDirPaths.insert(*nativeFolderPath);
+ std::set<Zstring, LessFilePath> availableDirPaths;
+ for (auto it = begin(folderCmp_); it != end(folderCmp_); ++it)
+ {
+ if (it->isAvailable<LEFT_SIDE>()) //do NOT check directory existence again!
+ if (Opt<Zstring> nativeFolderPath = AFS::getNativeItemPath(it->getAbstractPath<LEFT_SIDE>())) //restrict directory locking to native paths until further
+ availableDirPaths.insert(*nativeFolderPath);
- if (it->isAvailable<RIGHT_SIDE>())
- if (Opt<Zstring> nativeFolderPath = AFS::getNativeItemPath(it->getAbstractPath<RIGHT_SIDE>()))
- availableDirPaths.insert(*nativeFolderPath);
+ if (it->isAvailable<RIGHT_SIDE>())
+ if (Opt<Zstring> nativeFolderPath = AFS::getNativeItemPath(it->getAbstractPath<RIGHT_SIDE>()))
+ availableDirPaths.insert(*nativeFolderPath);
+ }
+ dirLocks = std::make_unique<LockHolder>(availableDirPaths, globalCfg_.warnDlgs.warnDirectoryLockFailed, statusHandler); //throw AbortProcess
}
- dirLocks = std::make_unique<LockHolder>(availableDirPaths, globalCfg_.warnDlgs.warnDirectoryLockFailed, statusHandler);
+
+ //START SYNCHRONIZATION
+ synchronize(syncStartTime,
+ globalCfg_.verifyFileCopy,
+ globalCfg_.copyLockedFiles,
+ globalCfg_.copyFilePermissions,
+ globalCfg_.failSafeFileCopy,
+ globalCfg_.runWithBackgroundPriority,
+ globalCfg_.folderAccessTimeout,
+ extractSyncCfg(guiCfg.mainCfg),
+ folderCmp_,
+ deviceParallelOps,
+ globalCfg_.warnDlgs,
+ statusHandler); //throw AbortProcess
}
+ catch (AbortProcess&) {}
- //START SYNCHRONIZATION
- const std::vector<FolderPairSyncCfg> syncProcessCfg = extractSyncCfg(guiCfg.mainCfg);
- if (syncProcessCfg.size() != folderCmp_.size())
- throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__));
- //should never happen: sync button is deactivated if they are not in sync
-
- synchronize(syncStartTime,
- globalCfg_.verifyFileCopy,
- globalCfg_.copyLockedFiles,
- globalCfg_.copyFilePermissions,
- globalCfg_.failSafeFileCopy,
- globalCfg_.runWithBackgroundPriority,
- globalCfg_.folderAccessTimeout,
- syncProcessCfg,
- folderCmp_,
- deviceParallelOps,
- globalCfg_.warnDlgs,
- statusHandler);
-
- //not cancelled? => update last sync date for selected cfg files
- updateLastSyncTimesToNow();
- }
- catch (AbortProcess&) {}
+ StatusHandlerFloatingDialog::Result r = statusHandler.reportFinalStatus(globalCfg_.logfilesMaxAgeDays, logFilePathsToKeep); //noexcept
+ //---------------------------------------------------------------------------
- //remove empty rows: just a beautification, invalid rows shouldn't cause issues
- filegrid::getDataView(*m_gridMainC).removeInvalidRows();
+ setLastOperationLog(r.summary, r.errorLog);
- updateGui();
+ //update last sync stats for the selected cfg files
+ updateConfigLastRunStats(std::chrono::system_clock::to_time_t(syncStartTime), r.summary.finalStatus, r.logFilePath);
- if (exitAfterSync)
- Destroy(); //don't use Close(): we don't want to show the prompt to save current config in OnClose()
-}
+ //remove empty rows: just a beautification, invalid rows shouldn't cause issues
+ filegrid::getDataView(*m_gridMainC).removeInvalidRows();
+ updateGui();
-void MainDialog::updateLastSyncTimesToNow()
-{
- const time_t now = std::time(nullptr);
+ exitAfterSync = r.exitAfterSync;
+ }
+
+ if (exitAfterSync) //don't use Close(): we don't want to show the prompt to save current config in OnClose()
+ Destroy();
+}
- std::vector<std::pair<Zstring, time_t>> lastSyncTimes;
- for (const Zstring& cfgPath : activeConfigFiles_)
- lastSyncTimes.emplace_back(cfgPath, now);
- cfggrid::getDataView(*m_gridCfgHistory).setLastSyncTime(lastSyncTimes);
+void MainDialog::updateConfigLastRunStats(time_t lastRunTime, SyncResult result, const Zstring& logFilePath)
+{
+ cfggrid::getDataView(*m_gridCfgHistory).setLastRunStats(activeConfigFiles_, { lastRunTime, result, logFilePath });
//re-apply selection: sort order changed if sorted by last sync time
cfggrid::addAndSelect(*m_gridCfgHistory, activeConfigFiles_, false /*scrollToSelection*/);
@@ -3939,15 +3980,147 @@ void MainDialog::updateLastSyncTimesToNow()
}
+void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::shared_ptr<const zen::ErrorLog>& errorLog)
+{
+ const wxBitmap statusImage = [&]
+ {
+ switch (summary.finalStatus)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ return getResourceImage(L"status_finished_success");
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return getResourceImage(L"status_finished_warnings");
+ case SyncResult::FINISHED_WITH_ERROR:
+ return getResourceImage(L"status_finished_errors");
+ case SyncResult::ABORTED:
+ return getResourceImage(L"status_aborted");
+ }
+ assert(false);
+ return wxNullBitmap;
+ }();
+
+ const wxBitmap statusOverlayImage = [&]
+ {
+ switch (summary.finalStatus)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ break;
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return getResourceImage(L"msg_warning_sicon");
+ case SyncResult::FINISHED_WITH_ERROR:
+ case SyncResult::ABORTED:
+ return getResourceImage(L"msg_error_sicon");
+ }
+ return wxNullBitmap;
+ }();
+
+ m_bitmapLogStatus->SetBitmap(statusImage);
+ m_staticTextLogStatus->SetLabel(getFinalStatusLabel(summary.finalStatus));
+
+
+ m_staticTextItemsProcessed->SetLabel(formatNumber(summary.statsProcessed.items));
+ m_staticTextBytesProcessed->SetLabel(L"(" + formatFilesizeShort(summary.statsProcessed.bytes) + L")");
+
+ if ((summary.statsTotal.items < 0 && summary.statsTotal.bytes < 0) || //no total items/bytes: e.g. for pure folder comparison
+ summary.statsProcessed == summary.statsTotal) //...if everything was processed successfully
+ m_panelItemsRemaining->Hide();
+ else
+ {
+ m_panelItemsRemaining->Show();
+ m_staticTextItemsRemaining->SetLabel( formatNumber(summary.statsTotal.items - summary.statsProcessed.items));
+ m_staticTextBytesRemaining->SetLabel(L"(" + formatFilesizeShort(summary.statsTotal.bytes - summary.statsProcessed.bytes) + L")");
+ }
+
+ const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(summary.totalTime).count();
+
+ m_staticTextTotalTime->SetLabel(totalTimeSec < 3600 ?
+ wxTimeSpan::Seconds(totalTimeSec).Format( L"%M:%S") :
+ wxTimeSpan::Seconds(totalTimeSec).Format(L"%H:%M:%S"));
+
+ logPanel_->setLog(errorLog);
+ m_panelLog->Layout();
+
+ setImage(*m_bpButtonShowLog, layOver(getResourceImage(L"log_file_small"), statusOverlayImage, wxALIGN_BOTTOM | wxALIGN_RIGHT));
+
+ m_bpButtonShowLog->Show(static_cast<bool>(errorLog));
+}
+
+
+void MainDialog::OnShowLog(wxCommandEvent& event)
+{
+ const bool show = !auiMgr_.GetPane(m_panelLog).IsShown();
+ showLogPanel(show);
+ if (show)
+ logPanel_->SetFocus();
+}
+
+
+void MainDialog::showLogPanel(bool show)
+{
+ wxAuiPaneInfo& logPane = auiMgr_.GetPane(m_panelLog);
+ if (show == logPane.IsShown()) return;
+
+ if (show)
+ {
+ logPane.Show();
+
+ //wxProblem: wxAuiManager::Update will not restore the panel to its old size (which is in logPane.rect)
+ // obviously to avoid overlapping(?) with other panes => HACK to do what it's supposed to do in first place:
+ if (logPane.rect.GetSize() != wxSize())
+ {
+ const bool hasNeighborPanel = [&]
+ {
+ wxAuiPaneInfoArray& paneArray = auiMgr_.GetAllPanes();
+ for (size_t i = 0; i < paneArray.size(); ++i)
+ {
+ const wxAuiPaneInfo& paneInfo = paneArray[i];
+
+ if (&paneInfo != &logPane && paneInfo.IsShown() &&
+ paneInfo.dock_layer == logPane.dock_layer &&
+ paneInfo.dock_direction == logPane.dock_direction &&
+ paneInfo.dock_row == logPane.dock_row)
+ return true;
+ }
+ return false;
+ }();
+
+ if (!hasNeighborPanel) //else: wxAUI for once does the right thing (= adapts to neightbor panels)
+ {
+ const wxSize oldSizeBest = logPane.best_size;
+ const wxSize oldSizeMin = logPane.min_size;
+ const wxSize oldSizeMax = logPane.max_size;
+
+ logPane.min_size = logPane.max_size = logPane.best_size = logPane.rect.GetSize();
+ auiMgr_.Update();
+
+ logPane.best_size = oldSizeBest;
+ logPane.min_size = oldSizeMin;
+ logPane.max_size = oldSizeMax;
+ }
+ }
+ }
+ else
+ {
+ if (logPane.IsMaximized()) //wxBugs: restored size is lost with wxAuiManager::ClosePane()
+ {
+ auiMgr_.RestorePane(logPane); //!= wxAuiPaneInfo::Restore() which does not un-hide other panels (WTF!?)
+ auiMgr_.Update();
+ }
+ logPane.Hide();
+ }
+ auiMgr_.Update();
+}
+
+
void MainDialog::onGridDoubleClickL(GridClickEvent& event)
{
- onGridDoubleClickRim(event.row_, true);
+ onGridDoubleClickRim(event.row_, true /*leftSide*/);
}
void MainDialog::onGridDoubleClickR(GridClickEvent& event)
{
- onGridDoubleClickRim(event.row_, false);
+ onGridDoubleClickRim(event.row_, false /*leftSide*/);
}
@@ -3977,9 +4150,9 @@ void MainDialog::onGridLabelLeftClick(bool onLeft, ColumnTypeRim type)
filegrid::getDataView(*m_gridMainC).sortView(type, itemPathFormat, onLeft, sortAscending);
- m_gridMainL->clearSelection(ALLOW_GRID_EVENT);
- m_gridMainC->clearSelection(ALLOW_GRID_EVENT);
- m_gridMainR->clearSelection(ALLOW_GRID_EVENT);
+ m_gridMainL->clearSelection(GridEventPolicy::ALLOW);
+ m_gridMainC->clearSelection(GridEventPolicy::ALLOW);
+ m_gridMainR->clearSelection(GridEventPolicy::ALLOW);
updateGui(); //refresh gridDataView
}
@@ -4315,7 +4488,7 @@ void MainDialog::startFindNext(bool searchAscending) //F3 or ENTER in m_textCtrl
assert(result.second >= 0);
filegrid::setScrollMaster(*grid);
- grid->setGridCursor(result.second);
+ grid->setGridCursor(result.second, GridEventPolicy::ALLOW);
focusWindowAfterSearch_ = &grid->getMainWin();
@@ -4473,6 +4646,7 @@ void MainDialog::onAddFolderPairKeyEvent(wxKeyEvent& event)
}
}
return;
+
case WXK_PAGEDOWN: //Alt + Page Down
case WXK_NUMPAD_PAGEDOWN:
{
diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h
index 3ca17d97..a91a100c 100755
--- a/FreeFileSync/Source/ui/main_dlg.h
+++ b/FreeFileSync/Source/ui/main_dlg.h
@@ -16,8 +16,11 @@
#include "file_grid.h"
#include "tree_grid.h"
#include "sync_cfg.h"
+#include "log_panel.h"
#include "folder_history_box.h"
+#include "../base/status_handler.h"
#include "../base/algorithm.h"
+#include "../base/return_codes.h"
namespace fff
@@ -128,7 +131,6 @@ private:
//void setStatusBarFullText(const wxString& msg);
void flashStatusInformation(const wxString& msg); //temporarily show different status (only valid for setStatusBarFileStatistics)
- void restoreStatusInformation(); //called automatically after a few seconds
//events
void onGridButtonEventL(wxKeyEvent& event) { onGridButtonEvent(event, *m_gridMainL, true); }
@@ -212,6 +214,7 @@ private:
void OnResizeTopButtonPanel (wxEvent& event);
void OnResizeConfigPanel (wxEvent& event);
void OnResizeViewPanel (wxEvent& event);
+ void OnShowLog (wxCommandEvent& event) override;
void OnCompare (wxCommandEvent& event) override;
void OnStartSync (wxCommandEvent& event) override;
void OnSwapSides (wxCommandEvent& event) override;
@@ -223,7 +226,10 @@ private:
void showConfigDialog(SyncConfigPanel panelToShow, int localPairIndexToShow);
- void updateLastSyncTimesToNow();
+ void updateConfigLastRunStats(time_t lastRunTime, SyncResult result, const Zstring& logFilePath);
+
+ void setLastOperationLog(const ProcessSummary& summary, const std::shared_ptr<const zen::ErrorLog>& errorLog);
+ void showLogPanel(bool show);
void filterExtension(const Zstring& extension, bool include);
void filterShortname(const FileSystemObject& fsObj, bool include);
@@ -296,7 +302,7 @@ private:
XmlGuiConfig lastSavedCfg_; //support for: "Save changed configuration?" dialog
- const Zstring lastRunConfigPath_; //let's not use another static...
+ const Zstring lastRunConfigPath_ = getLastRunConfigPath(); //let's not use another static...
//-------------------------------------
//the prime data structure of this tool *bling*:
@@ -316,6 +322,8 @@ private:
//compare status panel (hidden on start, shown when comparing)
std::unique_ptr<CompareProgressDialog> compareStatus_; //always bound
+ LogPanel* logPanel_ = nullptr;
+
//toggle to display configuration preview instead of comparison result:
//for read access use: m_bpButtonViewTypeSyncAction->isActive()
//when changing value use:
diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp
index 10eba321..7f9850b7 100755
--- a/FreeFileSync/Source/ui/progress_indicator.cpp
+++ b/FreeFileSync/Source/ui/progress_indicator.cpp
@@ -9,33 +9,34 @@
#include <wx/imaglist.h>
#include <wx/wupdlock.h>
#include <wx/sound.h>
-#include <wx/clipbrd.h>
-#include <wx/dcclient.h>
-#include <wx/dataobj.h> //wxTextDataObject
+//#include <wx/dcclient.h>
+//#include <wx/dataobj.h> //wxTextDataObject
+#include <wx/app.h>
#include <zen/basic_math.h>
#include <zen/format_unit.h>
#include <zen/scope_guard.h>
-#include <wx+/grid.h>
+//#include <wx+/grid.h>
#include <wx+/toggle_button.h>
#include <wx+/image_tools.h>
#include <wx+/graph.h>
-#include <wx+/context_menu.h>
+//#include <wx+/context_menu.h>
#include <wx+/no_flicker.h>
#include <wx+/font_size.h>
#include <wx+/std_button_layout.h>
-#include <wx+/popup_dlg.h>
-#include <wx+/image_resources.h>
+//#include <wx+/popup_dlg.h>
+//#include <wx+/image_resources.h>
#include <zen/file_access.h>
#include <zen/thread.h>
#include <zen/perf.h>
-#include <wx+/rtl.h>
+//#include <wx+/rtl.h>
#include <wx+/choice_enum.h>
-#include <wx+/focus.h>
+//#include <wx+/focus.h>
#include "gui_generated.h"
#include "../base/ffs_paths.h"
#include "../base/perf_check.h"
#include "tray_icon.h"
#include "taskbar.h"
+#include "log_panel.h"
#include "app_icon.h"
@@ -53,8 +54,6 @@ const std::chrono::seconds SPEED_ESTIMATE_SAMPLE_INTERVAL(1);
const size_t PROGRESS_GRAPH_SAMPLE_SIZE_MAX = 2500000; //sizeof(single node) worst case ~ 3 * 8 byte ptr + 16 byte key/value = 40 byte
-inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey
-
inline wxColor getColorBytes() { return { 111, 255, 99 }; } //light green
inline wxColor getColorItems() { return { 127, 147, 255 }; } //light blue
@@ -68,40 +67,26 @@ inline wxColor getColorBytesBackgroundRim() { return { 12, 128, 0 }; } //dark
inline wxColor getColorItemsBackgroundRim() { return { 53, 25, 255 }; } //dark blue
-std::wstring getDialogPhaseText(const Statistics* syncStat, bool paused, SyncProgressDialog::SyncResult finalResult)
+std::wstring getDialogPhaseText(const Statistics& syncStat, bool paused)
{
- if (syncStat) //sync running
- {
- if (paused)
- return _("Paused");
+ if (paused)
+ return _("Paused");
- if (syncStat->getAbortStatus())
- return _("Stop requested...");
- else
- switch (syncStat->currentPhase())
- {
- case ProcessCallback::PHASE_NONE:
- return _("Initializing..."); //dialog is shown *before* sync starts, so this text may be visible!
- case ProcessCallback::PHASE_SCANNING:
- return _("Scanning...");
- case ProcessCallback::PHASE_COMPARING_CONTENT:
- return _("Comparing content...");
- case ProcessCallback::PHASE_SYNCHRONIZING:
- return _("Synchronizing...");
- }
+ if (syncStat.getAbortStatus())
+ return _("Stop requested...");
+
+ switch (syncStat.currentPhase())
+ {
+ case ProcessCallback::PHASE_NONE:
+ return _("Initializing..."); //dialog is shown *before* sync starts, so this text may be visible!
+ case ProcessCallback::PHASE_SCANNING:
+ return _("Scanning...");
+ case ProcessCallback::PHASE_COMPARING_CONTENT:
+ return _("Comparing content...");
+ case ProcessCallback::PHASE_SYNCHRONIZING:
+ return _("Synchronizing...");
}
- else //sync finished
- switch (finalResult)
- {
- case SyncProgressDialog::RESULT_ABORTED:
- return _("Stopped");
- case SyncProgressDialog::RESULT_FINISHED_WITH_ERROR:
- return _("Completed with errors");
- case SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS:
- return _("Completed with warnings");
- case SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS:
- return _("Completed successfully");
- }
+ assert(false);
return std::wstring();
}
@@ -164,6 +149,15 @@ public:
bool getOptionIgnoreErrors() const { return ignoreErrors_; }
void setOptionIgnoreErrors(bool ignoreErrors) { ignoreErrors_ = ignoreErrors; updateStaticGui(); }
+ void timerSetStatus(bool active)
+ {
+ if (active)
+ stopWatch_.resume();
+ else
+ stopWatch_.pause();
+ }
+ bool timerIsRunning() const { return !stopWatch_.isPaused(); }
+
private:
//void OnToggleIgnoreErrors(wxCommandEvent& event) override { updateStaticGui(); }
@@ -173,12 +167,12 @@ private:
wxString parentTitleBackup_;
StopWatch stopWatch_;
- std::chrono::nanoseconds binCompStart_{}; //begin of binary comparison phase
+ std::chrono::nanoseconds phaseStart_{}; //begin of current phase
const Statistics* syncStat_ = nullptr; //only bound while sync is running
std::unique_ptr<Taskbar> taskbar_;
- std::unique_ptr<PerfCheck> perf_; //estimate remaining time
+ PerfCheck perf_{ WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC }; //estimate remaining time
std::chrono::nanoseconds timeLastSpeedEstimate_ = std::chrono::seconds(-100); //used for calculating intervals between showing and collecting perf samples
//initial value: just some big number
@@ -235,7 +229,7 @@ void CompareProgressDialog::Impl::init(const Statistics& syncStat, bool ignoreEr
//initialize progress indicator
bSizerProgressGraph->Show(false);
- perf_.reset();
+ perf_ = PerfCheck(WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC);
stopWatch_.restart(); //measure total time
//initially hide status that's relevant for comparing bytewise only
@@ -272,6 +266,11 @@ void CompareProgressDialog::Impl::teardown()
void CompareProgressDialog::Impl::initNewPhase()
{
+ //start new measurement
+ perf_ = PerfCheck(WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC);
+ timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check
+ phaseStart_ = stopWatch_.elapsed();
+
switch (syncStat_->currentPhase())
{
case ProcessCallback::PHASE_NONE:
@@ -281,12 +280,6 @@ void CompareProgressDialog::Impl::initNewPhase()
case ProcessCallback::PHASE_COMPARING_CONTENT:
case ProcessCallback::PHASE_SYNCHRONIZING:
- //start to measure perf
- perf_ = std::make_unique<PerfCheck>(WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC);
- timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check
-
- binCompStart_ = stopWatch_.elapsed();
-
bSizerProgressGraph->Show(true);
//show status for comparing bytewise
@@ -317,6 +310,7 @@ void CompareProgressDialog::Impl::updateStaticGui()
void CompareProgressDialog::Impl::updateProgressGui()
{
+ assert(syncStat_);
if (!syncStat_) //no comparison running!!
return;
@@ -329,19 +323,26 @@ void CompareProgressDialog::Impl::updateProgressGui()
bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary
const std::chrono::nanoseconds timeElapsed = stopWatch_.elapsed();
+ const int itemsCurrent = syncStat_->getStatsCurrent(syncStat_->currentPhase()).items;
+ const int64_t bytesCurrent = syncStat_->getStatsCurrent(syncStat_->currentPhase()).bytes;
+ const int itemsTotal = syncStat_->getStatsTotal (syncStat_->currentPhase()).items;
+ const int64_t bytesTotal = syncStat_->getStatsTotal (syncStat_->currentPhase()).bytes;
+
//status texts
setText(*m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts!
+ warn_static("harmonize phase handling!")
+
//write status information to taskbar, parent title ect.
switch (syncStat_->currentPhase())
{
case ProcessCallback::PHASE_NONE:
case ProcessCallback::PHASE_SCANNING:
{
- const wxString& scannedObjects = formatNumber(syncStat_->getItemsCurrent(ProcessCallback::PHASE_SCANNING));
+ const wxString& scannedObjects = formatNumber(itemsCurrent);
//dialog caption, taskbar
- setTitle(scannedObjects + SPACED_DASH + getDialogPhaseText(syncStat_, false /*paused*/, SyncProgressDialog::RESULT_ABORTED));
+ setTitle(scannedObjects + SPACED_DASH + getDialogPhaseText(*syncStat_, false /*paused*/));
if (taskbar_.get()) //support Windows 7 taskbar
taskbar_->setStatus(Taskbar::STATUS_INDETERMINATE);
@@ -353,18 +354,13 @@ void CompareProgressDialog::Impl::updateProgressGui()
case ProcessCallback::PHASE_SYNCHRONIZING:
case ProcessCallback::PHASE_COMPARING_CONTENT:
{
- const int itemsCurrent = syncStat_->getItemsCurrent(syncStat_->currentPhase());
- const int itemsTotal = syncStat_->getItemsTotal (syncStat_->currentPhase());
- const int64_t bytesCurrent = syncStat_->getBytesCurrent(syncStat_->currentPhase());
- const int64_t bytesTotal = syncStat_->getBytesTotal (syncStat_->currentPhase());
-
//add both bytes + item count, to handle "deletion-only" cases
const double fractionTotal = bytesTotal + itemsTotal == 0 ? 0 : 1.0 * (bytesCurrent + itemsCurrent) / (bytesTotal + itemsTotal);
const double fractionBytes = bytesTotal == 0 ? 0 : 1.0 * bytesCurrent / bytesTotal;
const double fractionItems = itemsTotal == 0 ? 0 : 1.0 * itemsCurrent / itemsTotal;
//dialog caption, taskbar
- setTitle(formatFraction(fractionTotal) + SPACED_DASH + getDialogPhaseText(syncStat_, false /*paused*/, SyncProgressDialog::RESULT_ABORTED));
+ setTitle(formatFraction(fractionTotal) + SPACED_DASH + getDialogPhaseText(*syncStat_, false /*paused*/));
if (taskbar_.get())
{
taskbar_->setProgress(fractionTotal);
@@ -380,26 +376,24 @@ void CompareProgressDialog::Impl::updateProgressGui()
setText(*m_staticTextBytesRemaining, L"(" + formatFilesizeShort(bytesTotal - bytesCurrent) + L")", &layoutChanged);
//remaining time and speed: only visible during binary comparison
- assert(perf_);
- if (perf_)
- if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL)
- {
- timeLastSpeedEstimate_ = timeElapsed;
-
- if (numeric::dist(binCompStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_INTERVAL) //discard stats for first second: probably messy
- perf_->addSample(timeElapsed, itemsCurrent, bytesCurrent);
-
- //current speed -> Win 7 copy uses 1 sec update interval instead
- Opt<std::wstring> bps = perf_->getBytesPerSecond();
- Opt<std::wstring> ips = perf_->getItemsPerSecond();
- m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(bps ? *bps : L"", Graph2D::CORNER_TOP_LEFT));
- m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(ips ? *ips : L"", Graph2D::CORNER_BOTTOM_LEFT));
-
- //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only
- //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter
- Opt<double> remTimeSec = perf_->getRemainingTimeSec(bytesTotal - bytesCurrent);
- setText(*m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : L"-", &layoutChanged);
- }
+ if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL)
+ {
+ timeLastSpeedEstimate_ = timeElapsed;
+
+ if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_INTERVAL) //discard stats for first second: probably messy
+ perf_.addSample(timeElapsed, itemsCurrent, bytesCurrent);
+
+ //current speed -> Win 7 copy uses 1 sec update interval instead
+ Opt<std::wstring> bps = perf_.getBytesPerSecond();
+ Opt<std::wstring> ips = perf_.getItemsPerSecond();
+ m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(bps ? *bps : L"", Graph2D::CORNER_TOP_LEFT));
+ m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(ips ? *ips : L"", Graph2D::CORNER_BOTTOM_LEFT));
+
+ //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only
+ //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter
+ Opt<double> remTimeSec = perf_.getRemainingTimeSec(bytesTotal - bytesCurrent);
+ setText(*m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : L"-", &layoutChanged);
+ }
m_panelProgressGraph->Refresh();
}
@@ -434,530 +428,8 @@ void CompareProgressDialog::initNewPhase() { pimpl_->initNewPhase(); }
void CompareProgressDialog::updateGui() { pimpl_->updateProgressGui(); }
bool CompareProgressDialog::getOptionIgnoreErrors() const { return pimpl_->getOptionIgnoreErrors(); }
void CompareProgressDialog::setOptionIgnoreErrors(bool ignoreErrors) { pimpl_->setOptionIgnoreErrors(ignoreErrors); }
-
-//########################################################################################
-
-namespace
-{
-inline
-wxBitmap getImageButtonPressed(const wchar_t* name)
-{
- return layOver(getResourceImage(L"msg_button_pressed"), getResourceImage(name));
-}
-
-
-inline
-wxBitmap getImageButtonReleased(const wchar_t* name)
-{
- return greyScale(getResourceImage(name)).ConvertToImage();
- //getResourceImage(utfTo<wxString>(name)).ConvertToImage().ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3); //treat all channels equally!
- //brighten(output, 30);
-
- //zen::moveImage(output, 1, 0); //move image right one pixel
- //return output;
-}
-
-
-//a vector-view on ErrorLog considering multi-line messages: prepare consumption by Grid
-class MessageView
-{
-public:
- MessageView(const ErrorLog& log) : log_(log) {}
-
- size_t rowsOnView() const { return viewRef_.size(); }
-
- struct LogEntryView
- {
- time_t time = 0;
- MessageType type = MSG_TYPE_INFO;
- Zstringw messageLine;
- bool firstLine = false; //if LogEntry::message spans multiple rows
- };
-
- Opt<LogEntryView> getEntry(size_t row) const
- {
- if (row < viewRef_.size())
- {
- const Line& line = viewRef_[row];
-
- LogEntryView output;
- output.time = line.logIt_->time;
- output.type = line.logIt_->type;
- output.messageLine = extractLine(line.logIt_->message, line.rowNumber_);
- output.firstLine = line.rowNumber_ == 0; //this is virtually always correct, unless first line of the original message is empty!
- return output;
- }
- return NoValue();
- }
-
- void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, ect. see error_log.h
- {
- viewRef_.clear();
-
- for (auto it = log_.begin(); it != log_.end(); ++it)
- if (it->type & includedTypes)
- {
- static_assert(std::is_same_v<GetCharTypeT<Zstringw>, wchar_t>);
- assert(!startsWith(it->message, L'\n'));
-
- size_t rowNumber = 0;
- bool lastCharNewline = true;
- for (const wchar_t c : it->message)
- if (c == L'\n')
- {
- if (!lastCharNewline) //do not reference empty lines!
- viewRef_.emplace_back(it, rowNumber);
- ++rowNumber;
- lastCharNewline = true;
- }
- else
- lastCharNewline = false;
-
- if (!lastCharNewline)
- viewRef_.emplace_back(it, rowNumber);
- }
- }
-
-private:
- static Zstringw extractLine(const Zstringw& message, size_t textRow)
- {
- auto it1 = message.begin();
- for (;;)
- {
- auto it2 = std::find_if(it1, message.end(), [](wchar_t c) { return c == L'\n'; });
- if (textRow == 0)
- return it1 == message.end() ? Zstringw() : Zstringw(&*it1, it2 - it1); //must not dereference iterator pointing to "end"!
-
- if (it2 == message.end())
- {
- assert(false);
- return Zstringw();
- }
-
- it1 = it2 + 1; //skip newline
- --textRow;
- }
- }
-
- struct Line
- {
- Line(ErrorLog::const_iterator logIt, size_t rowNumber) : logIt_(logIt), rowNumber_(rowNumber) {}
-
- ErrorLog::const_iterator logIt_; //always bound!
- size_t rowNumber_; //LogEntry::message may span multiple rows
- };
-
- std::vector<Line> viewRef_; //partial view on log_
- /* /|\
- | updateView()
- | */
- const ErrorLog log_;
-};
-
-//-----------------------------------------------------------------------------
-
-enum class ColumnTypeMsg
-{
- TIME,
- CATEGORY,
- TEXT,
-};
-
-//Grid data implementation referencing MessageView
-class GridDataMessages : public GridData
-{
-public:
- GridDataMessages(const ErrorLog& log) : msgView_(log) {}
-
- MessageView& getDataView() { return msgView_; }
-
- size_t getRowCount() const override { return msgView_.rowsOnView(); }
-
- std::wstring getValue(size_t row, ColumnType colType) const override
- {
- if (Opt<MessageView::LogEntryView> entry = msgView_.getEntry(row))
- switch (static_cast<ColumnTypeMsg>(colType))
- {
- case ColumnTypeMsg::TIME:
- if (entry->firstLine)
- return formatTime<std::wstring>(FORMAT_TIME, getLocalTime(entry->time));
- break;
-
- case ColumnTypeMsg::CATEGORY:
- if (entry->firstLine)
- switch (entry->type)
- {
- case MSG_TYPE_INFO:
- return _("Info");
- case MSG_TYPE_WARNING:
- return _("Warning");
- case MSG_TYPE_ERROR:
- return _("Error");
- case MSG_TYPE_FATAL_ERROR:
- return _("Serious Error");
- }
- break;
-
- case ColumnTypeMsg::TEXT:
- return copyStringTo<std::wstring>(entry->messageLine);
- }
- 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;
-
- //-------------- draw item separation line -----------------
- {
- wxDCPenChanger dummy2(dc, getColorGridLine());
- const bool drawBottomLine = [&] //don't separate multi-line messages
- {
- if (Opt<MessageView::LogEntryView> nextEntry = msgView_.getEntry(row + 1))
- return nextEntry->firstLine;
- return true;
- }();
-
- if (drawBottomLine)
- {
- dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
- --rectTmp.height;
- }
- }
- //--------------------------------------------------------
-
- if (Opt<MessageView::LogEntryView> entry = msgView_.getEntry(row))
- switch (static_cast<ColumnTypeMsg>(colType))
- {
- case ColumnTypeMsg::TIME:
- drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_CENTER);
- break;
-
- case ColumnTypeMsg::CATEGORY:
- if (entry->firstLine)
- switch (entry->type)
- {
- case MSG_TYPE_INFO:
- drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_info_sicon"), rectTmp, wxALIGN_CENTER);
- break;
- case MSG_TYPE_WARNING:
- drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_warning_sicon"), rectTmp, wxALIGN_CENTER);
- break;
- case MSG_TYPE_ERROR:
- case MSG_TYPE_FATAL_ERROR:
- drawBitmapRtlNoMirror(dc, getResourceImage(L"msg_error_sicon"), rectTmp, wxALIGN_CENTER);
- break;
- }
- break;
-
- case ColumnTypeMsg::TEXT:
- rectTmp.x += getColumnGapLeft();
- rectTmp.width -= getColumnGapLeft();
- drawCellText(dc, rectTmp, getValue(row, colType));
- break;
- }
- }
-
- int getBestSize(wxDC& dc, size_t row, ColumnType colType) override
- {
- // -> synchronize renderCell() <-> getBestSize()
-
- if (msgView_.getEntry(row))
- switch (static_cast<ColumnTypeMsg>(colType))
- {
- case ColumnTypeMsg::TIME:
- return 2 * getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
-
- case ColumnTypeMsg::CATEGORY:
- return getResourceImage(L"msg_info_sicon").GetWidth();
-
- case ColumnTypeMsg::TEXT:
- return getColumnGapLeft() + dc.GetTextExtent(getValue(row, colType)).GetWidth();
- }
- return 0;
- }
-
- static int getColumnTimeDefaultWidth(Grid& grid)
- {
- wxClientDC dc(&grid.getMainWin());
- dc.SetFont(grid.getMainWin().GetFont());
- return 2 * getColumnGapLeft() + dc.GetTextExtent(formatTime<wxString>(FORMAT_TIME)).GetWidth();
- }
-
- static int getColumnCategoryDefaultWidth()
- {
- return getResourceImage(L"msg_info_sicon").GetWidth();
- }
-
- static int getRowDefaultHeight(const Grid& grid)
- {
- return std::max(getResourceImage(L"msg_info_sicon").GetHeight(), grid.getMainWin().GetCharHeight() + fastFromDIP(2)) + 1; //+ some space + bottom border
- }
-
- std::wstring getToolTip(size_t row, ColumnType colType) const override
- {
- switch (static_cast<ColumnTypeMsg>(colType))
- {
- case ColumnTypeMsg::TIME:
- case ColumnTypeMsg::TEXT:
- break;
-
- case ColumnTypeMsg::CATEGORY:
- return getValue(row, colType);
- }
- return std::wstring();
- }
-
- std::wstring getColumnLabel(ColumnType colType) const override { return std::wstring(); }
-
-private:
- MessageView msgView_;
-};
-}
-
-
-class LogPanel : public LogPanelGenerated
-{
-public:
- LogPanel(wxWindow* parent, const ErrorLog& log) : LogPanelGenerated(parent)
- {
- const int errorCount = log.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR);
- const int warningCount = log.getItemCount(MSG_TYPE_WARNING);
- const int infoCount = log.getItemCount(MSG_TYPE_INFO);
-
- auto initButton = [](ToggleButton& btn, const wchar_t* imgName, const wxString& tooltip)
- {
- btn.init(getImageButtonPressed(imgName), getImageButtonReleased(imgName));
- btn.SetToolTip(tooltip);
- };
-
- initButton(*m_bpButtonErrors, L"msg_error", _("Error" ) + L" (" + formatNumber(errorCount) + L")");
- initButton(*m_bpButtonWarnings, L"msg_warning", _("Warning") + L" (" + formatNumber(warningCount) + L")");
- initButton(*m_bpButtonInfo, L"msg_info", _("Info" ) + L" (" + formatNumber(infoCount) + L")");
-
- m_bpButtonErrors ->setActive(true);
- m_bpButtonWarnings->setActive(true);
- m_bpButtonInfo ->setActive(errorCount + warningCount == 0);
-
- m_bpButtonErrors ->Show(errorCount != 0);
- m_bpButtonWarnings->Show(warningCount != 0);
- m_bpButtonInfo ->Show(infoCount != 0);
-
- //init grid, determine default sizes
- const int rowHeight = GridDataMessages::getRowDefaultHeight(*m_gridMessages);
- const int colMsgTimeWidth = GridDataMessages::getColumnTimeDefaultWidth(*m_gridMessages);
- const int colMsgCategoryWidth = GridDataMessages::getColumnCategoryDefaultWidth();
-
- m_gridMessages->setDataProvider(std::make_shared<GridDataMessages>(log));
- m_gridMessages->setColumnLabelHeight(0);
- m_gridMessages->showRowLabel(false);
- m_gridMessages->setRowHeight(rowHeight);
- m_gridMessages->setColumnConfig(
- {
- { static_cast<ColumnType>(ColumnTypeMsg::TIME ), colMsgTimeWidth, 0, true },
- { static_cast<ColumnType>(ColumnTypeMsg::CATEGORY), colMsgCategoryWidth, 0, true },
- { static_cast<ColumnType>(ColumnTypeMsg::TEXT ), -colMsgTimeWidth - colMsgCategoryWidth, 1, true },
- });
-
- //support for CTRL + C
- m_gridMessages->getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(LogPanel::onGridButtonEvent), nullptr, this);
-
- m_gridMessages->Connect(EVENT_GRID_MOUSE_RIGHT_UP, GridClickEventHandler(LogPanel::onMsgGridContext), nullptr, this);
-
- //enable dialog-specific key events
- Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(LogPanel::onLocalKeyEvent), nullptr, this);
-
- updateGrid();
- }
-
-private:
- MessageView& getDataView()
- {
- if (auto* prov = dynamic_cast<GridDataMessages*>(m_gridMessages->getDataProvider()))
- return prov->getDataView();
- throw std::runtime_error(std::string(__FILE__) + "[" + numberTo<std::string>(__LINE__) + "] m_gridMessages was not initialized.");
- }
-
- void OnErrors(wxCommandEvent& event) override
- {
- m_bpButtonErrors->toggle();
- updateGrid();
- }
-
- void OnWarnings(wxCommandEvent& event) override
- {
- m_bpButtonWarnings->toggle();
- updateGrid();
- }
-
- void OnInfo(wxCommandEvent& event) override
- {
- m_bpButtonInfo->toggle();
- updateGrid();
- }
-
- void updateGrid()
- {
- int includedTypes = 0;
- if (m_bpButtonErrors->isActive())
- includedTypes |= MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR;
-
- if (m_bpButtonWarnings->isActive())
- includedTypes |= MSG_TYPE_WARNING;
-
- if (m_bpButtonInfo->isActive())
- includedTypes |= MSG_TYPE_INFO;
-
- getDataView().updateView(includedTypes); //update MVC "model"
- m_gridMessages->Refresh(); //update MVC "view"
- }
-
- void onGridButtonEvent(wxKeyEvent& event)
- {
- int keyCode = event.GetKeyCode();
-
- if (event.ControlDown())
- switch (keyCode)
- {
- //case 'A': -> "select all" is already implemented by Grid!
-
- case 'C':
- case WXK_INSERT: //CTRL + C || CTRL + INS
- copySelectionToClipboard();
- return; // -> swallow event! don't allow default grid commands!
- }
-
- //else
- //switch (keyCode)
- //{
- // case WXK_RETURN:
- // case WXK_NUMPAD_ENTER:
- // return;
- //}
-
- event.Skip(); //unknown keypress: propagate
- }
-
- void onMsgGridContext(GridClickEvent& event)
- {
- const std::vector<size_t> selection = m_gridMessages->getSelectedRows();
-
- const size_t rowCount = [&]() -> size_t
- {
- if (auto prov = m_gridMessages->getDataProvider())
- return prov->getRowCount();
- return 0;
- }();
-
- ContextMenu menu;
- menu.addItem(_("Select all") + L"\tCtrl+A", [this] { m_gridMessages->selectAllRows(ALLOW_GRID_EVENT); }, nullptr, rowCount > 0);
- menu.addSeparator();
-
- menu.addItem(_("Copy") + L"\tCtrl+C", [this] { copySelectionToClipboard(); }, nullptr, !selection.empty());
- menu.popup(*this);
- }
-
- void onLocalKeyEvent(wxKeyEvent& event) //process key events without explicit menu entry :)
- {
- if (processingKeyEventHandler_) //avoid recursion
- {
- event.Skip();
- return;
- }
- processingKeyEventHandler_ = true;
- ZEN_ON_SCOPE_EXIT(processingKeyEventHandler_ = false);
-
-
- const int keyCode = event.GetKeyCode();
-
- if (event.ControlDown())
- switch (keyCode)
- {
- case 'A':
- m_gridMessages->SetFocus();
- m_gridMessages->selectAllRows(ALLOW_GRID_EVENT);
- return; // -> swallow event! don't allow default grid commands!
-
- //case 'C': -> already implemented by "Grid" class
- }
- else
- switch (keyCode)
- {
- //redirect certain (unhandled) keys directly to grid!
- case WXK_UP:
- case WXK_DOWN:
- case WXK_LEFT:
- case WXK_RIGHT:
- case WXK_PAGEUP:
- case WXK_PAGEDOWN:
- case WXK_HOME:
- case WXK_END:
-
- case WXK_NUMPAD_UP:
- case WXK_NUMPAD_DOWN:
- case WXK_NUMPAD_LEFT:
- case WXK_NUMPAD_RIGHT:
- case WXK_NUMPAD_PAGEUP:
- case WXK_NUMPAD_PAGEDOWN:
- case WXK_NUMPAD_HOME:
- case WXK_NUMPAD_END:
- if (!isComponentOf(wxWindow::FindFocus(), m_gridMessages) && //don't propagate keyboard commands if grid is already in focus
- m_gridMessages->IsEnabled())
- if (wxEvtHandler* evtHandler = m_gridMessages->getMainWin().GetEventHandler())
- {
- m_gridMessages->SetFocus();
-
- event.SetEventType(wxEVT_KEY_DOWN); //the grid event handler doesn't expect wxEVT_CHAR_HOOK!
- evtHandler->ProcessEvent(event); //propagating event catched at wxTheApp to child leads to recursion, but we prevented it...
- event.Skip(false); //definitively handled now!
- return;
- }
- break;
- }
-
- event.Skip();
- }
-
- void copySelectionToClipboard()
- {
- try
- {
- Zstringw clipboardString; //guaranteed exponential growth, unlike wxString
-
- if (auto prov = m_gridMessages->getDataProvider())
- {
- std::vector<Grid::ColAttributes> 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::ColAttributes& ca)
- {
- clipboardString += copyStringTo<Zstringw>(prov->getValue(row, ca.type));
- clipboardString += L'\t';
- });
- clipboardString += copyStringTo<Zstringw>(prov->getValue(row, colAttr.back().type));
- clipboardString += L'\n';
- }
- }
-
- //finally write to clipboard
- if (!clipboardString.empty())
- if (wxClipboard::Get()->Open())
- {
- ZEN_ON_SCOPE_EXIT(wxClipboard::Get()->Close());
- wxClipboard::Get()->SetData(new wxTextDataObject(copyStringTo<wxString>(clipboardString))); //ownership passed
- }
- }
- catch (const std::bad_alloc& e)
- {
- showNotificationDialog(nullptr, DialogInfoType::ERROR2, PopupDialogCfg().setMainInstructions(_("Out of memory.") + L" " + utfTo<std::wstring>(e.what())));
- }
- }
-
- bool processingKeyEventHandler_ = false;
-};
+void CompareProgressDialog::timerSetStatus(bool active) { pimpl_->timerSetStatus(active); }
+bool CompareProgressDialog::timerIsRunning() const { return pimpl_->timerIsRunning(); }
//########################################################################################
@@ -1209,7 +681,7 @@ public:
~SyncProgressDialogImpl() override;
//call this in StatusUpdater derived class destructor at the LATEST(!) to prevent access to currentStatusUpdater
- void showSummary(SyncResult resultId, const ErrorLog& log) override;
+ void showSummary(SyncResult finalStatus, const std::shared_ptr<const ErrorLog>& log /*bound!*/) override;
void closeDirectly(bool restoreParentFrame) override;
wxWindow* getWindowIfVisible() override { return this->IsShown() ? this : nullptr; }
@@ -1273,10 +745,8 @@ private:
const std::shared_ptr<int> lifeSign_ = std::make_shared<int>(42); //only bound while instance exists, see pause handling in updateProgressGui()
//wxWindow::Delete(), equals "delete this" on OS X!
- SyncResult finalResult_ = RESULT_ABORTED; //set after sync
-
//remaining time
- std::unique_ptr<PerfCheck> perf_;
+ PerfCheck perf_{ WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC };
std::chrono::nanoseconds timeLastSpeedEstimate_ = std::chrono::seconds(-100); //used for calculating intervals between collecting perf samples
//help calculate total speed
@@ -1513,19 +983,18 @@ void SyncProgressDialogImpl<TopLevelDialog>::initNewPhase()
updateStaticGui(); //evaluates "syncStat_->currentPhase()"
//reset graphs (e.g. after binary comparison)
- curveDataBytesCurrent_->setValue(0, 0, 0);
- curveDataItemsCurrent_->setValue(0, 0, 0);
curveDataBytesTotal_ ->setValue(0, 0);
curveDataItemsTotal_ ->setValue(0, 0);
+ curveDataBytesCurrent_->setValue(0, 0, 0);
+ curveDataItemsCurrent_->setValue(0, 0, 0);
curveDataBytes_ ->clear();
curveDataItems_ ->clear();
notifyProgressChange(); //make sure graphs get initial values
//start new measurement
- perf_ = std::make_unique<PerfCheck>(WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC);
+ perf_ = PerfCheck(WINDOW_REMAINING_TIME, WINDOW_BYTES_PER_SEC);
timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check
-
phaseStart_ = stopWatch_.elapsed();
updateProgressGui(false /*allowYield*/);
@@ -1536,23 +1005,11 @@ template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::notifyProgressChange() //noexcept!
{
if (syncStat_) //sync running
- switch (syncStat_->currentPhase())
- {
- case ProcessCallback::PHASE_NONE:
- //assert(false); -> can happen: e.g. batch run, log file creation failed, throw in BatchStatusHandler constructor
- case ProcessCallback::PHASE_SCANNING:
- break;
- case ProcessCallback::PHASE_COMPARING_CONTENT:
- case ProcessCallback::PHASE_SYNCHRONIZING:
- {
- const int64_t bytesCurrent = syncStat_->getBytesCurrent(syncStat_->currentPhase());
- const int itemsCurrent = syncStat_->getItemsCurrent(syncStat_->currentPhase());
-
- curveDataBytes_->addRecord(stopWatch_.elapsed(), bytesCurrent);
- curveDataItems_->addRecord(stopWatch_.elapsed(), itemsCurrent);
- }
- break;
- }
+ {
+ const ProgressStats stats = syncStat_->getStatsCurrent(syncStat_->currentPhase());
+ curveDataBytes_->addRecord(stopWatch_.elapsed(), stats.bytes);
+ curveDataItems_->addRecord(stopWatch_.elapsed(), stats.items);
+ }
}
@@ -1592,6 +1049,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::setExternalStatus(const wxString& s
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield)
{
+ assert(syncStat_);
if (!syncStat_) //sync not running
return;
@@ -1605,107 +1063,110 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield)
const std::chrono::nanoseconds timeElapsed = stopWatch_.elapsed();
const double timeElapsedDouble = std::chrono::duration<double>(timeElapsed).count();
+ const int itemsCurrent = syncStat_->getStatsCurrent(syncStat_->currentPhase()).items;
+ const int64_t bytesCurrent = syncStat_->getStatsCurrent(syncStat_->currentPhase()).bytes;
+ const int itemsTotal = syncStat_->getStatsTotal (syncStat_->currentPhase()).items;
+ const int64_t bytesTotal = syncStat_->getStatsTotal (syncStat_->currentPhase()).bytes;
+
//sync status text
setText(*pnl_.m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts!
- switch (syncStat_->currentPhase()) //no matter if paused or not
+
+ if (itemsTotal < 0 && bytesTotal < 0)
{
- case ProcessCallback::PHASE_NONE:
- case ProcessCallback::PHASE_SCANNING:
- //dialog caption, taskbar, systray tooltip
- setExternalStatus(getDialogPhaseText(syncStat_, paused_, finalResult_), formatNumber(syncStat_->getItemsCurrent(ProcessCallback::PHASE_SCANNING))); //status text may be "paused"!
+ //dialog caption, taskbar, systray tooltip
+ setExternalStatus(getDialogPhaseText(*syncStat_, paused_), formatNumber(itemsCurrent)); //status text may be "paused"!
- //progress indicators
- if (trayIcon_.get()) trayIcon_->setProgress(1); //100% = regular FFS logo
+ //progress indicators
+ if (trayIcon_.get()) trayIcon_->setProgress(1); //100% = regular FFS logo
+ //taskbar_ already set to STATUS_INDETERMINATE within initNewPhase()
+ }
+ else
+ {
+ //dialog caption, taskbar, systray tooltip
- //ignore graphs: should already have been cleared in initNewPhase()
+ const double fractionTotal = bytesTotal + itemsTotal == 0 ? 0 : 1.0 * (bytesCurrent + itemsCurrent) / (bytesTotal + itemsTotal);
+ //add both data + obj-count, to handle "deletion-only" cases
- //remaining objects and data
- setText(*pnl_.m_staticTextItemsRemaining, L"-", &layoutChanged);
- setText(*pnl_.m_staticTextBytesRemaining, L"", &layoutChanged);
+ setExternalStatus(getDialogPhaseText(*syncStat_, paused_), formatFraction(fractionTotal)); //status text may be "paused"!
- //remaining time and speed
- setText(*pnl_.m_staticTextTimeRemaining, L"-", &layoutChanged);
- pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(wxString(), Graph2D::CORNER_TOP_LEFT));
- pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(wxString(), Graph2D::CORNER_TOP_LEFT));
- break;
+ //progress indicators
+ if (trayIcon_.get()) trayIcon_->setProgress(fractionTotal);
+ if (taskbar_ .get()) taskbar_ ->setProgress(fractionTotal);
- case ProcessCallback::PHASE_COMPARING_CONTENT:
- case ProcessCallback::PHASE_SYNCHRONIZING:
- {
- const int64_t bytesCurrent = syncStat_->getBytesCurrent(syncStat_->currentPhase());
- const int64_t bytesTotal = syncStat_->getBytesTotal (syncStat_->currentPhase());
- const int itemsCurrent = syncStat_->getItemsCurrent(syncStat_->currentPhase());
- const int itemsTotal = syncStat_->getItemsTotal (syncStat_->currentPhase());
+ //----------------------------------------------------------------------------------------------------
+ const double timeTotalSecTentative = bytesCurrent == bytesTotal ? timeElapsedDouble : std::max(curveDataBytesTotal_->getValueX(), timeElapsedDouble);
- //add both data + obj-count, to handle "deletion-only" cases
- const double fractionTotal = bytesTotal + itemsTotal == 0 ? 0 : 1.0 * (bytesCurrent + itemsCurrent) / (bytesTotal + itemsTotal);
- //----------------------------------------------------------------------------------------------------
+ //constant line graph
+ curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, bytesCurrent);
+ curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, itemsCurrent);
- //dialog caption, taskbar, systray tooltip
- setExternalStatus(getDialogPhaseText(syncStat_, paused_, finalResult_), formatFraction(fractionTotal)); //status text may be "paused"!
+ //tentatively update total time, may be improved on below:
+ curveDataBytesTotal_->setValue(timeTotalSecTentative, bytesTotal);
+ curveDataItemsTotal_->setValue(timeTotalSecTentative, itemsTotal);
+ }
- //progress indicators
- if (trayIcon_.get()) trayIcon_->setProgress(fractionTotal);
- if (taskbar_ .get()) taskbar_ ->setProgress(fractionTotal);
+ //even though notifyProgressChange() already set the latest data, let's add another sample to have all curves consider "timeNowMs"
+ //no problem with adding too many records: CurveDataStatistics will remove duplicate entries!
+ curveDataBytes_->addRecord(timeElapsed, bytesCurrent);
+ curveDataItems_->addRecord(timeElapsed, itemsCurrent);
- const double timeTotalSecTentative = bytesTotal == bytesCurrent ? timeElapsedDouble : std::max(curveDataBytesTotal_->getValueX(), timeElapsedDouble);
- //constant line graph
- curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, bytesCurrent);
- curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, itemsCurrent);
+ //remaining objects and data
+ if (itemsTotal < 0 && bytesTotal < 0)
+ {
+ setText(*pnl_.m_staticTextItemsRemaining, L"-", &layoutChanged);
+ setText(*pnl_.m_staticTextBytesRemaining, L"", &layoutChanged);
+ }
+ else
+ {
+ setText(*pnl_.m_staticTextItemsRemaining, formatNumber(itemsTotal - itemsCurrent), &layoutChanged);
+ setText(*pnl_.m_staticTextBytesRemaining, L"(" + formatFilesizeShort(bytesTotal - bytesCurrent) + L")", &layoutChanged);
+ //it's possible data remaining becomes shortly negative if last file synced has ADS data and the bytesTotal was not yet corrected!
+ }
- //tentatively update total time, may be improved on below:
- curveDataBytesTotal_->setValue(timeTotalSecTentative, bytesTotal);
- curveDataItemsTotal_->setValue(timeTotalSecTentative, itemsTotal);
+ //remaining time and speed
+ if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL)
+ {
+ timeLastSpeedEstimate_ = timeElapsed;
- //even though notifyProgressChange() already set the latest data, let's add another sample to have all curves consider "timeNowMs"
- //no problem with adding too many records: CurveDataStatistics will remove duplicate entries!
- curveDataBytes_->addRecord(timeElapsed, bytesCurrent);
- curveDataItems_->addRecord(timeElapsed, itemsCurrent);
+ if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_INTERVAL) //discard stats for first second: probably messy
+ perf_.addSample(timeElapsed, itemsCurrent, bytesCurrent);
- //remaining item and byte count
- setText(*pnl_.m_staticTextItemsRemaining, formatNumber(itemsTotal - itemsCurrent), &layoutChanged);
- setText(*pnl_.m_staticTextBytesRemaining, L"(" + formatFilesizeShort(bytesTotal - bytesCurrent) + L")", &layoutChanged);
- //it's possible data remaining becomes shortly negative if last file synced has ADS data and the bytesTotal was not yet corrected!
-
- //remaining time and speed
- assert(perf_);
- if (perf_)
- if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL)
- {
- timeLastSpeedEstimate_ = timeElapsed;
-
- if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_INTERVAL) //discard stats for first second: probably messy
- perf_->addSample(timeElapsed, itemsCurrent, bytesCurrent);
-
- //current speed -> Win 7 copy uses 1 sec update interval instead
- Opt<std::wstring> bps = perf_->getBytesPerSecond();
- Opt<std::wstring> ips = perf_->getItemsPerSecond();
- pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(bps ? *bps : L"", Graph2D::CORNER_TOP_LEFT));
- pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(ips ? *ips : L"", Graph2D::CORNER_TOP_LEFT));
-
- //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only
- //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter
- Opt<double> remTimeSec = perf_->getRemainingTimeSec(bytesTotal - bytesCurrent);
- setText(*pnl_.m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : L"-", &layoutChanged);
-
- //update estimated total time marker with precision of "10% remaining time" only to avoid needless jumping around:
- const double timeRemainingSec = remTimeSec ? *remTimeSec : 0;
- const double timeTotalSec = timeElapsedDouble + timeRemainingSec;
- if (numeric::dist(curveDataBytesTotal_->getValueX(), timeTotalSec) > 0.1 * timeRemainingSec)
- {
- curveDataBytesTotal_->setValueX(timeTotalSec);
- curveDataItemsTotal_->setValueX(timeTotalSec);
- //don't forget to update these, too:
- curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSec, bytesCurrent);
- curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSec, itemsCurrent);
- }
- }
- break;
+ //current speed -> Win 7 copy uses 1 sec update interval instead
+ Opt<std::wstring> bps = perf_.getBytesPerSecond();
+ Opt<std::wstring> ips = perf_.getItemsPerSecond();
+ pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(bps ? *bps : L"", Graph2D::CORNER_TOP_LEFT));
+ pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(ips ? *ips : L"", Graph2D::CORNER_TOP_LEFT));
+
+ //remaining time
+ if (bytesTotal < 0)
+ {
+ setText(*pnl_.m_staticTextTimeRemaining, L"-", &layoutChanged);
+ //ignore graphs: should already have been cleared in initNewPhase()
+ }
+ else
+ {
+ //remaining time: display with relative error of 10% - based on samples taken every 0.5 sec only
+ //-> call more often than once per second to correctly show last few seconds countdown, but don't call too often to avoid occasional jitter
+ Opt<double> remTimeSec = perf_.getRemainingTimeSec(bytesTotal - bytesCurrent);
+ setText(*pnl_.m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : L"-", &layoutChanged);
+
+ //update estimated total time marker with precision of "10% remaining time" only to avoid needless jumping around:
+ const double timeRemainingSec = remTimeSec ? *remTimeSec : 0;
+ const double timeTotalSec = timeElapsedDouble + timeRemainingSec;
+ if (numeric::dist(curveDataBytesTotal_->getValueX(), timeTotalSec) > 0.1 * timeRemainingSec)
+ {
+ curveDataBytesTotal_->setValueX(timeTotalSec);
+ curveDataItemsTotal_->setValueX(timeTotalSec);
+ //don't forget to update these, too:
+ curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSec, bytesCurrent);
+ curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSec, itemsCurrent);
+ }
}
}
+
pnl_.m_panelGraphBytes->Refresh();
pnl_.m_panelGraphItems->Refresh();
@@ -1763,108 +1224,61 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield)
template <class TopLevelDialog>
-void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syncStat_, paused_, finalResult"
+void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syncStat_, paused_"
{
- const wxString dlgPhaseTxt = getDialogPhaseText(syncStat_, paused_, finalResult_);
-
- pnl_.m_staticTextPhase->SetLabel(dlgPhaseTxt);
- //pnl_.m_bitmapStatus->SetToolTip(dlgPhaseTxt); -> redundant
+ assert(syncStat_);
+ if (!syncStat_)
+ return;
- auto setStatusBitmap = [&](const wchar_t* bmpName)
- {
- pnl_.m_bitmapStatus->SetBitmap(getResourceImage(bmpName));
- pnl_.m_bitmapStatus->Show();
- };
+ pnl_.m_staticTextPhase->SetLabel(getDialogPhaseText(*syncStat_, paused_));
+ //pnl_.m_bitmapStatus->SetToolTip(); -> redundant
- //status bitmap
- if (syncStat_) //sync running
+ const wxBitmap statusImage = [&]
{
if (paused_)
- setStatusBitmap(L"status_pause");
- else
- {
- if (syncStat_->getAbortStatus())
- setStatusBitmap(L"status_aborted");
- else
- switch (syncStat_->currentPhase())
- {
- case ProcessCallback::PHASE_NONE:
- pnl_.m_bitmapStatus->Hide();
- break;
-
- case ProcessCallback::PHASE_SCANNING:
- setStatusBitmap(L"status_scanning");
- break;
-
- case ProcessCallback::PHASE_COMPARING_CONTENT:
- setStatusBitmap(L"status_binary_compare");
- break;
-
- case ProcessCallback::PHASE_SYNCHRONIZING:
- setStatusBitmap(L"status_syncing");
- break;
- }
- }
- }
- else //sync finished
- switch (finalResult_)
- {
- case RESULT_ABORTED:
- setStatusBitmap(L"status_aborted");
- break;
+ return getResourceImage(L"status_pause");
- case RESULT_FINISHED_WITH_ERROR:
- setStatusBitmap(L"status_finished_errors");
- break;
+ if (syncStat_->getAbortStatus())
+ return getResourceImage(L"status_aborted");
- case RESULT_FINISHED_WITH_WARNINGS:
- setStatusBitmap(L"status_finished_warnings");
- break;
-
- case RESULT_FINISHED_WITH_SUCCESS:
- setStatusBitmap(L"status_finished_success");
- break;
+ switch (syncStat_->currentPhase())
+ {
+ case ProcessCallback::PHASE_NONE:
+ case ProcessCallback::PHASE_SCANNING:
+ return getResourceImage(L"status_scanning");
+ case ProcessCallback::PHASE_COMPARING_CONTENT:
+ return getResourceImage(L"status_binary_compare");
+ case ProcessCallback::PHASE_SYNCHRONIZING:
+ return getResourceImage(L"status_syncing");
}
+ assert(false);
+ return wxNullBitmap;
+ }();
+ pnl_.m_bitmapStatus->SetBitmap(statusImage);
+
//show status on Windows 7 taskbar
if (taskbar_.get())
{
- if (syncStat_) //sync running
- {
- if (paused_)
- taskbar_->setStatus(Taskbar::STATUS_PAUSED);
- else
- switch (syncStat_->currentPhase())
- {
- case ProcessCallback::PHASE_NONE:
- case ProcessCallback::PHASE_SCANNING:
- taskbar_->setStatus(Taskbar::STATUS_INDETERMINATE);
- break;
-
- case ProcessCallback::PHASE_COMPARING_CONTENT:
- case ProcessCallback::PHASE_SYNCHRONIZING:
- taskbar_->setStatus(Taskbar::STATUS_NORMAL);
- break;
- }
- }
- else //sync finished
- switch (finalResult_)
+ if (paused_)
+ taskbar_->setStatus(Taskbar::STATUS_PAUSED);
+ else
+ switch (syncStat_->currentPhase())
{
- case RESULT_ABORTED:
- case RESULT_FINISHED_WITH_ERROR:
- taskbar_->setStatus(Taskbar::STATUS_ERROR);
+ case ProcessCallback::PHASE_NONE:
+ case ProcessCallback::PHASE_SCANNING:
+ taskbar_->setStatus(Taskbar::STATUS_INDETERMINATE);
break;
- case RESULT_FINISHED_WITH_WARNINGS:
- case RESULT_FINISHED_WITH_SUCCESS:
+ case ProcessCallback::PHASE_COMPARING_CONTENT:
+ case ProcessCallback::PHASE_SYNCHRONIZING:
taskbar_->setStatus(Taskbar::STATUS_NORMAL);
break;
}
}
//pause button
- if (syncStat_) //sync running
- pnl_.m_buttonPause->SetLabel(paused_ ? _("&Continue") : _("&Pause"));
+ pnl_.m_buttonPause->SetLabel(paused_ ? _("&Continue") : _("&Pause"));
pnl_.bSizerErrorsIgnore->Show(ignoreErrors_);
@@ -1875,7 +1289,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syn
template <class TopLevelDialog>
-void SyncProgressDialogImpl<TopLevelDialog>::closeDirectly(bool restoreParentFrame) //this should really be called: do not call back + schedule deletion
+void SyncProgressDialogImpl<TopLevelDialog>::closeDirectly(bool restoreParentFrame) //this should really be called "do not call back + schedule deletion"
{
assert(syncStat_ && abortCb_);
@@ -1895,8 +1309,9 @@ void SyncProgressDialogImpl<TopLevelDialog>::closeDirectly(bool restoreParentFra
}
+//essential to call this in StatusHandler derived class destructor
template <class TopLevelDialog>
-void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultId, const ErrorLog& log) //essential to call this in StatusHandler derived class destructor
+void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult finalStatus, const std::shared_ptr<const ErrorLog>& log /*bound!*/)
{
assert(syncStat_ && abortCb_);
//at the LATEST(!) to prevent access to currentStatusHandler
@@ -1909,56 +1324,86 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultId, co
//update numbers one last time (as if sync were still running)
notifyProgressChange(); //make one last graph entry at the *current* time
updateProgressGui(false /*allowYield*/);
+ //===================================================================================
- switch (syncStat_->currentPhase()) //no matter if paused or not
- {
- case ProcessCallback::PHASE_NONE:
- case ProcessCallback::PHASE_SCANNING:
- //set overall speed -> not needed
- //items processed -> not needed
- break;
+ const int itemsProcessed = syncStat_->getStatsCurrent(syncStat_->currentPhase()).items;
+ const int64_t bytesProcessed = syncStat_->getStatsCurrent(syncStat_->currentPhase()).bytes;
+ const int itemsTotal = syncStat_->getStatsTotal (syncStat_->currentPhase()).items;
+ const int64_t bytesTotal = syncStat_->getStatsTotal (syncStat_->currentPhase()).bytes;
- case ProcessCallback::PHASE_COMPARING_CONTENT:
- case ProcessCallback::PHASE_SYNCHRONIZING:
- {
- const int itemsCurrent = syncStat_->getItemsCurrent(syncStat_->currentPhase());
- const int itemsTotal = syncStat_->getItemsTotal (syncStat_->currentPhase());
- const int64_t bytesCurrent = syncStat_->getBytesCurrent(syncStat_->currentPhase());
- const int64_t bytesTotal = syncStat_->getBytesTotal (syncStat_->currentPhase());
- assert(bytesCurrent <= bytesTotal);
-
- //set overall speed (instead of current speed)
- const double timeDelta = std::chrono::duration<double>(stopWatch_.elapsed() - phaseStart_).count();
- //we need to consider "time within current phase" not total "timeElapsed"!
-
- const wxString overallBytesPerSecond = numeric::isNull(timeDelta) ? std::wstring() : formatFilesizeShort(numeric::round(bytesCurrent / timeDelta)) + _("/sec");
- const wxString overallItemsPerSecond = numeric::isNull(timeDelta) ? std::wstring() : replaceCpy(_("%x items/sec"), L"%x", formatThreeDigitPrecision(itemsCurrent / timeDelta));
-
- pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(overallBytesPerSecond, Graph2D::CORNER_TOP_LEFT));
- pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(overallItemsPerSecond, Graph2D::CORNER_TOP_LEFT));
-
- //show new element "items processed"
- pnl_.m_panelItemsProcessed->Show();
- pnl_.m_staticTextItemsProcessed->SetLabel(formatNumber(itemsCurrent));
- pnl_.m_staticTextBytesProcessed->SetLabel(L"(" + formatFilesizeShort(bytesCurrent) + L")");
-
- //hide remaining elements...
- if (itemsCurrent == itemsTotal && //...if everything was processed successfully
- bytesCurrent == bytesTotal)
- pnl_.m_panelItemsRemaining->Hide();
- }
- break;
+ //set overall speed (instead of current speed)
+ const double timeDelta = std::chrono::duration<double>(stopWatch_.elapsed() - phaseStart_).count();
+ //we need to consider "time within current phase" not total "timeElapsed"!
+
+ const wxString overallBytesPerSecond = numeric::isNull(timeDelta) ? std::wstring() : formatFilesizeShort(numeric::round(bytesProcessed / timeDelta)) + _("/sec");
+ const wxString overallItemsPerSecond = numeric::isNull(timeDelta) ? std::wstring() : replaceCpy(_("%x items/sec"), L"%x", formatThreeDigitPrecision(itemsProcessed / timeDelta));
+
+ pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(overallBytesPerSecond, Graph2D::CORNER_TOP_LEFT));
+ pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(overallItemsPerSecond, Graph2D::CORNER_TOP_LEFT));
+
+
+ //show new info box "items processed"
+ pnl_.m_panelItemsProcessed->Show();
+ pnl_.m_staticTextItemsProcessed->SetLabel( formatNumber(itemsProcessed));
+ pnl_.m_staticTextBytesProcessed->SetLabel(L"(" + formatFilesizeShort(bytesProcessed) + L")");
+
+
+ if ((itemsTotal < 0 && bytesTotal < 0) || //no total items/bytes: e.g. for pure folder comparison
+ (itemsProcessed == itemsTotal && //
+ bytesProcessed == bytesTotal)) //...if everything was processed successfully
+ pnl_.m_panelItemsRemaining->Hide();
+ else
+ {
+ pnl_.m_staticTextItemsRemaining->SetLabel( formatNumber(itemsTotal - itemsProcessed));
+ pnl_.m_staticTextBytesRemaining->SetLabel(L"(" + formatFilesizeShort(bytesTotal - bytesProcessed) + L")");
}
- //------- change class state -------
- finalResult_ = resultId;
+ //hide remaining time
+ pnl_.m_panelTimeRemaining->Hide();
+ //------- change class state -------
syncStat_ = nullptr;
abortCb_ = nullptr;
//----------------------------------
- updateStaticGui();
- setExternalStatus(getDialogPhaseText(syncStat_, paused_, finalResult_), wxString());
+ const wxBitmap statusImage = [&]
+ {
+ switch (finalStatus)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ return getResourceImage(L"status_finished_success");
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ return getResourceImage(L"status_finished_warnings");
+ case SyncResult::FINISHED_WITH_ERROR:
+ return getResourceImage(L"status_finished_errors");
+ case SyncResult::ABORTED:
+ return getResourceImage(L"status_aborted");
+ }
+ assert(false);
+ return wxNullBitmap;
+ }();
+ pnl_.m_bitmapStatus->SetBitmap(statusImage);
+
+ pnl_.m_staticTextPhase->SetLabel(getFinalStatusLabel(finalStatus));
+ //pnl_.m_bitmapStatus->SetToolTip(); -> redundant
+
+ //show status on Windows 7 taskbar
+ if (taskbar_.get())
+ switch (finalStatus)
+ {
+ case SyncResult::FINISHED_WITH_SUCCESS:
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ taskbar_->setStatus(Taskbar::STATUS_NORMAL);
+ break;
+
+ case SyncResult::FINISHED_WITH_ERROR:
+ case SyncResult::ABORTED:
+ taskbar_->setStatus(Taskbar::STATUS_ERROR);
+ break;
+ }
+ //----------------------------------
+
+ setExternalStatus(getFinalStatusLabel(finalStatus), wxString());
resumeFromSystray(); //if in tray mode...
@@ -1987,17 +1432,14 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultId, co
pnl_.m_staticlineFooter->Hide(); //win: m_notebookResult already has a window frame
- //hide remaining time
- pnl_.m_panelTimeRemaining->Hide();
-
//-------------------------------------------------------------
pnl_.m_notebookResult->SetPadding(wxSize(fastFromDIP(2), 0)); //height cannot be changed
+ //1. re-arrange graph into results listbook
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);
(void)wasDetached;
@@ -2006,12 +1448,12 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultId, co
//2. log file
assert(pnl_.m_notebookResult->GetPageCount() == 1);
- LogPanel* logPanel = new LogPanel(pnl_.m_notebookResult, log); //owned by m_notebookResult
+ LogPanel* logPanel = new LogPanel(pnl_.m_notebookResult); //owned by m_notebookResult
+ logPanel->setLog(log);
pnl_.m_notebookResult->AddPage(logPanel, _("Log"), false /*bSelect*/);
- //bSizerHoldStretch->Insert(0, logPanel, 1, wxEXPAND);
//show log instead of graph if errors occurred! (not required for ignored warnings)
- if (log.getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR) > 0)
+ if (log->getItemCount(MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR) > 0)
pnl_.m_notebookResult->ChangeSelection(pagePosLog);
//fill image list to cope with wxNotebook image setting design desaster...
@@ -2046,13 +1488,13 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultId, co
//pnl.m_panelTimeElapsed->Layout(); -> needed?
//play (optional) sound notification after sync has completed -> only play when waiting on results dialog, seems to be pointless otherwise!
- switch (finalResult_)
+ switch (finalStatus)
{
- case SyncProgressDialog::RESULT_ABORTED:
+ case SyncResult::ABORTED:
break;
- case SyncProgressDialog::RESULT_FINISHED_WITH_ERROR:
- case SyncProgressDialog::RESULT_FINISHED_WITH_WARNINGS:
- case SyncProgressDialog::RESULT_FINISHED_WITH_SUCCESS:
+ case SyncResult::FINISHED_WITH_ERROR:
+ case SyncResult::FINISHED_WITH_WARNINGS:
+ case SyncResult::FINISHED_WITH_SUCCESS:
if (!soundFileSyncComplete_.empty())
{
const Zstring soundFilePath = getResourceDirPf() + soundFileSyncComplete_;
@@ -2061,7 +1503,7 @@ void SyncProgressDialogImpl<TopLevelDialog>::showSummary(SyncResult resultId, co
//warning: this may fail and show a wxWidgets error message! => must not play when running FFS without user interaction!
}
//if (::GetForegroundWindow() != GetHWND())
- // RequestUserAttention(); -> probably too much since task bar already alreay is colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL
+ // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::STATUS_ERROR or STATUS_NORMAL
break;
}
diff --git a/FreeFileSync/Source/ui/progress_indicator.h b/FreeFileSync/Source/ui/progress_indicator.h
index 0a4d0ed8..5c6dae86 100755
--- a/FreeFileSync/Source/ui/progress_indicator.h
+++ b/FreeFileSync/Source/ui/progress_indicator.h
@@ -13,6 +13,7 @@
#include <wx/frame.h>
#include "../base/status_handler.h"
#include "../base/process_xml.h"
+#include "../base/return_codes.h"
namespace fff
@@ -35,6 +36,9 @@ public:
bool getOptionIgnoreErrors() const;
void setOptionIgnoreErrors(bool ignoreError);
+ void timerSetStatus(bool active); //start/stop all internal timers!
+ bool timerIsRunning() const;
+
private:
class Impl;
Impl* const pimpl_;
@@ -53,16 +57,9 @@ enum class PostSyncAction2
struct SyncProgressDialog
{
- enum SyncResult
- {
- RESULT_ABORTED,
- RESULT_FINISHED_WITH_ERROR,
- RESULT_FINISHED_WITH_WARNINGS,
- RESULT_FINISHED_WITH_SUCCESS
- };
//essential to call one of these two methods in StatusUpdater derived class' destructor at the LATEST(!)
//to prevent access to callback to updater (e.g. request abort)
- virtual void showSummary(SyncResult resultId, const zen::ErrorLog& log) = 0; //sync finished, still dialog may live on
+ virtual void showSummary(SyncResult finalStatus, const std::shared_ptr<const zen::ErrorLog>& log /*bound!*/) = 0; //sync finished, still dialog may live on
virtual void closeDirectly(bool restoreParentFrame) = 0; //don't wait for user
//---------------------------------------------------------------------------
@@ -101,13 +98,14 @@ SyncProgressDialog* createProgressDialog(AbortCallback& abortCb,
//DON'T delete the pointer! it will be deleted by the user clicking "OK/Cancel"/wxWindow::Destroy() after showSummary() or closeDirectly()
+template <class ProgressDlg>
class PauseTimers
{
public:
- PauseTimers(SyncProgressDialog& ss) : ss_(ss), timerWasRunning_(ss.timerIsRunning()) { ss_.timerSetStatus(false); }
+ PauseTimers(ProgressDlg& ss) : ss_(ss), timerWasRunning_(ss.timerIsRunning()) { ss_.timerSetStatus(false); }
~PauseTimers() { ss_.timerSetStatus(timerWasRunning_); } //restore previous state: support recursive calls
private:
- SyncProgressDialog& ss_;
+ ProgressDlg& ss_;
const bool timerWasRunning_;
};
}
diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp
index 55ac4f09..630429c0 100755
--- a/FreeFileSync/Source/ui/small_dlgs.cpp
+++ b/FreeFileSync/Source/ui/small_dlgs.cpp
@@ -9,6 +9,7 @@
#include <zen/format_unit.h>
#include <zen/build_info.h>
#include <zen/stl_tools.h>
+#include <zen/shell_execute.h>
#include <wx/wupdlock.h>
#include <wx/filedlg.h>
#include <wx/clipbrd.h>
@@ -29,6 +30,7 @@
#include "../base/help_provider.h"
#include "../base/hard_filter.h"
#include "../base/status_handler.h" //updateUiIsAllowed()
+#include "../base/generate_logfile.h"
#include "../version/version.h"
@@ -544,6 +546,9 @@ private:
void OnAddRow (wxCommandEvent& event) override;
void OnRemoveRow (wxCommandEvent& event) override;
void OnHelpShowExamples(wxHyperlinkEvent& event) override { displayHelpEntry(L"external-applications", this); }
+ void OnShowLogFolder (wxHyperlinkEvent& event) override;
+ void OnToggleLogfilesLimit(wxCommandEvent& event) override { updateGui(); }
+
void onResize(wxSizeEvent& event);
void updateGui();
@@ -576,11 +581,15 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalSettings) :
{
setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOkay).setCancel(m_buttonCancel));
-
//setMainInstructionFont(*m_staticTextHeader);
-
m_gridCustomCommand->SetTabBehaviour(wxGrid::Tab_Leave);
+ m_bitmapLogFile->SetBitmap(getResourceImage(L"log_file_small"));
+ m_spinCtrlLogFilesMaxAge->SetMinSize(wxSize(fastFromDIP(70), -1)); //Hack: set size (why does wxWindow::Size() not work?)
+ m_hyperlinkLogFolder->SetLabel(utfTo<wxString>(getDefaultLogFolderPath()));
+ setRelativeFontSize(*m_hyperlinkLogFolder, 1.2);
+
+ //--------------------------------------------------------------------------------
m_bitmapSettings ->SetBitmap (getResourceImage(L"settings"));
m_bpButtonAddRow ->SetBitmapLabel(getResourceImage(L"item_add"));
m_bpButtonRemoveRow->SetBitmapLabel(getResourceImage(L"item_remove"));
@@ -593,6 +602,10 @@ OptionsDlg::OptionsDlg(wxWindow* parent, XmlGlobalSettings& globalSettings) :
setExtApp(globalSettings.gui.externalApps);
+ m_checkBoxLogFilesMaxAge->SetValue(globalSettings.logfilesMaxAgeDays > 0);
+ m_spinCtrlLogFilesMaxAge->SetValue(globalSettings.logfilesMaxAgeDays > 0 ? globalSettings.logfilesMaxAgeDays : 14);
+ //--------------------------------------------------------------------------------
+
updateGui();
bSizerLockedFiles->Show(false);
@@ -652,6 +665,8 @@ void OptionsDlg::updateGui()
setBitmapTextLabel(*m_buttonResetDialogs, getResourceImage(L"reset_dialogs").ConvertToImage(), haveHiddenDialogs ? _("Show hidden dialogs again") : _("All dialogs shown"));
Layout();
m_buttonResetDialogs->Enable(haveHiddenDialogs);
+
+ m_spinCtrlLogFilesMaxAge->Enable(m_checkBoxLogFilesMaxAge->GetValue());
}
@@ -688,6 +703,8 @@ void OptionsDlg::OnOkay(wxCommandEvent& event)
globalCfgOut_.warnDlgs = warnDlgs_;
globalCfgOut_.autoCloseProgressDialog = autoCloseProgressDialog_;
+ globalCfgOut_.logfilesMaxAgeDays = m_checkBoxLogFilesMaxAge->GetValue() ? m_spinCtrlLogFilesMaxAge->GetValue() : -1;
+
EndModal(ReturnSmallDlg::BUTTON_OKAY);
}
@@ -706,7 +723,7 @@ void OptionsDlg::setExtApp(const std::vector<ExternalApp>& extApps)
{
const int row = it - extApps.begin();
- const std::wstring description = zen::translate(it->description);
+ const std::wstring description = translate(it->description);
if (description != it->description) //remember english description to save in GlobalSettings.xml later rather than hard-code translation
descriptionTransToEng_[description] = it->description;
@@ -765,6 +782,16 @@ void OptionsDlg::OnRemoveRow(wxCommandEvent& event)
}
+void OptionsDlg::OnShowLogFolder(wxHyperlinkEvent& event)
+{
+ try
+ {
+ openWithDefaultApplication(getDefaultLogFolderPath()); //throw FileError
+ }
+ catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::ERROR2, PopupDialogCfg().setDetailInstructions(e.toString())); }
+}
+
+
ReturnSmallDlg::ButtonPressed fff::showOptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg)
{
OptionsDlg dlg(parent, globalCfg);
@@ -860,19 +887,6 @@ void SelectTimespanDlg::OnOkay(wxCommandEvent& event)
timeFromOut_ = from.GetTicks();
timeToOut_ = to .GetTicks();
- /*
- {
- time_t current = zen::to<time_t>(timeFrom_);
- struct tm* tdfewst = ::localtime(&current);
- int budfk = 3;
- }
- {
- time_t current = zen::to<time_t>(timeTo_);
- struct tm* tdfewst = ::localtime(&current);
- int budfk = 3;
- }
- */
-
EndModal(ReturnSmallDlg::BUTTON_OKAY);
}
diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp
index 09054972..7383fbc1 100755
--- a/FreeFileSync/Source/ui/tree_grid.cpp
+++ b/FreeFileSync/Source/ui/tree_grid.cpp
@@ -735,7 +735,7 @@ private:
return dirRight;
else if (dirRight.empty())
return dirLeft;
- return dirLeft + L" \u2013"/*en dash*/ + L"\n" + dirRight;
+ return dirLeft + L" " + EN_DASH + L"\n" + dirRight;
}
break;
@@ -772,18 +772,18 @@ private:
void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) override
{
- wxRect rectInside = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectInside, highlighted);
+ const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
+ wxRect rectRemain = rectInner;
- rectInside.x += getColumnGapLeft();
- rectInside.width -= getColumnGapLeft();
- drawColumnLabelText(dc, rectInside, getColumnLabel(colType));
+ rectRemain.x += getColumnGapLeft();
+ rectRemain.width -= getColumnGapLeft();
+ drawColumnLabelText(dc, rectRemain, getColumnLabel(colType));
auto sortInfo = treeDataView_.getSortDirection();
if (colType == static_cast<ColumnType>(sortInfo.first))
{
const wxBitmap& marker = getResourceImage(sortInfo.second ? L"sort_ascending" : L"sort_descending");
- drawBitmapRtlNoMirror(dc, marker, rectInside, wxALIGN_CENTER_HORIZONTAL);
+ drawBitmapRtlNoMirror(dc, marker, rectInner, wxALIGN_CENTER_HORIZONTAL);
}
}
@@ -1074,7 +1074,7 @@ private:
const int parentRow = treeDataView_.getParent(row);
if (parentRow >= 0)
- grid_.setGridCursor(parentRow);
+ grid_.setGridCursor(parentRow, GridEventPolicy::ALLOW);
break;
}
return; //swallow event
@@ -1085,7 +1085,7 @@ private:
switch (treeDataView_.getStatus(row))
{
case TreeView::STATUS_EXPANDED:
- grid_.setGridCursor(std::min(rowCount - 1, row + 1));
+ grid_.setGridCursor(std::min(rowCount - 1, row + 1), GridEventPolicy::ALLOW);
break;
case TreeView::STATUS_REDUCED:
return expandNode(row);
@@ -1161,7 +1161,7 @@ private:
sortAscending = !sortInfo.second;
treeDataView_.setSortDirection(colTypeTree, sortAscending);
- grid_.clearSelection(ALLOW_GRID_EVENT);
+ grid_.clearSelection(GridEventPolicy::ALLOW);
grid_.Refresh();
}
@@ -1169,7 +1169,7 @@ private:
{
treeDataView_.expandNode(row);
grid_.Refresh(); //implicitly clears selection (changed row count after expand)
- grid_.setGridCursor(row);
+ grid_.setGridCursor(row, GridEventPolicy::ALLOW);
//grid_.autoSizeColumns(); -> doesn't look as good as expected
}
@@ -1177,7 +1177,7 @@ private:
{
treeDataView_.reduceNode(row);
grid_.Refresh();
- grid_.setGridCursor(row);
+ grid_.setGridCursor(row, GridEventPolicy::ALLOW);
}
TreeView treeDataView_;
diff --git a/FreeFileSync/Source/ui/version_check_impl.h b/FreeFileSync/Source/ui/version_check_impl.h
index ccf2c387..64030de3 100755
--- a/FreeFileSync/Source/ui/version_check_impl.h
+++ b/FreeFileSync/Source/ui/version_check_impl.h
@@ -18,7 +18,7 @@ inline
time_t getVersionCheckInactiveId()
{
//use current version to calculate a changing number for the inactive state near UTC begin, in order to always check for updates after installing a new version
- //=> convert version into 11-based *unique* number (this breaks lexicographical version ordering, but that's irrelevant!)
+ //=> interpret version as 11-based *unique* number (this breaks lexicographical version ordering, but that's irrelevant!)
int id = 0;
const char* first = ffsVersion;
const char* last = first + zen::strLength(ffsVersion);
diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h
index adcaf3c8..63deee3e 100755
--- a/FreeFileSync/Source/version/version.h
+++ b/FreeFileSync/Source/version/version.h
@@ -3,7 +3,7 @@
namespace fff
{
-const char ffsVersion[] = "10.2"; //internal linkage!
+const char ffsVersion[] = "10.3"; //internal linkage!
const char FFS_VERSION_SEPARATOR = '.';
}
diff --git a/License.txt b/License.txt
index 55a9c091..db3fc4ce 100755
--- a/License.txt
+++ b/License.txt
@@ -1,811 +1,811 @@
-A. GNU GENERAL PUBLIC LICENSE
-B. OpenSSL and SSLeay License
-C. cURL License
-D. libssh2 License
-
-A. GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-
-B. OpenSSL and SSLeay License
-
-OpenSSL License
-
-====================================================================
-Copyright (c) 1998-2018 The OpenSSL Project. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
-1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in
- the documentation and/or other materials provided with the
- distribution.
-
-3. All advertising materials mentioning features or use of this
- software must display the following acknowledgment:
- "This product includes software developed by the OpenSSL Project
- for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
-
-4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
- endorse or promote products derived from this software without
- prior written permission. For written permission, please contact
- openssl-core@openssl.org.
-
-5. Products derived from this software may not be called "OpenSSL"
- nor may "OpenSSL" appear in their names without prior written
- permission of the OpenSSL Project.
-
-6. Redistributions of any form whatsoever must retain the following
- acknowledgment:
- "This product includes software developed by the OpenSSL Project
- for use in the OpenSSL Toolkit (http://www.openssl.org/)"
-
-THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
-EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
-ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
-NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-OF THE POSSIBILITY OF SUCH DAMAGE.
-====================================================================
-
-This product includes cryptographic software written by Eric Young
-(eay@cryptsoft.com). This product includes software written by Tim
-Hudson (tjh@cryptsoft.com).
-
-Original SSLeay License
-
-Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
-All rights reserved.
-
-This package is an SSL implementation written
-by Eric Young (eay@cryptsoft.com).
-The implementation was written so as to conform with Netscapes SSL.
-
-This library is free for commercial and non-commercial use as long as
-the following conditions are aheared to. The following conditions
-apply to all code found in this distribution, be it the RC4, RSA,
-lhash, DES, etc., code; not just the SSL code. The SSL documentation
-included with this distribution is covered by the same copyright terms
-except that the holder is Tim Hudson (tjh@cryptsoft.com).
-
-Copyright remains Eric Young's, and as such any Copyright notices in
-the code are not to be removed.
-If this package is used in a product, Eric Young should be given attribution
-as the author of the parts of the library used.
-This can be in the form of a textual message at program startup or
-in documentation (online or textual) provided with the package.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-1. Redistributions of source code must retain the copyright
- notice, this list of conditions and the following disclaimer.
-2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-3. All advertising materials mentioning features or use of this software
- must display the following acknowledgement:
- "This product includes cryptographic software written by
- Eric Young (eay@cryptsoft.com)"
- The word 'cryptographic' can be left out if the rouines from the library
- being used are not cryptographic related :-).
-4. If you include any Windows specific code (or a derivative thereof) from
- the apps directory (application code) you must include an acknowledgement:
- "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
-
-THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGE.
-
-The licence and distribution terms for any publically available version or
-derivative of this code cannot be changed. i.e. this code cannot simply be
-copied and put under another distribution licence
-[including the GNU Public Licence.]
-
-
-C. cURL License
-
-COPYRIGHT AND PERMISSION NOTICE
-
-Copyright (c) 1996 - 2018, Daniel Stenberg, <daniel@haxx.se>, and many
-contributors, see the THANKS file.
-
-All rights reserved.
-
-Permission to use, copy, modify, and distribute this software for any purpose
-with or without fee is hereby granted, provided that the above copyright
-notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN
-NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
-OR OTHER DEALINGS IN THE SOFTWARE.
-
-Except as contained in this notice, the name of a copyright holder shall not
-be used in advertising or otherwise to promote the sale, use or other dealings
-in this Software without prior written authorization of the copyright holder.
-
-
-D. libssh2 License
-
-Copyright (c) 2004-2007 Sara Golemon <sarag@libssh2.org>
-Copyright (c) 2005,2006 Mikhail Gusarov <dottedmag@dottedmag.net>
-Copyright (c) 2006-2007 The Written Word, Inc.
-Copyright (c) 2007 Eli Fant <elifantu@mail.ru>
-Copyright (c) 2009-2014 Daniel Stenberg
-Copyright (C) 2008, 2009 Simon Josefsson
-All rights reserved.
-
-Redistribution and use in source and binary forms,
-with or without modification, are permitted provided
-that the following conditions are met:
-
- Redistributions of source code must retain the above
- copyright notice, this list of conditions and the
- following disclaimer.
-
- Redistributions in binary form must reproduce the above
- copyright notice, this list of conditions and the following
- disclaimer in the documentation and/or other materials
- provided with the distribution.
-
- Neither the name of the copyright holder nor the names
- of any other contributors may be used to endorse or
- promote products derived from this software without
- specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
-CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
-CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
-WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
-OF SUCH DAMAGE.
-
- END OF TERMS AND CONDITIONS
+A. GNU GENERAL PUBLIC LICENSE
+B. OpenSSL and SSLeay License
+C. cURL License
+D. libssh2 License
+
+A. GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+
+B. OpenSSL and SSLeay License
+
+OpenSSL License
+
+====================================================================
+Copyright (c) 1998-2018 The OpenSSL Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. All advertising materials mentioning features or use of this
+ software must display the following acknowledgment:
+ "This product includes software developed by the OpenSSL Project
+ for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
+
+4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
+ endorse or promote products derived from this software without
+ prior written permission. For written permission, please contact
+ openssl-core@openssl.org.
+
+5. Products derived from this software may not be called "OpenSSL"
+ nor may "OpenSSL" appear in their names without prior written
+ permission of the OpenSSL Project.
+
+6. Redistributions of any form whatsoever must retain the following
+ acknowledgment:
+ "This product includes software developed by the OpenSSL Project
+ for use in the OpenSSL Toolkit (http://www.openssl.org/)"
+
+THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
+EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR
+ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
+====================================================================
+
+This product includes cryptographic software written by Eric Young
+(eay@cryptsoft.com). This product includes software written by Tim
+Hudson (tjh@cryptsoft.com).
+
+Original SSLeay License
+
+Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
+All rights reserved.
+
+This package is an SSL implementation written
+by Eric Young (eay@cryptsoft.com).
+The implementation was written so as to conform with Netscapes SSL.
+
+This library is free for commercial and non-commercial use as long as
+the following conditions are aheared to. The following conditions
+apply to all code found in this distribution, be it the RC4, RSA,
+lhash, DES, etc., code; not just the SSL code. The SSL documentation
+included with this distribution is covered by the same copyright terms
+except that the holder is Tim Hudson (tjh@cryptsoft.com).
+
+Copyright remains Eric Young's, and as such any Copyright notices in
+the code are not to be removed.
+If this package is used in a product, Eric Young should be given attribution
+as the author of the parts of the library used.
+This can be in the form of a textual message at program startup or
+in documentation (online or textual) provided with the package.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+ "This product includes cryptographic software written by
+ Eric Young (eay@cryptsoft.com)"
+ The word 'cryptographic' can be left out if the rouines from the library
+ being used are not cryptographic related :-).
+4. If you include any Windows specific code (or a derivative thereof) from
+ the apps directory (application code) you must include an acknowledgement:
+ "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
+
+THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
+The licence and distribution terms for any publically available version or
+derivative of this code cannot be changed. i.e. this code cannot simply be
+copied and put under another distribution licence
+[including the GNU Public Licence.]
+
+
+C. cURL License
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1996 - 2018, Daniel Stenberg, <daniel@haxx.se>, and many
+contributors, see the THANKS file.
+
+All rights reserved.
+
+Permission to use, copy, modify, and distribute this software for any purpose
+with or without fee is hereby granted, provided that the above copyright
+notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall not
+be used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization of the copyright holder.
+
+
+D. libssh2 License
+
+Copyright (c) 2004-2007 Sara Golemon <sarag@libssh2.org>
+Copyright (c) 2005,2006 Mikhail Gusarov <dottedmag@dottedmag.net>
+Copyright (c) 2006-2007 The Written Word, Inc.
+Copyright (c) 2007 Eli Fant <elifantu@mail.ru>
+Copyright (c) 2009-2014 Daniel Stenberg
+Copyright (C) 2008, 2009 Simon Josefsson
+All rights reserved.
+
+Redistribution and use in source and binary forms,
+with or without modification, are permitted provided
+that the following conditions are met:
+
+ Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+
+ Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+ Neither the name of the copyright holder nor the names
+ of any other contributors may be used to endorse or
+ promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGE.
+
+ END OF TERMS AND CONDITIONS
diff --git a/wx+/async_task.h b/wx+/async_task.h
index 8c2602ca..d1f30fec 100755
--- a/wx+/async_task.h
+++ b/wx+/async_task.h
@@ -70,7 +70,7 @@ public:
void add(Fun&& evalAsync, Fun2&& evalOnGui)
{
using ResultType = decltype(evalAsync());
- tasks_.push_back(std::make_unique<ConcreteTask<ResultType, Fun2>>(zen::runAsync(std::forward<Fun>(evalAsync)), std::forward<Fun2>(evalOnGui)));
+ tasks_.push_back(std::make_unique<ConcreteTask<ResultType, std::decay_t<Fun2>>>(zen::runAsync(std::forward<Fun>(evalAsync)), std::forward<Fun2>(evalOnGui)));
}
//equivalent to "evalOnGui(evalAsync())"
// -> evalAsync: the usual thread-safety requirements apply!
diff --git a/wx+/graph.h b/wx+/graph.h
index bf0e7a70..e0c2c12b 100755
--- a/wx+/graph.h
+++ b/wx+/graph.h
@@ -342,7 +342,7 @@ private:
using CurveList = std::vector<std::pair<std::shared_ptr<CurveData>, CurveAttributes>>;
CurveList curves_;
- //perf!!! generating the font is *very* expensive! don't do this repeatedly in Graph2D::render()!
+ //perf!!! generating the font is *very* expensive! => buffer for Graph2D::render()!
const wxFont labelFont_ { wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, L"Arial" };
};
}
diff --git a/wx+/grid.cpp b/wx+/grid.cpp
index adc0615c..52ee6e6b 100755
--- a/wx+/grid.cpp
+++ b/wx+/grid.cpp
@@ -54,6 +54,37 @@ const int COLUMN_FILL_GAP_TOLERANCE_DIP = 10; //enlarge column to fill full wid
const int COLUMN_MOVE_MARKER_WIDTH_DIP = 3;
const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time?
+
+/*
+IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:
+
+void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
+child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
+The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
+and *draw* with it on the background while the grid refreshes as disabled incrementally!
+
+=> Don't use IsEnabled() since it considers the top level window, but a disabled top-level should NOT
+lead to child elements being rendered disabled!
+
+=> IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.
+
+The perfect solution would be a bool renderAsEnabled() { return "IsEnabled() but ignore effects of showing a modal dialog"; }
+
+However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
+(Similar problem on Win 7: e.g. directly click sync button without comparing first)
+
+=> 2018-07-30: roll our own:
+*/
+bool renderAsEnabled(wxWindow& win)
+{
+ if (win.IsTopLevel())
+ return true;
+
+ if (wxWindow* parent = win.GetParent())
+ return win.IsThisEnabled() && renderAsEnabled(*parent);
+ else
+ return win.IsThisEnabled();
+}
}
//----------------------------------------------------------------------------------------------------------------
@@ -184,40 +215,35 @@ wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring&
void GridData::renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted)
{
- wxRect rectTmp = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectTmp, highlighted);
+ wxRect rectRemain = drawColumnLabelBackground(dc, rect, highlighted);
- rectTmp.x += getColumnGapLeft();
- rectTmp.width -= getColumnGapLeft();
- drawColumnLabelText(dc, rectTmp, getColumnLabel(colType));
+ rectRemain.x += getColumnGapLeft();
+ rectRemain.width -= getColumnGapLeft();
+ drawColumnLabelText(dc, rectRemain, getColumnLabel(colType));
}
-wxRect GridData::drawColumnLabelBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
+wxRect GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted)
{
- //draw white line
+ //left border
{
wxDCPenChanger dummy(dc, *wxWHITE_PEN);
dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
}
-
- //draw border (with gradient)
+ //bottom, right border
{
wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
}
- return wxRect(rect.x + 1, rect.y, rect.width - 2, rect.height - 1); //we really don't like wxRect::Deflate, do we?
-}
-
-
-void GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted)
-{
+ wxRect rectInside(rect.x + 1, rect.y, rect.width - 2, rect.height - 1);
if (highlighted)
- dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH);
+ dc.GradientFillLinear(rectInside, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH);
else //regular background gradient
- dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); //clear overlapping cells
+ dc.GradientFillLinear(rectInside, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH);
+
+ return wxRect(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2); //we really don't like wxRect::Deflate, do we?
}
@@ -471,28 +497,7 @@ private:
void render(wxDC& dc, const wxRect& rect) override
{
- /*
- IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:
-
- void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
- child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
- The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
- and *draw* with it on the background while the grid refreshes as disabled incrementally!
-
- => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention,
- but wrong when not refreshing child-windows: the control designer decides how his control should be rendered!
-
- => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.
-
- The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; }
-
- However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
- (Similar problem on Win 7: e.g. directly click sync button without comparing first)
- */
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+ clearArea(dc, rect, wxSystemSettings::GetColour(/*!renderAsEnabled(*this) ? wxSYS_COLOUR_BTNFACE :*/wxSYS_COLOUR_WINDOW));
wxFont labelFont = GetFont();
//labelFont.SetWeight(wxFONTWEIGHT_BOLD);
@@ -609,10 +614,7 @@ private:
void render(wxDC& dc, const wxRect& rect) override
{
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+ clearArea(dc, rect, wxSystemSettings::GetColour(/*!renderAsEnabled(*this) ? wxSYS_COLOUR_BTNFACE :*/wxSYS_COLOUR_WINDOW));
//coordinate with "colLabelHeight" in Grid constructor:
wxFont labelFont = GetFont();
@@ -750,7 +752,7 @@ private:
const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error
if (bestWidth >= 0)
{
- refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT);
+ refParent().setColumnWidth(bestWidth, action->col, GridEventPolicy::ALLOW);
refParent().Refresh(); //refresh main grid as well!
}
}
@@ -765,12 +767,12 @@ private:
const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX();
//set width tentatively
- refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT);
+ refParent().setColumnWidth(newWidth, col, GridEventPolicy::ALLOW);
//check if there's a small gap after last column, if yes, fill it
const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth());
if (std::abs(gapWidth) < fastFromDIP(COLUMN_FILL_GAP_TOLERANCE_DIP))
- refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT);
+ refParent().setColumnWidth(newWidth + gapWidth, col, GridEventPolicy::ALLOW);
refParent().Refresh(); //refresh columns on main grid as well!
}
@@ -822,6 +824,7 @@ private:
void onLeaveWindow(wxMouseEvent& event) override
{
highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize!
+
Refresh();
event.Skip();
}
@@ -881,10 +884,7 @@ public:
private:
void render(wxDC& dc, const wxRect& rect) override
{
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+ clearArea(dc, rect, wxSystemSettings::GetColour(/*!renderAsEnabled(*this) ? wxSYS_COLOUR_BTNFACE :*/wxSYS_COLOUR_WINDOW));
dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize()
@@ -913,7 +913,7 @@ private:
{
const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight));
RecursiveDcClipper dummy3(dc, rowRect);
- prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row));
+ prov->renderRowBackgound(dc, rowRect, row, renderAsEnabled(*this), drawAsSelected(row));
}
//draw single cells, column by column
@@ -927,7 +927,7 @@ private:
{
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, renderAsEnabled(*this), drawAsSelected(row), getRowHoverToDraw(row));
}
cellAreaTL.x += cw.width;
}
@@ -981,41 +981,41 @@ private:
void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
{
- if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
- SetFocus();
-
if (auto prov = refParent().getDataProvider())
{
const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff!
+ //row < 0 possible!!! Pressing "Menu Key" simulates mouse-right-button 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 (!sendEventNow(mouseEvent)) //allow client to swallow event!
+ {
+ if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
+ SetFocus();
- 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<MouseSelection>(*this, row, !refParent().isSelected(row) /*positive*/, mouseEvent);
- else if (event.ShiftDown())
- {
- activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true /*positive*/, mouseEvent);
- refParent().clearSelectionImpl(&mouseSelectBegin, ALLOW_GRID_EVENT);
- }
- else
+ if (row >= 0)
+ if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area!
{
- activeSelection_ = std::make_unique<MouseSelection>(*this, row, true /*positive*/, mouseEvent);
- refParent().clearSelectionImpl(&mouseSelectBegin, ALLOW_GRID_EVENT);
+ if (event.ControlDown())
+ activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row) /*positive*/, false /*gridWasCleared*/, mouseEvent);
+ else if (event.ShiftDown())
+ {
+ refParent().clearSelection(GridEventPolicy::DENY);
+ activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true /*positive*/, true /*gridWasCleared*/, mouseEvent);
+ }
+ else
+ {
+ refParent().clearSelection(GridEventPolicy::DENY);
+ activeSelection_ = std::make_unique<MouseSelection>(*this, row, true /*positive*/, true /*gridWasCleared*/, mouseEvent);
+ //DO NOT emit range event for clearing selection! would be inconsistent with keyboard handling (moving cursor neither emits range event)
+ //and is also harmful when range event is considered a final action
+ //e.g. cfg grid would prematurely show a modal dialog after changed config
+ }
}
- }
- 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);
+ Refresh();
+ }
}
event.Skip(); //allow changing focus
}
@@ -1042,15 +1042,14 @@ private:
}
//slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys
- const ptrdiff_t rowFrom = activeSelection_->getStartRow();
- const ptrdiff_t rowTo = activeSelection_->getCurrentRow();
- const bool positive = activeSelection_->isPositiveSelect();
- const MouseSelect mouseSelect{ activeSelection_->getFirstClick(), true /*complete*/ };
+ const ptrdiff_t rowFrom = activeSelection_->getStartRow();
+ const ptrdiff_t rowTo = activeSelection_->getCurrentRow();
+ const bool positive = activeSelection_->isPositiveSelect();
+ const GridClickEvent mouseClick = activeSelection_->getFirstClick();
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);
+ refParent().selectRange(rowFrom, rowTo, positive, &mouseClick, GridEventPolicy::ALLOW);
}
if (auto prov = refParent().getDataProvider())
@@ -1074,9 +1073,15 @@ private:
void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
{
- activeSelection_.reset();
- highlight_.row = -1;
- Refresh();
+ if (activeSelection_)
+ {
+ if (activeSelection_->gridWasCleared())
+ refParent().clearSelection(GridEventPolicy::ALLOW); //see onMouseDown(); selection is "completed" => emit GridSelectEvent
+
+ activeSelection_.reset();
+ }
+ highlight_.row = -1;
+ Refresh();
//event.Skip(); -> we DID handle it!
}
@@ -1128,8 +1133,8 @@ private:
class MouseSelection : private wxEvtHandler
{
public:
- MouseSelection(MainWin& wnd, size_t rowStart, bool positive, const GridClickEvent& firstClick) :
- wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positive), firstClick_(firstClick)
+ MouseSelection(MainWin& wnd, size_t rowStart, bool positive, bool gridWasCleared, const GridClickEvent& firstClick) :
+ wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positive), gridWasCleared_(gridWasCleared), firstClick_(firstClick)
{
wnd_.CaptureMouse();
timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this);
@@ -1141,6 +1146,8 @@ private:
size_t getStartRow () const { return rowStart_; }
size_t getCurrentRow () const { return rowCurrent_; }
bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting?
+ bool gridWasCleared () const { return gridWasCleared_; }
+
const GridClickEvent& getFirstClick() const { return firstClick_; }
void evalMousePos()
@@ -1207,6 +1214,7 @@ private:
const size_t rowStart_;
ptrdiff_t rowCurrent_;
const bool positiveSelect_;
+ const bool gridWasCleared_;
const GridClickEvent firstClick_;
wxTimer timer_;
double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse
@@ -1494,7 +1502,7 @@ void Grid::onKeyDown(wxKeyEvent& event)
if (rowCount > 0)
{
numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
- setGridCursor(row);
+ setGridCursor(row, GridEventPolicy::ALLOW);
}
};
@@ -1503,7 +1511,7 @@ void Grid::onKeyDown(wxKeyEvent& event)
if (rowCount > 0)
{
numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
- selectWithCursor(row);
+ selectWithCursor(row); //emits GridSelectEvent
}
};
@@ -1596,12 +1604,12 @@ void Grid::onKeyDown(wxKeyEvent& event)
case 'A': //Ctrl + A - select all
if (event.ControlDown())
- selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/);
+ selectRange(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/, GridEventPolicy::ALLOW);
break;
case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all
if (event.ControlDown())
- autoSizeColumns(ALLOW_GRID_EVENT);
+ autoSizeColumns(GridEventPolicy::ALLOW);
return;
}
@@ -1628,9 +1636,9 @@ void Grid::selectRow(size_t row, GridEventPolicy rangeEventPolicy)
selection_.selectRow(row);
mainWin_->Refresh();
- if (rangeEventPolicy == ALLOW_GRID_EVENT)
+ if (rangeEventPolicy == GridEventPolicy::ALLOW)
{
- GridSelectEvent selEvent(row, row + 1, true, nullptr);
+ GridSelectEvent selEvent(row, row + 1, true, nullptr /*mouseClick*/);
if (wxEvtHandler* evtHandler = GetEventHandler())
evtHandler->ProcessEvent(selEvent);
}
@@ -1642,23 +1650,23 @@ void Grid::selectAllRows(GridEventPolicy rangeEventPolicy)
selection_.selectAll();
mainWin_->Refresh();
- if (rangeEventPolicy == ALLOW_GRID_EVENT)
+ if (rangeEventPolicy == GridEventPolicy::ALLOW)
{
- GridSelectEvent selEvent(0, getRowCount(), true /*positive*/, nullptr);
+ GridSelectEvent selEvent(0, getRowCount(), true /*positive*/, nullptr /*mouseClick*/);
if (wxEvtHandler* evtHandler = GetEventHandler())
evtHandler->ProcessEvent(selEvent);
}
}
-void Grid::clearSelectionImpl(const MouseSelect* mouseSelect, GridEventPolicy rangeEventPolicy)
+void Grid::clearSelection(GridEventPolicy rangeEventPolicy)
{
selection_.clear();
mainWin_->Refresh();
- if (rangeEventPolicy == ALLOW_GRID_EVENT)
+ if (rangeEventPolicy == GridEventPolicy::ALLOW)
{
- GridSelectEvent unselectionEvent(0, getRowCount(), false /*positive*/, mouseSelect);
+ GridSelectEvent unselectionEvent(0, getRowCount(), false /*positive*/, nullptr /*mouseClick*/);
if (wxEvtHandler* evtHandler = GetEventHandler())
evtHandler->ProcessEvent(unselectionEvent);
}
@@ -1940,17 +1948,17 @@ void Grid::refreshCell(size_t row, ColumnType colType)
}
-void Grid::setGridCursor(size_t row)
+void Grid::setGridCursor(size_t row, GridEventPolicy rangeEventPolicy)
{
mainWin_->setCursor(row, row);
makeRowVisible(row);
selection_.clear(); //clear selection, do NOT fire event
- selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
+ selectRange(row, row, true /*positive*/, nullptr /*mouseInitiated*/, rangeEventPolicy); //set new selection + fire event
}
-void Grid::selectWithCursor(ptrdiff_t row)
+void Grid::selectWithCursor(ptrdiff_t row) //emits GridSelectEvent
{
const size_t anchorRow = mainWin_->getAnchor();
@@ -1958,7 +1966,7 @@ void Grid::selectWithCursor(ptrdiff_t row)
makeRowVisible(row);
selection_.clear(); //clear selection, do NOT fire event
- selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
+ selectRange(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/, GridEventPolicy::ALLOW); //set new selection + fire event
}
@@ -2005,7 +2013,7 @@ void Grid::makeRowVisible(size_t row)
}
-void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const MouseSelect* mouseSelect)
+void Grid::selectRange(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseClick, GridEventPolicy rangeEventPolicy)
{
//sort + convert to half-open range
auto rowFirst = std::min(rowFrom, rowTo);
@@ -2018,10 +2026,12 @@ void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positiv
selection_.selectRange(rowFirst, rowLast, positive);
mainWin_->Refresh();
- //notify event
- GridSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseSelect);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(selectionEvent);
+ if (rangeEventPolicy == GridEventPolicy::ALLOW)
+ {
+ GridSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseClick);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(selectionEvent);
+ }
}
@@ -2121,7 +2131,7 @@ void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEve
if (visibleCols_[col2].stretch > 0) //normalize stretched columns only
visibleCols_[col2].offset = std::max(visibleCols_[col2].offset, fastFromDIP(COLUMN_MIN_WIDTH_DIP) - stretchedWidths[col2]);
- if (columnResizeEventPolicy == ALLOW_GRID_EVENT)
+ if (columnResizeEventPolicy == GridEventPolicy::ALLOW)
{
GridColumnResizeEvent sizeEvent(vcRs.offset, vcRs.type);
if (wxEvtHandler* evtHandler = GetEventHandler())
diff --git a/wx+/grid.h b/wx+/grid.h
index c90981b5..b9a04842 100755
--- a/wx+/grid.h
+++ b/wx+/grid.h
@@ -47,23 +47,18 @@ struct GridClickEvent : public wxMouseEvent
const HoverArea hoverArea_; //may be HoverArea::NONE
};
-struct MouseSelect
-{
- 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) :
+ GridSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick) :
wxCommandEvent(EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive),
- mouseSelect_(mouseSelect ? *mouseSelect : Opt<MouseSelect>()) { assert(rowFirst <= rowLast); }
+ mouseClick_(mouseClick ? *mouseClick : Opt<GridClickEvent>()) { 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!
- const Opt<MouseSelect> mouseSelect_; //filled unless selection was performed via keyboard shortcuts
+ const Opt<GridClickEvent> mouseClick_; //filled unless selection was performed via keyboard shortcuts
};
struct GridLabelClickEvent : public wxMouseEvent
@@ -127,16 +122,15 @@ public:
static wxRect drawCellBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle
static void drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor);
- static wxRect drawColumnLabelBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle
- static void drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted);
+ static wxRect drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted); //returns inner rectangle
static void drawColumnLabelText (wxDC& dc, const wxRect& rect, const std::wstring& text);
};
-enum GridEventPolicy
+enum class GridEventPolicy
{
- ALLOW_GRID_EVENT,
- DENY_GRID_EVENT
+ ALLOW,
+ DENY
};
@@ -187,7 +181,7 @@ public:
std::vector<size_t> getSelectedRows() const { return selection_.get(); }
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 clearSelection(GridEventPolicy rangeEventPolicy); //
void scrollDelta(int deltaX, int deltaY); //in scroll units
@@ -212,7 +206,7 @@ public:
void enableColumnMove (bool value) { allowColumnMove_ = value; }
void enableColumnResize(bool value) { allowColumnResize_ = value; }
- void setGridCursor(size_t row); //set + show + select cursor (+ emit range selection event)
+ void setGridCursor(size_t row, GridEventPolicy rangeEventPolicy); //set + show + select cursor
size_t getGridCursor() const; //returns row
void scrollTo(size_t row);
@@ -232,7 +226,7 @@ private:
void updateWindowSizes(bool updateScrollbar = true);
- void selectWithCursor(ptrdiff_t row);
+ void selectWithCursor(ptrdiff_t row); //emits GridSelectEvent
void redirectRowLabelEvent(wxMouseEvent& event);
@@ -317,9 +311,8 @@ private:
wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found
- 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);
+ //select inclusive range [rowFrom, rowTo]
+ void selectRange(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseClick, GridEventPolicy rangeEventPolicy);
bool isSelected(size_t row) const { return selection_.isSelected(row); }
diff --git a/wx+/image_tools.cpp b/wx+/image_tools.cpp
index 88a78b21..0cb0e328 100755
--- a/wx+/image_tools.cpp
+++ b/wx+/image_tools.cpp
@@ -216,6 +216,42 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const
}
+wxBitmap zen::layOver(const wxBitmap& background, const wxBitmap& foreground, int alignment)
+{
+ if (!foreground.IsOk()) return background;
+
+ assert(foreground.HasAlpha() == background.HasAlpha()); //we don't support mixed-mode brittleness!
+ const int offsetX = [&]
+ {
+ if (alignment & wxALIGN_RIGHT)
+ return background.GetWidth() - foreground.GetWidth();
+ if (alignment & wxALIGN_CENTER_HORIZONTAL)
+ return (background.GetWidth() - foreground.GetWidth()) / 2;
+
+ static_assert(wxALIGN_LEFT == 0);
+ return 0;
+ }();
+
+ const int offsetY = [&]
+ {
+ if (alignment & wxALIGN_BOTTOM)
+ return background.GetHeight() - foreground.GetHeight();
+ if (alignment & wxALIGN_CENTER_VERTICAL)
+ return (background.GetHeight() - foreground.GetHeight()) / 2;
+
+ static_assert(wxALIGN_TOP == 0);
+ return 0;
+ }();
+
+ wxBitmap output(background.ConvertToImage()); //attention: wxBitmap/wxImage use ref-counting without copy on write!
+ {
+ wxMemoryDC dc(output);
+ dc.DrawBitmap(foreground, offsetX, offsetY);
+ }
+ return output;
+}
+
+
void zen::convertToVanillaImage(wxImage& img)
{
if (!img.HasAlpha())
diff --git a/wx+/image_tools.h b/wx+/image_tools.h
index 6dd9b26b..ca82a031 100755
--- a/wx+/image_tools.h
+++ b/wx+/image_tools.h
@@ -22,7 +22,7 @@ enum class ImageStackLayout
VERTICAL
};
-enum class ImageStackAlignment
+enum class ImageStackAlignment //one-dimensional unlike wxAlignment
{
CENTER,
LEFT,
@@ -34,7 +34,7 @@ wxImage stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout d
wxImage createImageFromText(const wxString& text, const wxFont& font, const wxColor& col, ImageStackAlignment textAlign = ImageStackAlignment::LEFT); //CENTER/LEFT/RIGHT
-wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground); //merge
+wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground, int alignment = wxALIGN_CENTER);
wxImage greyScale(const wxImage& img); //greyscale + brightness adaption
wxBitmap greyScale(const wxBitmap& bmp); //
@@ -147,23 +147,6 @@ void adjustBrightness(wxImage& img, int targetLevel)
inline
-wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground)
-{
- assert(foreground.HasAlpha() == background.HasAlpha()); //we don't support mixed-mode brittleness!
-
- wxBitmap output(background.ConvertToImage()); //attention: wxBitmap/wxImage use ref-counting without copy on write!
- {
- wxMemoryDC dc(output);
-
- const int offsetX = (background.GetWidth () - foreground.GetWidth ()) / 2;
- const int offsetY = (background.GetHeight() - foreground.GetHeight()) / 2;
- dc.DrawBitmap(foreground, offsetX, offsetY);
- }
- return output;
-}
-
-
-inline
bool isEqual(const wxBitmap& lhs, const wxBitmap& rhs)
{
if (lhs.IsOk() != rhs.IsOk())
diff --git a/wx+/popup_dlg_generated.cpp b/wx+/popup_dlg_generated.cpp
index 25ac00e1..3e490757 100755
--- a/wx+/popup_dlg_generated.cpp
+++ b/wx+/popup_dlg_generated.cpp
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
-// C++ code generated with wxFormBuilder (version Jan 23 2018)
+// C++ code generated with wxFormBuilder (version May 29 2018)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
diff --git a/wx+/popup_dlg_generated.h b/wx+/popup_dlg_generated.h
index 0d3459e2..9d9bc3f8 100755
--- a/wx+/popup_dlg_generated.h
+++ b/wx+/popup_dlg_generated.h
@@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
-// C++ code generated with wxFormBuilder (version Jan 23 2018)
+// C++ code generated with wxFormBuilder (version May 29 2018)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
diff --git a/xBRZ/src/xbrz.cpp b/xBRZ/src/xbrz.cpp
index 3cbd0d64..b8065f5d 100755
--- a/xBRZ/src/xbrz.cpp
+++ b/xBRZ/src/xbrz.cpp
@@ -1065,11 +1065,11 @@ struct ColorGradientARGB
void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth, int srcHeight, ColorFormat colFmt, const xbrz::ScalerCfg& cfg, int yFirst, int yLast)
{
-if (factor == 1)
- {
- std::copy(src + yFirst * srcWidth, src + yLast * srcWidth, trg);
- return;
- }
+ if (factor == 1)
+ {
+ std::copy(src + yFirst * srcWidth, src + yLast * srcWidth, trg);
+ return;
+ }
static_assert(SCALE_FACTOR_MAX == 6);
switch (colFmt)
diff --git a/zen/dir_watcher.h b/zen/dir_watcher.h
index b4796618..f552e2b2 100755
--- a/zen/dir_watcher.h
+++ b/zen/dir_watcher.h
@@ -44,7 +44,7 @@ public:
enum ActionType
{
- ACTION_CREATE, //informal only!
+ ACTION_CREATE, //informal!
ACTION_UPDATE, //use for debugging/logging only!
ACTION_DELETE, //
};
@@ -52,7 +52,7 @@ public:
struct Entry
{
ActionType action = ACTION_CREATE;
- Zstring filePath;
+ Zstring itemPath;
};
//extract accumulated changes since last call
diff --git a/zen/error_log.h b/zen/error_log.h
index 0de88856..4a3f5f2c 100755
--- a/zen/error_log.h
+++ b/zen/error_log.h
@@ -33,15 +33,13 @@ struct LogEntry
Zstringw message; //std::wstring may employ small string optimization: we cannot accept bloating the "ErrorLog::entries" memory block below (think 1 million items)
};
-template <class String>
-String formatMessage(const LogEntry& entry);
+std::wstring formatMessage(const LogEntry& entry);
class ErrorLog
{
public:
- template <class String> //a wchar_t-based string!
- void logMsg(const String& text, MessageType type);
+ void logMsg(const std::wstring& msg, MessageType type);
int getItemCount(int typeFilter = MSG_TYPE_INFO | MSG_TYPE_WARNING | MSG_TYPE_ERROR | MSG_TYPE_FATAL_ERROR) const;
@@ -49,10 +47,10 @@ public:
using const_iterator = std::vector<LogEntry>::const_iterator;
const_iterator begin() const { return entries_.begin(); }
const_iterator end () const { return entries_.end (); }
- bool empty() const { return entries_.empty(); }
+ bool empty() const { return entries_.empty(); }
private:
- std::vector<LogEntry> entries_; //list of non-resolved errors and warnings
+ std::vector<LogEntry> entries_;
};
@@ -64,10 +62,10 @@ private:
//######################## implementation ##########################
-template <class String> inline
-void ErrorLog::logMsg(const String& text, MessageType type)
+inline
+void ErrorLog::logMsg(const std::wstring& msg, MessageType type)
{
- entries_.push_back({ std::time(nullptr), type, copyStringTo<Zstringw>(text) });
+ entries_.push_back({ std::time(nullptr), type, copyStringTo<Zstringw>(msg) });
}
@@ -80,8 +78,7 @@ int ErrorLog::getItemCount(int typeFilter) const
namespace
{
-template <class String>
-String formatMessageImpl(const LogEntry& entry) //internal linkage
+std::wstring formatMessageImpl(const LogEntry& entry)
{
auto getTypeName = [&]
{
@@ -100,17 +97,14 @@ String formatMessageImpl(const LogEntry& entry) //internal linkage
return std::wstring();
};
- String formattedText = L"[" + formatTime<String>(FORMAT_TIME, getLocalTime(entry.time)) + L"] " + copyStringTo<String>(getTypeName()) + L": ";
- const size_t prefixLen = formattedText.size(); //considers UTF-16 only!
+ std::wstring msgFmt = L"[" + formatTime<std::wstring>(FORMAT_TIME, getLocalTime(entry.time)) + L"] " + getTypeName() + L": ";
+ const size_t prefixLen = msgFmt.size(); //considers UTF-16 only!
for (auto it = entry.message.begin(); it != entry.message.end(); )
if (*it == L'\n')
{
- formattedText += L'\n';
-
- String blanks;
- blanks.resize(prefixLen, L' ');
- formattedText += blanks;
+ msgFmt += L'\n';
+ msgFmt.append(prefixLen, L' ');
do //skip duplicate newlines
{
@@ -119,14 +113,14 @@ String formatMessageImpl(const LogEntry& entry) //internal linkage
while (it != entry.message.end() && *it == L'\n');
}
else
- formattedText += *it++;
+ msgFmt += *it++;
- return formattedText;
+ return msgFmt;
}
}
-template <class String> inline
-String formatMessage(const LogEntry& entry) { return formatMessageImpl<String>(entry); }
+inline
+std::wstring formatMessage(const LogEntry& entry) { return formatMessageImpl(entry); }
}
#endif //ERROR_LOG_H_8917590832147915
diff --git a/zen/process_priority.cpp b/zen/process_priority.cpp
index c2a7ed20..e925f142 100755
--- a/zen/process_priority.cpp
+++ b/zen/process_priority.cpp
@@ -11,8 +11,6 @@
using namespace zen;
-//wxPowerResourceBlocker? http://docs.wxwidgets.org/trunk/classwx_power_resource_blocker.html
-//nah, "currently the power events are only available under Windows"
struct PreventStandby::Impl {};
PreventStandby::PreventStandby() {}
PreventStandby::~PreventStandby() {}
diff --git a/zen/shell_execute.h b/zen/shell_execute.h
index 43bede61..a0e5634b 100755
--- a/zen/shell_execute.h
+++ b/zen/shell_execute.h
@@ -68,6 +68,13 @@ void shellExecute(const Zstring& command, ExecutionType type) //throw FileError
}
}
}
+
+
+inline
+void openWithDefaultApplication(const Zstring& itemPath) //throw FileError
+{
+ shellExecute("xdg-open \"" + itemPath + "\"", ExecutionType::ASYNC); //
+}
}
#endif //SHELL_EXECUTE_H_23482134578134134
diff --git a/zen/string_tools.h b/zen/string_tools.h
index e09cb61f..8746722a 100755
--- a/zen/string_tools.h
+++ b/zen/string_tools.h
@@ -75,8 +75,8 @@ template <class S> S trimCpy(S str, bool fromLeft = true, bo
template <class S> void trim (S& str, bool fromLeft = true, bool fromRight = true);
template <class S, class Function> void trim(S& str, bool fromLeft, bool fromRight, Function trimThisChar);
-template <class S, class T, class U> void replace ( S& str, const T& oldTerm, const U& newTerm, bool replaceAll = true);
-template <class S, class T, class U> S replaceCpy(const S& str, const T& oldTerm, const U& newTerm, bool replaceAll = true);
+template <class S, class T, class U> void replace (S& str, const T& oldTerm, const U& newTerm, bool replaceAll = true);
+template <class S, class T, class U> S replaceCpy(S str, const T& oldTerm, const U& newTerm, bool replaceAll = true);
//high-performance conversion between numbers and strings
template <class S, class Num> S numberTo(const Num& number);
@@ -348,19 +348,28 @@ ZEN_INIT_DETECT_MEMBER(append);
template <class S, class InputIterator> inline
std::enable_if_t<HasMember_append<S>::value> stringAppend(S& str, InputIterator first, InputIterator last) { str.append(first, last); }
-template <class S, class InputIterator> inline
-std::enable_if_t<!HasMember_append<S>::value> stringAppend(S& str, InputIterator first, InputIterator last) { str += S(first, last); }
+//inefficient append: keep disabled until really needed
+//template <class S, class InputIterator> inline
+//std::enable_if_t<!HasMember_append<S>::value> stringAppend(S& str, InputIterator first, InputIterator last) { str += S(first, last); }
+}
+
+
+template <class S, class T, class U> inline
+S replaceCpy(S str, const T& oldTerm, const U& newTerm, bool replaceAll)
+{
+ replace(str, oldTerm, newTerm, replaceAll);
+ return str;
}
template <class S, class T, class U> inline
-S replaceCpy(const S& str, const T& oldTerm, const U& newTerm, bool replaceAll)
+void replace(S& str, const T& oldTerm, const U& newTerm, bool replaceAll)
{
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
static_assert(std::is_same_v<GetCharTypeT<T>, GetCharTypeT<U>>);
const size_t oldLen = strLength(oldTerm);
if (oldLen == 0)
- return str;
+ return;
const auto* const oldBegin = strBegin(oldTerm);
const auto* const oldEnd = oldBegin + oldLen;
@@ -368,35 +377,31 @@ S replaceCpy(const S& str, const T& oldTerm, const U& newTerm, bool replaceAll)
const auto* const newBegin = strBegin(newTerm);
const auto* const newEnd = newBegin + strLength(newTerm);
- S output;
-
- for (auto it = str.begin();;)
- {
- const auto itFound = std::search(it, str.end(),
- oldBegin, oldEnd);
- if (itFound == str.end() && it == str.begin())
- return str; //optimize "oldTerm not found": return ref-counted copy
+ auto it = strBegin(str); //don't use str.begin() or wxString will return this wxUni* nonsense!
+ const auto* const strEnd = it + strLength(str);
- impl::stringAppend(output, it, itFound);
- if (itFound == str.end())
- return output;
+ auto itFound = std::search(it, strEnd,
+ oldBegin, oldEnd);
+ if (itFound == strEnd)
+ return; //optimize "oldTerm not found"
+ S output(it, itFound);
+ do
+ {
impl::stringAppend(output, newBegin, newEnd);
it = itFound + oldLen;
if (!replaceAll)
- {
- impl::stringAppend(output, it, str.end());
- return output;
- }
- }
-}
+ itFound = strEnd;
+ else
+ itFound = std::search(it, strEnd,
+ oldBegin, oldEnd);
+ impl::stringAppend(output, it, itFound);
+ }
+ while (itFound != strEnd);
-template <class S, class T, class U> inline
-void replace(S& str, const T& oldTerm, const U& newTerm, bool replaceAll)
-{
- str = replaceCpy(str, oldTerm, newTerm, replaceAll);
+ str = std::move(output);
}
@@ -437,7 +442,7 @@ S trimCpy(S str, bool fromLeft, bool fromRight)
{
//implementing trimCpy() in terms of trim(), instead of the other way round, avoids memory allocations when trimming from right!
trim(str, fromLeft, fromRight);
- return std::move(str); //"str" is an l-value parameter => no copy elision!
+ return str;
}
diff --git a/zen/zstring.h b/zen/zstring.h
index 026737da..3938cef1 100755
--- a/zen/zstring.h
+++ b/zen/zstring.h
@@ -125,7 +125,7 @@ S makeUpperCopy(S str)
if (len > 0)
makeUpperInPlace(&*str.begin(), len);
- return std::move(str); //"str" is an l-value parameter => no copy elision!
+ return str;
}
bgstack15