Files
free-file-sync-mirror/FreeFileSync/Source/ui/progress_indicator.cpp
2025-12-10 14:38:26 -08:00

1747 lines
72 KiB
C++

// *****************************************************************************
// * 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 "progress_indicator.h"
#include <memory>
#include <wx/wupdlock.h>
#include <wx/app.h>
#include <zen/format_unit.h>
#include <wx+/image_tools.h>
#include <wx+/graph.h>
#include <wx+/no_flicker.h>
#include <wx+/window_layout.h>
#include <zen/perf.h>
#include <wx+/choice_enum.h>
#include "wx+/taskbar.h"
#include "gui_generated.h"
#include "tray_icon.h"
#include "log_panel.h"
#include "app_icon.h"
#include "../icon_buffer.h"
#include "../base/speed_test.h"
using namespace zen;
using namespace fff;
namespace
{
constexpr std::chrono::seconds PERF_WINDOW_BYTES_PER_SEC (4); //window size used for statistics
constexpr std::chrono::seconds PERF_WINDOW_REMAINING_TIME(60); //USB memory stick can have 40-second-hangs
constexpr std::chrono::seconds SPEED_ESTIMATE_SAMPLE_SKIP(1);
constexpr std::chrono::milliseconds SPEED_ESTIMATE_UPDATE_INTERVAL(500);
constexpr std::chrono::seconds GRAPH_TOTAL_TIME_UPDATE_INTERVAL(2);
const size_t PROGRESS_GRAPH_SAMPLE_SIZE_MAX = 2'500'000; //sizeof(CurveDataStatistics::Sample) == 16 byte key/value
wxColor getColorBytes () { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x16, 0xd2, 0x02} /*medium green*/ : wxColor{111, 255, 99} /*light green*/; }
wxColor getColorItems () { return wxSystemSettings::GetAppearance().IsDark() ? wxColor{0x53, 0x71, 0xfb} /*medium blue*/ : wxColor{127, 147, 255} /*light blue*/; }
wxColor getColorEstimate() { return wxSystemSettings::GetAppearance().IsDark() ? 0xc4c4c4 /*medium grey*/ : 0xf0f0f0 /*light grey*/; }
wxColor getColorEstimateText() { return 0x000000UL; }
std::wstring getDialogPhaseText(const Statistics& syncStat, bool paused)
{
if (paused)
return _("Paused");
if (syncStat.taskCancelled())
return _("Stop requested...");
switch (syncStat.currentPhase())
{
case ProcessPhase::none:
return _("Initializing..."); //dialog is shown *before* sync starts, so this text may be visible!
case ProcessPhase::scan:
return _("Scanning...");
case ProcessPhase::binaryCompare:
return _("Comparing content...");
case ProcessPhase::sync:
return _("Synchronizing...");
}
assert(false);
return std::wstring();
}
class CurveDataProgressBar : public CurveData
{
public:
CurveDataProgressBar(bool drawTop) : drawTop_(drawTop) {}
void setFraction(double fraction) { fraction_ = fraction; } //value between [0, 1]
private:
std::pair<double, double> getRangeX() const override { return {0, 1}; }
std::vector<CurvePoint> getPoints(double minX, double maxX, const wxSize& areaSizePx) const override
{
const double yLow = drawTop_ ? 1 : -1; //draw partially out of vertical bounds to not render top/bottom borders of the bars
const double yHigh = drawTop_ ? 3 : 1; //
return
{
{0, yHigh},
{fraction_, yHigh},
{fraction_, yLow },
{0, yLow },
};
}
double fraction_ = 0;
const bool drawTop_;
};
class CurveDataProgressSeparatorLine : public CurveData
{
std::pair<double, double> getRangeX() const override { return {0, 1}; }
std::vector<CurvePoint> getPoints(double minX, double maxX, const wxSize& areaSizePx) const override
{
return
{
{0, 1},
{1, 1},
};
}
};
}
class CompareProgressPanel::Impl : public CompareProgressDlgGenerated
{
public:
explicit Impl(wxFrame& parentWindow);
void init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount); //constructor/destructor semantics, but underlying Window is reused
void teardown(); //
void initNewPhase();
void updateProgressGui(bool allowYield);
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(); }
std::chrono::milliseconds pauseAndGetTotalTime()
{
stopWatch_.pause();
return std::chrono::duration_cast<std::chrono::milliseconds>(stopWatch_.elapsed());
}
private:
//void onToggleIgnoreErrors(wxCommandEvent& event) override { updateStaticGui(); }
void updateStaticGui();
wxFrame& parentWindow_;
wxString parentTitleBackup_;
StopWatch stopWatch_;
std::chrono::nanoseconds phaseStart_{}; //begin of current phase
const Statistics* syncStat_ = nullptr; //only bound while sync is running
std::optional<Taskbar> taskbar_;
SpeedTest remTimeTest_{PERF_WINDOW_REMAINING_TIME};
SpeedTest speedTest_ {PERF_WINDOW_BYTES_PER_SEC};
std::chrono::nanoseconds timeLastSpeedEstimate_ = std::chrono::seconds(-100); //used for calculating intervals between showing and collecting perf samples
//initial value: just some big number
SharedRef<CurveDataProgressBar> curveDataBytes_{makeSharedRef<CurveDataProgressBar>(true /*drawTop*/)};
SharedRef<CurveDataProgressBar> curveDataItems_{makeSharedRef<CurveDataProgressBar>(false /*drawTop*/)};
bool ignoreErrors_ = false;
};
CompareProgressPanel::Impl::Impl(wxFrame& parentWindow) :
CompareProgressDlgGenerated(&parentWindow),
parentWindow_(parentWindow)
{
setImage(*m_bitmapItemStat, IconBuffer::genericFileIcon(IconBuffer::IconSize::small));
setImage(*m_bitmapTimeStat, loadImage("time", -1 /*maxWidth*/, IconBuffer::getPixSize(IconBuffer::IconSize::small)));
m_bitmapTimeStat->SetMinSize({-1, screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small))});
setImage(*m_bitmapErrors, loadImage("msg_error", dipToScreen(getMenuIconDipSize())));
setImage(*m_bitmapWarnings, loadImage("msg_warning", dipToScreen(getMenuIconDipSize())));
setImage(*m_bitmapIgnoreErrors, loadImage("error_ignore_active", dipToScreen(getMenuIconDipSize())));
setImage(*m_bitmapRetryErrors, loadImage("error_retry", dipToScreen(getMenuIconDipSize())));
//make sure standard height matches ProcessPhase::binaryCompare statistics layout (== largest)
//init graph
m_panelProgressGraph->setAttributes(Graph2D::MainAttributes().setMinY(0).setMaxY(2).
setLabelX(XLabelPos::none).
setLabelY(YLabelPos::none).
setBaseColors(getColorEstimateText(), getColorEstimate()).
setSelectionMode(GraphSelMode::none));
const wxColor gridLineColor = m_panelProgressGraph->getAttributes().getGridLineColor();
m_panelProgressGraph->addCurve(curveDataBytes_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillPolygonArea(getColorBytes()).setColor(gridLineColor));
m_panelProgressGraph->addCurve(curveDataItems_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillPolygonArea(getColorItems()).setColor(gridLineColor));
m_panelProgressGraph->addCurve(makeSharedRef<CurveDataProgressSeparatorLine>(), Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).setColor(gridLineColor));
Layout();
m_panelItemStats->Layout();
m_panelTimeStats->Layout();
m_panelErrorStats->Layout();
GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize()
#ifdef __WXGTK3__
//Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088
//Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404
#endif
}
void CompareProgressPanel::Impl::init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount)
{
assert(!syncStat_);
syncStat_ = &syncStat;
parentTitleBackup_ = parentWindow_.GetTitle();
try //try to get access to Windows 7/Ubuntu taskbar
{
taskbar_.emplace(this); //throw TaskbarNotAvailable
}
catch (const TaskbarNotAvailable&) {}
stopWatch_ = StopWatch(); //reset to measure total time
setText(*m_staticTextRetryCount, L'(' + formatNumber(autoRetryCount) + MULT_SIGN + L')');
bSizerErrorsRetry->Show(autoRetryCount > 0);
//allow changing a few options dynamically during sync
ignoreErrors_ = ignoreErrors;
updateStaticGui();
initNewPhase();
}
void CompareProgressPanel::Impl::teardown()
{
assert(stopWatch_.isPaused()); //why wasn't pauseAndGetTotalTime() called?
syncStat_ = nullptr;
parentWindow_.SetTitle(parentTitleBackup_);
taskbar_.reset();
}
void CompareProgressPanel::Impl::initNewPhase()
{
//start new measurement
remTimeTest_.clear();
speedTest_ .clear();
timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check
phaseStart_ = stopWatch_.elapsed();
const int itemsTotal = syncStat_->getTotalStats().items;
const int64_t bytesTotal = syncStat_->getTotalStats().bytes;
const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0;
if (taskbar_) taskbar_->setStatus(haveTotalStats ? Taskbar::Status::normal : Taskbar::Status::indeterminate);
m_staticTextProcessed ->Show(haveTotalStats);
m_staticTextRemaining ->Show(haveTotalStats);
m_staticTextItemsRemaining->Show(haveTotalStats);
m_staticTextBytesRemaining->Show(haveTotalStats);
m_staticTextTimeRemaining ->Show(haveTotalStats);
bSizerProgressGraph ->Show(haveTotalStats);
Layout(); //
m_panelItemStats->Layout(); //redundant? can we trust updateProgressGui() to do the same after detecting "layoutChanged"?
m_panelTimeStats->Layout(); //
updateProgressGui(false /*allowYield*/);
}
void CompareProgressPanel::Impl::updateStaticGui()
{
bSizerErrorsIgnore->Show(ignoreErrors_);
Layout();
}
void CompareProgressPanel::Impl::updateProgressGui(bool allowYield)
{
assert(syncStat_);
if (!syncStat_) //no comparison running!?
return;
auto setTitle = [&](const wxString& title)
{
if (parentWindow_.GetTitle() != title)
parentWindow_.SetTitle(title);
};
bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary
const std::chrono::nanoseconds timeElapsed = stopWatch_.elapsed();
const int itemsCurrent = syncStat_->getCurrentStats().items;
const int64_t bytesCurrent = syncStat_->getCurrentStats().bytes;
const int itemsTotal = syncStat_->getTotalStats ().items;
const int64_t bytesTotal = syncStat_->getTotalStats ().bytes;
const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0;
//status texts
setText(*m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts!
if (!haveTotalStats)
{
//dialog caption, taskbar
setTitle(formatNumber(itemsCurrent) + L' ' + getDialogPhaseText(*syncStat_, false /*paused*/));
//progress indicators
//taskbar_ already set to STATUS_INDETERMINATE by initNewPhase()
}
else
{
//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(formatProgressPercent(fractionTotal) + L' ' + getDialogPhaseText(*syncStat_, false /*paused*/));
//progress indicators
if (taskbar_) taskbar_->setProgress(fractionTotal);
curveDataBytes_.ref().setFraction(fractionBytes);
curveDataItems_.ref().setFraction(fractionItems);
}
//item and data stats
if (!haveTotalStats)
{
setText(*m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged);
setText(*m_staticTextBytesProcessed, L"", &layoutChanged);
}
else
{
setText(*m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged);
setText(*m_staticTextBytesProcessed, L'(' + formatFilesizeShort(bytesCurrent) + L')', &layoutChanged);
setText(*m_staticTextItemsRemaining, formatNumber(itemsTotal - itemsCurrent), &layoutChanged);
setText(*m_staticTextBytesRemaining, L'(' + formatFilesizeShort(bytesTotal - bytesCurrent) + L')', &layoutChanged);
}
auto showIfNeeded = [&](wxWindow& wnd, bool show)
{
if (wnd.IsShown() != show)
{
wnd.Show(show);
layoutChanged = true;
}
};
//errors and warnings (pop up dynamically)
const Statistics::ErrorStats errorStats = syncStat_->getErrorStats();
showIfNeeded(*m_staticTextErrors, errorStats.errorCount != 0);
showIfNeeded(*m_staticTextWarnings, errorStats.warningCount != 0);
showIfNeeded(*m_panelErrorStats, errorStats.errorCount != 0 || errorStats.warningCount != 0);
if (m_panelErrorStats->IsShown())
{
showIfNeeded(*m_bitmapErrors, errorStats.errorCount != 0);
showIfNeeded(*m_staticTextErrorCount, errorStats.errorCount != 0);
if (m_staticTextErrorCount->IsShown())
setText(*m_staticTextErrorCount, formatNumber(errorStats.errorCount), &layoutChanged);
showIfNeeded(*m_bitmapWarnings, errorStats.warningCount != 0);
showIfNeeded(*m_staticTextWarningCount, errorStats.warningCount != 0);
if (m_staticTextWarningCount->IsShown())
setText(*m_staticTextWarningCount, formatNumber(errorStats.warningCount), &layoutChanged);
}
//current time elapsed
const int64_t timeElapSec = std::chrono::duration_cast<std::chrono::seconds>(timeElapsed).count();
setText(*m_staticTextTimeElapsed, utfTo<wxString>(formatTimeSpan(timeElapSec, false /*hourRequired*/)), &layoutChanged);
if (haveTotalStats) //remaining time and speed: only visible during binary comparison
if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL)
{
timeLastSpeedEstimate_ = timeElapsed;
if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_SKIP) //discard stats for first second: probably messy
{
remTimeTest_.addSample(timeElapsed, itemsCurrent, bytesCurrent);
speedTest_ .addSample(timeElapsed, itemsCurrent, bytesCurrent);
}
//current speed -> Win 7 copy uses 1 sec update interval instead
m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(speedTest_.getBytesPerSecFmt(), GraphCorner::topL));
m_panelProgressGraph->setAttributes(m_panelProgressGraph->getAttributes().setCornerText(speedTest_.getItemsPerSecFmt(), GraphCorner::bottomL));
//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
std::optional<double> remTimeSec = remTimeTest_.getRemainingSec(itemsTotal - itemsCurrent, bytesTotal - bytesCurrent);
setText(*m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : std::wstring(1, EM_DASH), &layoutChanged);
}
if (haveTotalStats)
m_panelProgressGraph->Refresh();
//adapt layout after content changes above
if (layoutChanged)
{
Layout();
m_panelItemStats->Layout();
m_panelTimeStats->Layout();
if (m_panelErrorStats->IsShown())
m_panelErrorStats->Layout();
}
//do the ui update
if (allowYield)
wxTheApp->Yield(); //pump GUI messages
else
this->Update(); //don't wait until next idle event (who knows what blocking process comes next?)
}
//########################################################################################
//redirect to implementation
CompareProgressPanel::CompareProgressPanel(wxFrame& parentWindow) : pimpl_(new Impl(parentWindow)) {} //owned by parentWindow
wxWindow* CompareProgressPanel::getAsWindow() { return pimpl_; }
void CompareProgressPanel::init(const Statistics& syncStat, bool ignoreErrors, size_t autoRetryCount) { pimpl_->init(syncStat, ignoreErrors, autoRetryCount); }
void CompareProgressPanel::teardown() { pimpl_->teardown(); }
void CompareProgressPanel::initNewPhase() { pimpl_->initNewPhase(); }
void CompareProgressPanel::updateGui() { pimpl_->updateProgressGui(true /*allowYield*/); }
bool CompareProgressPanel::getOptionIgnoreErrors() const { return pimpl_->getOptionIgnoreErrors(); }
void CompareProgressPanel::setOptionIgnoreErrors(bool ignoreErrors) { pimpl_->setOptionIgnoreErrors(ignoreErrors); }
void CompareProgressPanel::timerSetStatus(bool active) { pimpl_->timerSetStatus(active); }
bool CompareProgressPanel::timerIsRunning() const { return pimpl_->timerIsRunning(); }
std::chrono::milliseconds CompareProgressPanel::pauseAndGetTotalTime() { return pimpl_->pauseAndGetTotalTime(); }
//########################################################################################
namespace
{
class CurveDataStatistics : public SparseCurveData
{
public:
CurveDataStatistics() : SparseCurveData(true /*addSteps*/) {}
void clear() { samples_.clear(); lastSample_ = {}; }
void addSample(double timeElapsed /*[sec]*/, double value /*[items|bytes]*/)
{
assert(( samples_.empty() && lastSample_.x == 0 && lastSample_.y == 0) ||
(!samples_.empty() && samples_.back().x <= lastSample_.x));
if (timeElapsed < lastSample_.x) //time *required* to be monotonously ascending for std::partition_point
{
assert(false);
return;
}
lastSample_ = {timeElapsed, value};
//allow for at most one sample per 100ms (handles duplicate inserts, too!) => unrelated to UI_UPDATE_INTERVAL!
if (!samples_.empty() && timeElapsed - samples_.back().x < 0.1)
return;
samples_.push_back(CurvePoint{timeElapsed, value});
if (samples_.size() > PROGRESS_GRAPH_SAMPLE_SIZE_MAX) //limit buffer size
samples_.pop_front();
}
private:
std::pair<double, double> getRangeX() const override
{
if (samples_.empty())
return {};
/*
//report some additional width by 5% elapsed time to make graph recalibrate before hitting the right border
//caveat: graph for batch mode binary comparison does NOT start at elapsed time 0!! ProcessPhase::binaryCompare and ProcessPhase::sync!
//=> consider width of current sample set!
upperEndMs += 0.05 *(upperEndMs - samples.begin()->first);
*/
return {samples_.front().x, //need not start with 0, e.g. "binary comparison, graph reset, followed by sync"
lastSample_.x};
}
std::optional<CurvePoint> getLessEq(double x) const override //x: seconds since begin
{
//--------- add artifical last sample value --------
if (!samples_.empty() && lastSample_.x <= x)
return lastSample_;
//--------------------------------------------------
//find first item > x, then go one step back:
auto it = std::partition_point(samples_.begin(), samples_.end(),
/*find first item for which "!pred"*/ [x](const CurvePoint& p) { return p.x <= x; });
if (it == samples_.begin())
return std::nullopt;
--it; //bound!
return *it;
}
std::optional<CurvePoint> getGreaterEq(double x) const override
{
//find first item >= x
const auto it = std::partition_point(samples_.begin(), samples_.end(),
/*find first item for which "!pred"*/ [x](const CurvePoint& p) { return p.x < x; });
if (it != samples_.end())
return *it;
//--------- add artifical last sample value --------
if (!samples_.empty() && x <= lastSample_.x)
return lastSample_;
//--------------------------------------------------
return std::nullopt;
}
RingBuffer<CurvePoint> samples_; //x: monotonously ascending with time!
CurvePoint lastSample_; //artificial record after end of samples to visualize current time!
};
class CurveDataEstimate : public CurveData
{
public:
void setValue(double x1, double x2, double y1, double y2) { x1_ = x1; x2_ = x2; y1_ = y1; y2_ = y2; }
void setTotalTime(double x2) { x2_ = x2; }
double getTotalTime() const { return x2_; }
private:
std::pair<double, double> getRangeX() const override { return {x1_, x2_}; }
std::vector<CurvePoint> getPoints(double minX, double maxX, const wxSize& areaSizePx) const override
{
return
{
{x1_, y1_},
{x2_, y2_},
};
}
double x1_ = 0; //elapsed time [s]
double x2_ = 0; //total time [s] (estimated)
double y1_ = 0; //items/bytes processed
double y2_ = 0; //items/bytes total
};
class CurveDataTimeMarker : public CurveData
{
public:
void setValue(double x, double y) { x_ = x; y_ = y; }
void setTime(double x) { x_ = x; }
private:
std::pair<double, double> getRangeX() const override { return {x_, x_}; }
std::vector<CurvePoint> getPoints(double minX, double maxX, const wxSize& areaSizePx) const override
{
return
{
{x_, y_},
{x_, 0 },
};
}
double x_ = 0; //time [s]
double y_ = 0; //items/bytes
};
const double stretchDefaultBlockSize = 1.4; //enlarge block default size
struct LabelFormatterBytes : public LabelFormatter
{
double getOptimalBlockSize(double bytesProposed) const override
{
bytesProposed *= stretchDefaultBlockSize; //enlarge block default size
if (bytesProposed <= 1) //never smaller than 1 byte
return 1;
//round to next number which is a convenient to read block size
const double k = std::floor(std::log(bytesProposed) / std::numbers::ln2);
const double e = std::pow(2.0, k);
if (numeric::isNull(e))
return 0;
const double a = bytesProposed / e; //bytesProposed = a * 2^k with a in [1, 2)
assert(1 <= a && a < 2);
const double steps[] = {1, 2};
return e * numeric::roundToGrid(a, std::begin(steps), std::end(steps));
}
wxString formatText(double value, double optimalBlockSize) const override { return formatFilesizeShort(static_cast<int64_t>(value)); }
};
struct LabelFormatterItemCount : public LabelFormatter
{
double getOptimalBlockSize(double itemsProposed) const override
{
itemsProposed *= stretchDefaultBlockSize; //enlarge block default size
const double steps[] = {1, 2, 5, 10};
if (itemsProposed <= 10)
return numeric::roundToGrid(itemsProposed, std::begin(steps), std::end(steps)); //like nextNiceNumber(), but without the 2.5 step!
return nextNiceNumber(itemsProposed);
}
wxString formatText(double value, double optimalBlockSize) const override
{
return formatNumber(std::round(value)); //not enough room for a "%x items" representation
}
};
struct LabelFormatterTimeElapsed : public LabelFormatter
{
double getOptimalBlockSize(double secProposed) const override
{
//5 sec minimum block size
const double stepsSec[] = {5, 10, 20, 30, 60}; //nice numbers for seconds
if (secProposed <= 60)
return numeric::roundToGrid(secProposed, std::begin(stepsSec), std::end(stepsSec));
const double stepsMin[] = {1, 2, 5, 10, 15, 20, 30, 60}; //nice numbers for minutes
if (secProposed <= 3600)
return 60 * numeric::roundToGrid(secProposed / 60, std::begin(stepsMin), std::end(stepsMin));
if (secProposed <= 3600 * 24)
return 3600 * nextNiceNumber(secProposed / 3600); //round to full hours
return 24 * 3600 * nextNiceNumber(secProposed / (24 * 3600)); //round to full days
}
wxString formatText(double timeElapsed, double optimalBlockSize) const override
{
const int64_t timeElapsedSec = std::round(timeElapsed);
if (timeElapsedSec < 60)
return _P("1 sec", "%x sec", timeElapsedSec);
return utfTo<wxString>(formatTimeSpan(timeElapsedSec, false /*hourRequired*/));
}
};
}
template <class TopLevelDialog> //can be a wxFrame or wxDialog
class SyncProgressDialogImpl : public TopLevelDialog, public SyncProgressDialog
/* we need derivation, not composition:
1. SyncProgressDialogImpl IS a wxFrame/wxDialog
2. implement virtual ~wxFrame()
3. event handling below assumes lifetime is larger-equal than wxFrame's */
{
public:
SyncProgressDialogImpl(long style, //wxFrame/wxDialog style
const WindowLayout::Dimensions& dim,
const std::function<void()>& userRequestCancel,
const Statistics& syncStat,
wxFrame* parentFrame,
bool showProgress,
bool autoCloseDialog,
const std::vector<std::wstring>& jobNames,
time_t syncStartTime,
bool ignoreErrors,
size_t autoRetryCount,
PostSyncAction postSyncAction);
Result destroy(bool autoClose, bool restoreParentFrame, TaskResult syncResult, const SharedRef<const zen::ErrorLog>& log) override;
wxWindow* getWindowIfVisible() override { return this->IsShown() ? this : nullptr; }
//workaround macOS bug: if "this" is used as parent window for a modal dialog then this dialog will erroneously un-hide its parent!
void initNewPhase () override;
void notifyProgressChange() override;
void updateGui () override { updateProgressGui(true /*allowYield*/); }
bool getOptionIgnoreErrors() const override { return ignoreErrors_; }
void setOptionIgnoreErrors(bool ignoreErrors) override { ignoreErrors_ = ignoreErrors; updateStaticGui(); }
PostSyncAction getAndFreezePostSyncAction() const override
{
pnl_.m_choicePostSyncAction->Disable();
return enumPostSyncAction_.get();
}
bool getOptionAutoCloseDialog() const override { return pnl_.m_checkBoxAutoClose->GetValue(); }
void timerSetStatus(bool active) override
{
if (active)
stopWatch_.resume();
else
stopWatch_.pause();
}
bool timerIsRunning() const override { return !stopWatch_.isPaused(); }
std::chrono::milliseconds pauseAndGetTotalTime() override
{
stopWatch_.pause();
return std::chrono::duration_cast<std::chrono::milliseconds>(stopWatch_.elapsed());
}
private:
void onLocalKeyEvent (wxKeyEvent& event);
void onParentKeyEvent(wxKeyEvent& event);
void onPause (wxCommandEvent& event);
void onCancel (wxCommandEvent& event);
void onClose(wxCloseEvent& event);
void onIconize(wxIconizeEvent& event);
//void onToggleIgnoreErrors(wxCommandEvent& event) { updateStaticGui(); }
void showSummary(TaskResult syncResult, const SharedRef<const ErrorLog>& log);
void minimizeToTray();
void resumeFromSystray(bool userRequested);
void updateStaticGui();
void updateProgressGui(bool allowYield);
void setExternalStatus(const wxString& status, const wxString& progress); //progress may be empty!
SyncProgressPanelGenerated& pnl_; //wxPanel containing the GUI controls of *this
const TimeComp syncStartTime_;
const wxString jobName_;
StopWatch stopWatch_;
wxFrame* parentFrame_; //optional
const std::function<void()> userRequestAbort_; //cancel button or dialog close
//status variables
const Statistics* syncStat_; //valid only while sync is running
bool paused_ = false;
bool closePressed_ = false;
//remaining time
SpeedTest remTimeTest_{PERF_WINDOW_REMAINING_TIME};
SpeedTest speedTest_ {PERF_WINDOW_BYTES_PER_SEC};
std::chrono::nanoseconds timeLastSpeedEstimate_ = std::chrono::seconds(-100); //used for calculating intervals between collecting perf samples
std::chrono::nanoseconds timeLastGraphTotalUpdate_ = std::chrono::seconds(-100);
//help calculate total speed
std::chrono::nanoseconds phaseStart_{}; //begin of current phase
SharedRef<CurveDataStatistics> curveBytes_ = makeSharedRef<CurveDataStatistics>();
SharedRef<CurveDataStatistics> curveItems_ = makeSharedRef<CurveDataStatistics>();
SharedRef<CurveDataEstimate > curveBytesEstim_ = makeSharedRef<CurveDataEstimate >();
SharedRef<CurveDataEstimate > curveItemsEstim_ = makeSharedRef<CurveDataEstimate >();
SharedRef<CurveDataTimeMarker> curveBytesTimeNow_ = makeSharedRef<CurveDataTimeMarker>();
SharedRef<CurveDataTimeMarker> curveItemsTimeNow_ = makeSharedRef<CurveDataTimeMarker>();
SharedRef<CurveDataTimeMarker> curveBytesTimeEstim_ = makeSharedRef<CurveDataTimeMarker>();
SharedRef<CurveDataTimeMarker> curveItemsTimeEstim_ = makeSharedRef<CurveDataTimeMarker>();
const wxColor colorBytesRim_ = enhanceContrast(getColorBytes(),
wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text
const wxColor colorItemsRim_ = enhanceContrast(getColorItems(),
wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text
const wxColor colorEstimRim_ = enhanceContrast(getColorEstimate(),
wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text
const wxColor colorBytesNow_ = enhanceContrast(getColorBytes(), getColorEstimate(), 4.5 /*contrastRatioMin*/);
const wxColor colorItemsNow_ = enhanceContrast(getColorItems(), getColorEstimate(), 4.5 /*contrastRatioMin*/);
wxString parentTitleBackup_;
std::optional<FfsTrayIcon> trayIcon_; //optional: if filled all other windows should be hidden and conversely
std::optional<Taskbar> taskbar_;
bool ignoreErrors_ = false;
EnumDescrList<PostSyncAction> enumPostSyncAction_
{
*pnl_.m_choicePostSyncAction, [this]
{
std::vector<EnumDescrList<PostSyncAction>::DescrItem> descr;
descr.push_back({PostSyncAction::none, L"", {}});
if (parentFrame_) //enable EXIT option for gui mode sync
descr.push_back({PostSyncAction::exit, wxControl::RemoveMnemonics(_("E&xit")), {}});
descr.push_back({PostSyncAction::sleep, _("System: Sleep"), {}});
descr.push_back({PostSyncAction::shutdown, _("System: Shut down"), {}});
return descr;
}()
};
};
template <class TopLevelDialog>
SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxFrame/wxDialog style
const WindowLayout::Dimensions& dim,
const std::function<void()>& userRequestCancel,
const Statistics& syncStat,
wxFrame* parentFrame,
bool showProgress,
bool autoCloseDialog,
const std::vector<std::wstring>& jobNames,
time_t syncStartTime,
bool ignoreErrors,
size_t autoRetryCount,
PostSyncAction postSyncAction) :
TopLevelDialog(parentFrame, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, style), //title is overwritten anyway in setExternalStatus()
pnl_(*new SyncProgressPanelGenerated(this)), //ownership passed to "this"
syncStartTime_(getLocalTime(syncStartTime)), //returns TimeComp() on error
jobName_([&]
{
std::wstring tmp;
if (!jobNames.empty())
{
tmp = jobNames[0];
std::for_each(jobNames.begin() + 1, jobNames.end(), [&](const std::wstring& jobName)
{ tmp += L" + " + jobName; });
}
return tmp;
}
()),
parentFrame_(parentFrame),
userRequestAbort_(userRequestCancel),
syncStat_(&syncStat)
{
static_assert(std::is_same_v<TopLevelDialog, wxFrame > ||
std::is_same_v<TopLevelDialog, wxDialog>);
assert((std::is_same_v<TopLevelDialog, wxFrame> == !parentFrame));
//finish construction of this dialog:
this->pnl_.m_panelProgress->SetMinSize({dipToWxsize(550), dipToWxsize(340)});
wxBoxSizer* bSizer170 = new wxBoxSizer(wxVERTICAL);
bSizer170->Add(&pnl_, 1, wxEXPAND);
this->SetSizer(bSizer170); //pass ownership
//lifetime of event sources is subset of this instance's lifetime => no wxEvtHandler::Unbind() needed
this->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& event) { onClose(event); });
this->Bind(wxEVT_ICONIZE, [this](wxIconizeEvent& event) { onIconize(event); });
this->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); });
pnl_.m_buttonClose ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { closePressed_ = true; });
pnl_.m_buttonPause ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onPause(event); });
pnl_.m_buttonStop ->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { onCancel(event); });
pnl_.m_bpButtonMinimizeToTray->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](wxCommandEvent& event) { minimizeToTray(); });
if (parentFrame_)
parentFrame_->Bind(wxEVT_CHAR_HOOK, &SyncProgressDialogImpl::onParentKeyEvent, this);
assert(pnl_.m_buttonClose->GetId() == wxID_OK); //we cannot use wxID_CLOSE else ESC key won't work: yet another wxWidgets bug??
setRelativeFontSize(*pnl_.m_staticTextPhase, 1.5);
setRelativeFontSize(*pnl_.m_staticTextPercentTotal, 1.5);
if (parentFrame_)
parentTitleBackup_ = parentFrame_->GetTitle(); //save old title (will be used as progress indicator)
//pnl.m_animCtrlSyncing->SetAnimation(getResourceAnimation(L"working"));
//pnl.m_animCtrlSyncing->Play();
//this->EnableCloseButton(false); //this is NOT honored on OS X or with ALT+F4 on Windows! -> why disable button at all??
try //try to get access to Windows 7/Ubuntu taskbar
{
taskbar_.emplace(this); //throw TaskbarNotAvailable
}
catch (const TaskbarNotAvailable&) {}
//hide until end of process:
pnl_.m_notebookResult ->Hide();
pnl_.m_buttonClose ->Show(false);
//set std order after button visibility was set
setStandardButtonLayout(*pnl_.bSizerStdButtons, StdButtons().setAffirmative(pnl_.m_buttonPause).setCancel(pnl_.m_buttonStop));
setImage(*pnl_.m_bpButtonMinimizeToTray, loadImage("minimize_to_tray"));
setImage(*pnl_.m_bitmapItemStat, IconBuffer::genericFileIcon(IconBuffer::IconSize::small));
setImage(*pnl_.m_bitmapTimeStat, loadImage("time", -1 /*maxWidth*/, IconBuffer::getPixSize(IconBuffer::IconSize::small)));
pnl_.m_bitmapTimeStat->SetMinSize({-1, screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small))});
setImage(*pnl_.m_bitmapErrors, loadImage("msg_error", dipToScreen(getMenuIconDipSize())));
setImage(*pnl_.m_bitmapWarnings, loadImage("msg_warning", dipToScreen(getMenuIconDipSize())));
setImage(*pnl_.m_bitmapIgnoreErrors, loadImage("error_ignore_active", dipToScreen(getMenuIconDipSize())));
setImage(*pnl_.m_bitmapRetryErrors, loadImage("error_retry", dipToScreen(getMenuIconDipSize())));
//init graph
const int xLabelHeight = this->GetCharHeight() + dipToWxsize(2) /*margin*/; //use same height for both graphs to make sure they stretch evenly
const int yLabelWidth = dipToWxsize(70);
pnl_.m_panelGraphBytes->setAttributes(Graph2D::MainAttributes().
setLabelX(XLabelPos::top, xLabelHeight, std::make_shared<LabelFormatterTimeElapsed>()).
setLabelY(YLabelPos::right, yLabelWidth, std::make_shared<LabelFormatterBytes>()).
setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)).
setSelectionMode(GraphSelMode::none));
pnl_.m_panelGraphItems->setAttributes(Graph2D::MainAttributes().
setLabelX(XLabelPos::bottom, xLabelHeight, std::make_shared<LabelFormatterTimeElapsed>()).
setLabelY(YLabelPos::right, yLabelWidth, std::make_shared<LabelFormatterItemCount>()).
setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)).
setSelectionMode(GraphSelMode::none));
pnl_.m_panelGraphBytes->addCurve(curveBytes_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorBytes()).setColor(colorBytesRim_));
pnl_.m_panelGraphItems->addCurve(curveItems_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorItems()).setColor(colorItemsRim_));
pnl_.m_panelGraphBytes->addCurve(curveBytesEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorEstimate()).setColor(colorEstimRim_));
pnl_.m_panelGraphItems->addCurve(curveItemsEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(1)).fillCurveArea(getColorEstimate()).setColor(colorEstimRim_));
pnl_.m_panelGraphBytes->addCurve(curveBytesTimeNow_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorBytesNow_));
pnl_.m_panelGraphItems->addCurve(curveItemsTimeNow_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorItemsNow_));
pnl_.m_panelGraphBytes->addCurve(curveBytesTimeEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorEstimRim_));
pnl_.m_panelGraphItems->addCurve(curveItemsTimeEstim_, Graph2D::CurveAttributes().setLineWidth(dipToWxsize(2)).setColor(colorEstimRim_));
//graph legend:
const wxSize squareSize{this->GetCharHeight(), this->GetCharHeight()};
setImage(*pnl_.m_bitmapGraphKeyBytes, rectangleImage({wxsizeToScreen(squareSize.x), wxsizeToScreen(squareSize.y)}, getColorBytes(), colorBytesRim_, dipToScreen(1)));
setImage(*pnl_.m_bitmapGraphKeyItems, rectangleImage({wxsizeToScreen(squareSize.x), wxsizeToScreen(squareSize.y)}, getColorItems(), colorItemsRim_, dipToScreen(1)));
pnl_.bSizerDynSpace->SetMinSize(yLabelWidth, -1); //ensure item/time stats are nicely centered
setText(*pnl_.m_staticTextRetryCount, L'(' + formatNumber(autoRetryCount) + MULT_SIGN + L')');
pnl_.bSizerErrorsRetry->Show(autoRetryCount > 0);
//allow changing a few options dynamically during sync
ignoreErrors_ = ignoreErrors;
enumPostSyncAction_.set(postSyncAction);
pnl_.m_checkBoxAutoClose->SetValue(autoCloseDialog);
updateStaticGui(); //null-status will be shown while waiting for dir locks
//make sure that standard height matches ProcessPhase::binaryCompare statistics layout (== largest)
this->GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize()
#ifdef __WXGTK3__
this->Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088
//Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404
#endif
pnl_.Layout();
this->Center(); //call *after* dialog layout update and *before* wxWindow::Show()!
WindowLayout::setInitial(*this, dim, this->GetSize() /*defaultSize*/);
pnl_.m_buttonStop->SetDefault();
if (showProgress)
{
this->Show();
//clear gui flicker, remove dummy texts: window must be visible to make this work!
updateProgressGui(true /*allowYield*/); //at least on OS X a real Yield() is required to flush pending GUI updates; Update() is not enough
setFocusIfActive(*pnl_.m_buttonStop); //don't steal focus when starting in sys-tray!
}
else
minimizeToTray();
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::onLocalKeyEvent(wxKeyEvent& event)
{
switch (event.GetKeyCode())
{
case WXK_ESCAPE:
{
wxButton& activeButton = pnl_.m_buttonStop->IsShown() ? *pnl_.m_buttonStop : *pnl_.m_buttonClose;
wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED);
activeButton.Command(dummy); //simulate click
return;
}
}
event.Skip();
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::onParentKeyEvent(wxKeyEvent& event)
{
//redirect keys from main dialog to progress dialog
switch (event.GetKeyCode())
{
case WXK_ESCAPE:
this->SetFocus();
this->onLocalKeyEvent(event); //event will be handled => no event recursion to parent dialog!
return;
}
event.Skip();
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::initNewPhase()
{
updateStaticGui(); //evaluates "syncStat_->currentPhase()"
//reset graphs (e.g. after binary comparison)
curveBytes_ .ref().clear();
curveItems_ .ref().clear();
curveBytesEstim_ .ref().setValue(0, 0, 0, 0);
curveItemsEstim_ .ref().setValue(0, 0, 0, 0);
curveBytesTimeNow_ .ref().setValue(0, 0);
curveItemsTimeNow_ .ref().setValue(0, 0);
curveBytesTimeEstim_.ref().setValue(0, 0);
curveItemsTimeEstim_.ref().setValue(0, 0);
notifyProgressChange(); //make sure graphs get initial values
//start new measurement
remTimeTest_.clear();
speedTest_ .clear();
timeLastGraphTotalUpdate_ = timeLastSpeedEstimate_ = std::chrono::seconds(-100); //make sure estimate is updated upon next check
phaseStart_ = stopWatch_.elapsed();
updateProgressGui(false /*allowYield*/);
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::notifyProgressChange() //noexcept!
{
if (syncStat_) //sync running
{
const double timeElapsedDouble = std::chrono::duration<double>(stopWatch_.elapsed()).count();
const ProgressStats stats = syncStat_->getCurrentStats();
curveBytes_.ref().addSample(timeElapsedDouble, stats.bytes);
curveItems_.ref().addSample(timeElapsedDouble, stats.items);
}
}
namespace
{
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::setExternalStatus(const wxString& status, const wxString& progress) //progress may be empty!
{
//sys tray: order "top-down": jobname, status, progress
wxString tooltip = L"FreeFileSync";
if (!jobName_.empty())
tooltip += SPACED_DASH + jobName_;
tooltip += L'\n' + status;
if (!progress.empty())
tooltip += L' ' + progress;
//window caption/taskbar; inverse order: progress, status, jobname
wxString title;
if (!progress.empty())
title += progress + L' ';
title += status;
if (!jobName_.empty() && !parentFrame_ /*job name already visible in sync config panel, unlike with batch jobs*/)
title += SPACED_DASH + jobName_;
#if 0 //why again does start time have to be visible in the title!?
const Zchar* format = [&tc = syncStartTime_]
{
if (const TimeComp& tcNow = getLocalTime();
tc.day == tcNow.day &&
tc.month == tcNow.month &&
tc.year == tcNow.year)
return formatTimeTag;
return formatDateTimeTag;
}();
title += SPACED_DASH + utfTo<std::wstring>(formatTime(format, syncStartTime_));
#endif
//---------------------------------------------------------------------------
//systray tooltip, if window is minimized
if (trayIcon_)
trayIcon_->setToolTip(tooltip);
//top level dialog title also shows in Windows taskbar!
if (parentFrame_)
{
if (parentFrame_->GetTitle() != title)
parentFrame_->SetTitle(title);
}
else if (this->GetTitle() != title)
this->SetTitle(title);
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield)
{
assert(syncStat_);
if (!syncStat_) //sync not running!?
return;
//normally we don't update the "static" GUI components here, but we have to make an exception
//if sync is cancelled (by user or error handling option)
if (syncStat_->taskCancelled())
updateStaticGui(); //called more than once after cancel... ok
const std::chrono::nanoseconds timeElapsed = stopWatch_.elapsed();
const double timeElapsedDouble = std::chrono::duration<double>(timeElapsed).count();
const int itemsCurrent = syncStat_->getCurrentStats().items;
const int64_t bytesCurrent = syncStat_->getCurrentStats().bytes;
const int itemsTotal = syncStat_->getTotalStats ().items;
const int64_t bytesTotal = syncStat_->getTotalStats ().bytes;
const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0;
bool headerLayoutChanged = false;
//status texts
setText(*pnl_.m_staticTextStatus, replaceCpy(syncStat_->currentStatusText(), L'\n', L' ')); //no layout update for status texts!
if (!haveTotalStats)
{
//dialog caption, taskbar, systray tooltip
setExternalStatus(getDialogPhaseText(*syncStat_, paused_), formatNumber(itemsCurrent)); //status text may be "paused"!
//progress indicators
setText(*pnl_.m_staticTextPercentTotal, L"", &headerLayoutChanged);
if (trayIcon_) trayIcon_->setProgress(1); //100% = fully visible FFS logo
//taskbar_ already set to STATUS_INDETERMINATE by initNewPhase()
}
else
{
//dialog caption, taskbar, systray tooltip
const double fractionTotal = bytesTotal + itemsTotal == 0 ? 0 : 1.0 * (bytesCurrent + itemsCurrent) / (bytesTotal + itemsTotal);
//add both data + obj-count, to handle "deletion-only" cases
const std::wstring percentTotal = formatProgressPercent(fractionTotal);
setExternalStatus(getDialogPhaseText(*syncStat_, paused_), percentTotal); //status text may be "paused"!
//progress indicators
setText(*pnl_.m_staticTextPercentTotal, L' ' + percentTotal, &headerLayoutChanged);
if (trayIcon_) trayIcon_->setProgress(fractionTotal);
if (taskbar_ ) taskbar_ ->setProgress(fractionTotal);
const double timeTotalSecTentative = bytesCurrent == bytesTotal ? timeElapsedDouble : std::max(curveBytesEstim_.ref().getTotalTime(), timeElapsedDouble);
curveBytesEstim_.ref().setValue(timeElapsedDouble, timeTotalSecTentative, bytesCurrent, bytesTotal);
curveItemsEstim_.ref().setValue(timeElapsedDouble, timeTotalSecTentative, itemsCurrent, itemsTotal);
//tentatively update total time, may be improved on below:
curveBytesTimeNow_.ref().setValue(timeElapsedDouble, bytesCurrent);
curveItemsTimeNow_.ref().setValue(timeElapsedDouble, itemsCurrent);
curveBytesTimeEstim_.ref().setValue(timeTotalSecTentative, bytesTotal);
curveItemsTimeEstim_.ref().setValue(timeTotalSecTentative, itemsTotal);
}
//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!
curveBytes_.ref().addSample(timeElapsedDouble, bytesCurrent);
curveItems_.ref().addSample(timeElapsedDouble, itemsCurrent);
bool layoutChanged = false; //avoid screen flicker by calling layout() only if necessary
auto showIfNeeded = [&](wxWindow& wnd, bool show)
{
if (wnd.IsShown() != show)
{
wnd.Show(show);
layoutChanged = true;
}
};
//item and data stats
if (!haveTotalStats)
{
setText(*pnl_.m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged);
setText(*pnl_.m_staticTextBytesProcessed, L"", &layoutChanged);
setText(*pnl_.m_staticTextItemsRemaining, std::wstring(1, EM_DASH), &layoutChanged);
setText(*pnl_.m_staticTextBytesRemaining, L"", &layoutChanged);
}
else
{
setText(*pnl_.m_staticTextItemsProcessed, formatNumber(itemsCurrent), &layoutChanged);
setText(*pnl_.m_staticTextBytesProcessed, L'(' + formatFilesizeShort(bytesCurrent) + L')', &layoutChanged);
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!
}
//errors and warnings (pop up dynamically)
const Statistics::ErrorStats errorStats = syncStat_->getErrorStats();
showIfNeeded(*pnl_.m_staticTextErrors, errorStats.errorCount != 0);
showIfNeeded(*pnl_.m_staticTextWarnings, errorStats.warningCount != 0);
showIfNeeded(*pnl_.m_panelErrorStats, errorStats.errorCount != 0 || errorStats.warningCount != 0);
if (pnl_.m_panelErrorStats->IsShown())
{
showIfNeeded(*pnl_.m_bitmapErrors, errorStats.errorCount != 0);
showIfNeeded(*pnl_.m_staticTextErrorCount, errorStats.errorCount != 0);
if (pnl_.m_staticTextErrorCount->IsShown())
setText(*pnl_.m_staticTextErrorCount, formatNumber(errorStats.errorCount), &layoutChanged);
showIfNeeded(*pnl_.m_bitmapWarnings, errorStats.warningCount != 0);
showIfNeeded(*pnl_.m_staticTextWarningCount, errorStats.warningCount != 0);
if (pnl_.m_staticTextWarningCount->IsShown())
setText(*pnl_.m_staticTextWarningCount, formatNumber(errorStats.warningCount), &layoutChanged);
}
//current time elapsed
const int64_t timeElapSec = std::chrono::duration_cast<std::chrono::seconds>(timeElapsed).count();
setText(*pnl_.m_staticTextTimeElapsed, utfTo<wxString>(formatTimeSpan(timeElapSec, false /*hourRequired*/)), &layoutChanged);
//remaining time and speed
if (numeric::dist(timeLastSpeedEstimate_, timeElapsed) >= SPEED_ESTIMATE_UPDATE_INTERVAL)
{
timeLastSpeedEstimate_ = timeElapsed;
if (numeric::dist(phaseStart_, timeElapsed) >= SPEED_ESTIMATE_SAMPLE_SKIP) //discard stats for first second: probably messy
{
remTimeTest_.addSample(timeElapsed, itemsCurrent, bytesCurrent);
speedTest_ .addSample(timeElapsed, itemsCurrent, bytesCurrent);
}
//current speed -> Win 7 copy uses 1 sec update interval instead
pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(speedTest_.getBytesPerSecFmt(), GraphCorner::topL));
pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(speedTest_.getItemsPerSecFmt(), GraphCorner::topL));
//remaining time
if (!haveTotalStats)
{
setText(*pnl_.m_staticTextTimeRemaining, std::wstring(1, EM_DASH), &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
std::optional<double> remTimeSec = remTimeTest_.getRemainingSec(itemsTotal - itemsCurrent, bytesTotal - bytesCurrent);
setText(*pnl_.m_staticTextTimeRemaining, remTimeSec ? formatRemainingTime(*remTimeSec) : std::wstring(1, EM_DASH), &layoutChanged);
const double timeRemainingSec = remTimeSec ? *remTimeSec : 0;
const double timeTotalSec = timeElapsedDouble + timeRemainingSec;
//update estimated total time marker only with precision of "20% remaining time" to avoid needless jumping around:
if (numeric::dist(curveBytesEstim_.ref().getTotalTime(), timeTotalSec) > 0.2 * timeRemainingSec)
{
//avoid needless flicker and don't update total time graph too often:
static_assert(std::chrono::duration_cast<std::chrono::milliseconds>(GRAPH_TOTAL_TIME_UPDATE_INTERVAL).count() % SPEED_ESTIMATE_UPDATE_INTERVAL.count() == 0);
if (numeric::dist(timeLastGraphTotalUpdate_, timeElapsed) >= GRAPH_TOTAL_TIME_UPDATE_INTERVAL)
{
timeLastGraphTotalUpdate_ = timeElapsed;
curveBytesEstim_.ref().setTotalTime(timeTotalSec);
curveItemsEstim_.ref().setTotalTime(timeTotalSec);
curveBytesTimeEstim_.ref().setTime(timeTotalSec);
curveItemsTimeEstim_.ref().setTime(timeTotalSec);
}
}
}
}
pnl_.m_panelGraphBytes->Refresh();
pnl_.m_panelGraphItems->Refresh();
//adapt layout after content changes above
if (headerLayoutChanged)
pnl_.Layout();
if (layoutChanged)
{
pnl_.m_panelProgress->Layout();
//small statistics panels:
pnl_.m_panelItemStats->Layout();
pnl_.m_panelTimeStats->Layout();
if (pnl_.m_panelErrorStats->IsShown())
pnl_.m_panelErrorStats->Layout();
}
if (allowYield)
{
if (paused_) //support for pause button
{
PauseTimers dummy(*this);
while (paused_)
{
wxTheApp->Yield(); //receive UI message that ends pause
//*first* refresh GUI (removing flicker) before sleeping!
std::this_thread::sleep_for(UI_UPDATE_INTERVAL);
}
}
else
/*
/|\
| keep this sequence to ensure one full progress update before entering pause mode!
\|/
*/
wxTheApp->Yield(); //receive UI message that sets pause status OR forceful termination!
}
else
this->Update(); //don't wait until next idle event (who knows what blocking process comes next?)
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::updateStaticGui() //depends on "syncStat_, paused_"
{
assert(syncStat_);
if (!syncStat_)
return;
pnl_.m_staticTextPhase->SetLabelText(getDialogPhaseText(*syncStat_, paused_));
//pnl_.m_bitmapStatus->SetToolTip(); -> redundant
const wxImage statusImage = [&]
{
if (paused_)
return loadImage("status_pause");
if (syncStat_->taskCancelled())
return loadImage("result_error");
switch (syncStat_->currentPhase())
{
case ProcessPhase::none:
case ProcessPhase::scan:
return loadImage("status_scanning");
case ProcessPhase::binaryCompare:
return loadImage("status_binary_compare");
case ProcessPhase::sync:
return loadImage("status_syncing");
}
assert(false);
return wxNullImage;
}();
setImage(*pnl_.m_bitmapStatus, statusImage);
//show status on Windows 7 taskbar
if (taskbar_)
{
if (paused_)
taskbar_->setStatus(Taskbar::Status::paused);
else
{
const int itemsTotal = syncStat_->getTotalStats().items;
const int64_t bytesTotal = syncStat_->getTotalStats().bytes;
const bool haveTotalStats = itemsTotal >= 0 || bytesTotal >= 0;
taskbar_->setStatus(haveTotalStats ? Taskbar::Status::normal : Taskbar::Status::indeterminate);
}
}
//pause button
pnl_.m_buttonPause->SetLabel(paused_ ? _("&Continue") : _("&Pause"));
pnl_.bSizerErrorsIgnore->Show(ignoreErrors_);
pnl_.Layout();
pnl_.m_panelProgress->Layout(); //for bSizerErrorsIgnore
//this->Refresh(); //a few pixels below the status text need refreshing -> still needed?
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::showSummary(TaskResult syncResult, const SharedRef<const ErrorLog>& log)
{
assert(syncStat_);
//at the LATEST(!) to prevent access to currentStatusHandler
//enable okay and close events; may be set in this method ONLY
paused_ = false; //you never know?
//update numbers one last time (as if sync were still running)
notifyProgressChange(); //make one last graph entry at the *current* time
updateProgressGui(false /*allowYield*/);
//===================================================================================
const int itemsProcessed = syncStat_->getCurrentStats().items;
const int64_t bytesProcessed = syncStat_->getCurrentStats().bytes;
const int itemsTotal = syncStat_->getTotalStats ().items;
const int64_t bytesTotal = syncStat_->getTotalStats ().bytes;
//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() :
replaceCpy(_("%x/sec"), L"%x", formatFilesizeShort(std::round(bytesProcessed / timeDelta)));
const wxString overallItemsPerSecond = numeric::isNull(timeDelta) ? std::wstring() :
replaceCpy(_("%x/sec"), L"%x", replaceCpy(_("%x items"), L"%x", formatThreeDigitPrecision(itemsProcessed / timeDelta)));
pnl_.m_panelGraphBytes->setAttributes(pnl_.m_panelGraphBytes->getAttributes().setCornerText(overallBytesPerSecond, GraphCorner::topL));
pnl_.m_panelGraphItems->setAttributes(pnl_.m_panelGraphItems->getAttributes().setCornerText(overallItemsPerSecond, GraphCorner::topL));
//...if everything was processed successfully
if (itemsTotal >= 0 && bytesTotal >= 0 && //itemsTotal < 0 && bytesTotal < 0 => e.g. cancel during folder comparison
itemsProcessed == itemsTotal &&
bytesProcessed == bytesTotal)
{
pnl_.m_staticTextPercentTotal->Hide();
pnl_.m_staticTextProcessed ->Hide();
pnl_.m_staticTextRemaining ->Hide();
pnl_.m_staticTextItemsRemaining->Hide();
pnl_.m_staticTextBytesRemaining->Hide();
pnl_.m_staticTextTimeRemaining ->Hide();
}
//generally not interesting anymore (e.g. items > 0 due to skipped errors)
pnl_.m_staticTextTimeRemaining->Hide();
const int64_t totalTimeSec = std::chrono::duration_cast<std::chrono::seconds>(stopWatch_.elapsed()).count();
pnl_.m_staticTextTimeElapsed->SetLabelText(utfTo<wxString>(formatTimeSpan(totalTimeSec)));
//hourOptional? -> let's use full precision for max. clarity: https://freefilesync.org/forum/viewtopic.php?t=6308
resumeFromSystray(false /*userRequested*/); //if in tray mode...
//------- change class state -------
syncStat_ = nullptr;
//----------------------------------
const wxImage statusImage = [&]
{
switch (syncResult)
{
case TaskResult::success:
return loadImage("result_success");
case TaskResult::warning:
return loadImage("result_warning");
case TaskResult::error:
case TaskResult::cancelled:
return loadImage("result_error");
}
assert(false);
return wxNullImage;
}();
setImage(*pnl_.m_bitmapStatus, statusImage);
pnl_.m_staticTextPhase->SetLabelText(getSyncResultLabel(syncResult));
//pnl_.m_bitmapStatus->SetToolTip(); -> redundant
//show status on Windows 7 taskbar
if (taskbar_)
switch (syncResult)
{
case TaskResult::success:
taskbar_->setStatus(Taskbar::Status::normal);
break;
case TaskResult::warning:
taskbar_->setStatus(Taskbar::Status::warning);
break;
case TaskResult::error:
case TaskResult::cancelled:
taskbar_->setStatus(Taskbar::Status::error);
break;
}
//----------------------------------
setExternalStatus(getSyncResultLabel(syncResult), wxString());
//this->EnableCloseButton(true);
pnl_.m_bpButtonMinimizeToTray->Hide();
pnl_.m_buttonStop->Disable();
pnl_.m_buttonStop->Hide();
pnl_.m_buttonPause->Disable();
pnl_.m_buttonPause->Hide();
pnl_.m_buttonClose->Show();
pnl_.m_buttonClose->Enable();
pnl_.bSizerProgressFooter->Show(false);
if (!parentFrame_) //hide checkbox for batch mode sync (where value won't be retrieved after close)
pnl_.m_checkBoxAutoClose->Hide();
//set std order after button visibility was set
setStandardButtonLayout(*pnl_.bSizerStdButtons, StdButtons().setAffirmative(pnl_.m_buttonClose));
//hide current operation status
pnl_.bSizerStatusText->Show(false);
pnl_.m_staticlineFooter->Hide(); //win: m_notebookResult already has a window frame
//-------------------------------------------------------------
pnl_.m_notebookResult->SetPadding(wxSize(dipToWxsize(2), 0)); //height cannot be changed
//1. re-arrange graph into results listbook
const size_t pagePosProgress = 0;
const size_t pagePosLog = 1;
[[maybe_unused]] const bool wasDetached = pnl_.bSizerRoot->Detach(pnl_.m_panelProgress);
assert(wasDetached);
pnl_.m_panelProgress->Reparent(pnl_.m_notebookResult);
pnl_.m_notebookResult->AddPage(pnl_.m_panelProgress, _("Progress"), true /*bSelect*/);
//2. log file
assert(pnl_.m_notebookResult->GetPageCount() == 1);
LogPanel* logPanel = new LogPanel(pnl_.m_notebookResult); //owned by m_notebookResult
logPanel->setLog(log.ptr());
pnl_.m_notebookResult->AddPage(logPanel, _("Log"), false /*bSelect*/);
//show log instead of graph if errors occurred! (not required for ignored warnings)
const ErrorLogStats logCount = getStats(log.ref());
if (logCount.errors > 0)
pnl_.m_notebookResult->ChangeSelection(pagePosLog);
//fill image list to cope with wxNotebook image setting design desaster...
const int imgListSize = dipToWxsize(16); //also required by GTK => don't use getMenuIconDipSize()
auto imgList = std::make_unique<wxImageList>(imgListSize, imgListSize);
imgList->Add(toScaledBitmap(loadImage("progress", wxsizeToScreen(imgListSize))));
imgList->Add(toScaledBitmap(loadImage("log_file", wxsizeToScreen(imgListSize))));
pnl_.m_notebookResult->AssignImageList(imgList.release()); //pass ownership
pnl_.m_notebookResult->SetPageImage(pagePosProgress, pagePosProgress);
pnl_.m_notebookResult->SetPageImage(pagePosLog, pagePosLog);
//Caveat: we need "Show()" *after" the above wxNotebook::ChangeSelection() to get the correct selection on Linux
pnl_.m_notebookResult->Show();
//GetSizer()->SetSizeHints(this); //~=Fit() //not a good idea: will shrink even if window is maximized or was enlarged by the user
pnl_.Layout();
pnl_.m_panelProgress->Layout();
//small statistics panels:
pnl_.m_panelItemStats->Layout();
pnl_.m_panelTimeStats->Layout();
if (pnl_.m_panelErrorStats->IsShown())
pnl_.m_panelErrorStats->Layout();
//this->Raise(); -> don't! user may be watching a movie in the meantime ;)
pnl_.m_buttonClose->SetDefault();
setFocusIfActive(*pnl_.m_buttonClose);
}
template <class TopLevelDialog>
auto SyncProgressDialogImpl<TopLevelDialog>::destroy(bool autoClose, bool restoreParentFrame, TaskResult syncResult, const SharedRef<const ErrorLog>& log) -> Result
{
assert(stopWatch_.isPaused()); //why wasn't pauseAndGetTotalTime() called?
if (autoClose)
{
assert(syncStat_);
//ATTENTION: dialog may live a little longer, so watch callbacks!
//e.g. wxGTK calls onIconize after wxWindow::Close() (better not ask why) and before physical destruction! => indirectly calls updateStaticGui(), which reads syncStat_!!!
syncStat_ = nullptr;
}
else
{
showSummary(syncResult, log);
//wait until user closes the dialog by pressing "Close"
while (!closePressed_)
{
wxTheApp->Yield(); //refresh GUI *first* before sleeping! (remove flicker)
std::this_thread::sleep_for(UI_UPDATE_INTERVAL);
}
restoreParentFrame = true;
}
//------------------------------------------------------------------------
if (parentFrame_)
{
[[maybe_unused]] bool ubOk = parentFrame_->Unbind(wxEVT_CHAR_HOOK, &SyncProgressDialogImpl::onParentKeyEvent, this);
assert(ubOk);
parentFrame_->SetTitle(parentTitleBackup_); //restore title text
if (restoreParentFrame)
{
//make sure main dialog is shown again if still "minimized to systray"!
parentFrame_->Show();
//if (parentFrame_->IsIconized()) //caveat: if window is maximized calling Iconize(false) will erroneously un-maximize!
// parentFrame_->Iconize(false);
}
}
//else: don't call transformAppType(): consider "switch to main dialog" option during silent batch run
//------------------------------------------------------------------------
const bool autoCloseDialog = getOptionAutoCloseDialog();
const WindowLayout::Dimensions dims = WindowLayout::getBeforeClose(*this);
this->Destroy(); //wxWidgets macOS: simple "delete"!!!!!!!
return {autoCloseDialog, dims};
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::onClose(wxCloseEvent& event)
{
assert(event.CanVeto()); //this better be true: if "this" is parent of a modal error dialog, there is NO way (in hell) we allow destruction here!!!
//wxEVT_END_SESSION is already handled by application.cpp::onSystemShutdown()!
event.Veto();
closePressed_ = true; //"temporary" auto-close: preempt closing results dialog
if (syncStat_)
{
//user closing dialog => cancel sync + auto-close dialog
userRequestAbort_();
paused_ = false; //[!] we could be pausing here!
updateStaticGui(); //update status + pause button
}
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::onCancel(wxCommandEvent& event)
{
userRequestAbort_();
paused_ = false;
updateStaticGui(); //update status + pause button
//no UI-update here to avoid cascaded Yield()-call!
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::onPause(wxCommandEvent& event)
{
paused_ = !paused_;
updateStaticGui(); //update status + pause button
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::onIconize(wxIconizeEvent& event)
{
/* propagate progress dialog minimize/maximize to parent
-----------------------------------------------------
Fedora/Debian/Ubuntu:
- wxDialog cannot be minimized
- worse, wxGTK sends stray iconize events *after* wxDialog::Destroy()
- worse, on Fedora an iconize event is issued directly after calling Close()
- worse, even wxDialog::Hide() causes iconize event!
=> nothing to do
SUSE:
- wxDialog can be minimized (it just vanishes!) and in general also minimizes parent: except for our progress wxDialog!!!
- worse, wxDialog::Hide() causes iconize event
- probably the same issues with stray iconize events like Fedora/Debian/Ubuntu
- minimize button is always shown, even if wxMINIMIZE_BOX is omitted!
=> nothing to do
macOS:
- wxDialog can be minimized but does not also minimize parent
=> propagate event to parent
Windows:
- wxDialog can be minimized but does not also minimize parent
- iconize events only seen for manual minimize
=> propagate event to parent */
event.Skip();
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::minimizeToTray()
{
if (!trayIcon_)
{
trayIcon_.emplace([this] { this->resumeFromSystray(true /*userRequested*/); }); //FfsTrayIcon lifetime is a subset of "this"'s lifetime!
//we may destroy FfsTrayIcon even while in the FfsTrayIcon callback!!!!
updateProgressGui(false /*allowYield*/); //set tray tooltip + progress: e.g. no updates while paused
#warning("need delay for minimize animation to play out?")
//std::this_thread::sleep_for(std::chrono::milliseconds(100));
this->Hide();
if (parentFrame_)
parentFrame_->Hide();
}
}
template <class TopLevelDialog>
void SyncProgressDialogImpl<TopLevelDialog>::resumeFromSystray(bool userRequested)
{
if (trayIcon_)
{
trayIcon_.reset();
if (parentFrame_)
parentFrame_->Show();
this->Show();
updateStaticGui(); //restore Windows 7 task bar status (e.g. required in pause mode)
updateProgressGui(false /*allowYield*/); //restore Windows 7 task bar progress (e.g. required in pause mode)
if (userRequested)
{
if (parentFrame_)
parentFrame_->Raise();
this->Raise();
pnl_.m_bpButtonMinimizeToTray->SetFocus();
}
}
}
//########################################################################################
SyncProgressDialog* SyncProgressDialog::create(const WindowLayout::Dimensions& dim,
const std::function<void()>& userRequestCancel,
const Statistics& syncStat,
wxFrame* parentWindow, //may be nullptr
bool showProgress,
bool autoCloseDialog,
const std::vector<std::wstring>& jobNames,
time_t syncStartTime,
bool ignoreErrors,
size_t autoRetryCount,
PostSyncAction postSyncAction)
{
if (parentWindow) //FFS GUI sync
return new SyncProgressDialogImpl<wxDialog>(wxDEFAULT_DIALOG_STYLE | wxMAXIMIZE_BOX | wxMINIMIZE_BOX | wxRESIZE_BORDER,
dim, userRequestCancel, syncStat, parentWindow, showProgress,
autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction);
else //FFS batch job
{
auto dlg = new SyncProgressDialogImpl<wxFrame>(wxDEFAULT_FRAME_STYLE,
dim, userRequestCancel, syncStat, parentWindow, showProgress,
autoCloseDialog, jobNames, syncStartTime, ignoreErrors, autoRetryCount, postSyncAction);
dlg->SetIcon(getFfsIcon()); //only top level windows should have an icon
return dlg;
}
}