diff options
author | B Stack <bgstack15@gmail.com> | 2018-08-10 11:29:21 -0400 |
---|---|---|
committer | B Stack <bgstack15@gmail.com> | 2018-08-10 11:29:21 -0400 |
commit | a9255194193b04d599b9f0a64cde6b831dd7d916 (patch) | |
tree | 8b975393f5b0f7b4403da85f5095ad57581178e5 | |
parent | 10.2 (diff) | |
download | FreeFileSync-10.3.tar.gz FreeFileSync-10.3.tar.bz2 FreeFileSync-10.3.zip |
10.310.3
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><installation folder></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 @@ <<b>RunWithBackgroundPriority</b> Enabled="false"/><br> <<b>LockDirectoriesDuringSync</b> Enabled="true"/><br> <<b>VerifyCopiedFiles</b> Enabled="false"/><br> - <<b>LastSyncsLogSizeMax</b> Bytes="100000"/><br> <<b>NotificationSound</b> CompareFinished="ding.wav" SyncFinished="harp.wav"/> </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">"C:\Program Files (x86)\WinMerge\WinMergeU.exe" "%local_path%" "%local_path2%"</div><br> + <li>Start file content comparison tool (WinMerge):<br> + <div class="command-line">"C:\Program Files (x86)\WinMerge\WinMergeU.exe" "%local_path%" "%local_path2%"</div> + <br> + opendiff on macOS (requires Xcode):<br> + <div class="command-line">opendiff "%local_path%" "%local_path2%"</div><br> <li>Show file in Windows Explorer:<br> <div class="command-line">explorer /select, "%local_path%"</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> - - <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> - + <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 Binary files differindex 0c189c5c..435762b4 100755 --- a/FreeFileSync/Build/Help/images/performance.png +++ b/FreeFileSync/Build/Help/images/performance.png diff --git a/FreeFileSync/Build/Help/images/setup-batch-job.png b/FreeFileSync/Build/Help/images/setup-batch-job.png Binary files differindex cc38e85c..4eb8556b 100755 --- a/FreeFileSync/Build/Help/images/setup-batch-job.png +++ b/FreeFileSync/Build/Help/images/setup-batch-job.png diff --git a/FreeFileSync/Build/Help/images/versioning.png b/FreeFileSync/Build/Help/images/versioning.png Binary files differindex f9fd5b56..9d4260ca 100755 --- a/FreeFileSync/Build/Help/images/versioning.png +++ b/FreeFileSync/Build/Help/images/versioning.png 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 Binary files differindex fda17b61..48cee7b5 100755 --- a/FreeFileSync/Build/Resources.zip +++ b/FreeFileSync/Build/Resources.zip 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(¤t); - int budfk = 3; - } - { - time_t current = zen::to<time_t>(timeTo_); - struct tm* tdfewst = ::localtime(¤t); - 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()) @@ -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; } |