// ***************************************************************************** // * 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 "gui_status_handler.h" #include #include #include #include using namespace zen; using namespace fff; namespace { constexpr std::chrono::seconds TEMP_PANEL_DISPLAY_DELAY(1); } StatusHandlerTemporaryPanel::StatusHandlerTemporaryPanel(MainDialog& dlg, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, size_t autoRetryCount, std::chrono::seconds autoRetryDelay, const Zstring& soundFileAlertPending) : mainDlg_(dlg), ignoreErrors_(ignoreErrors), autoRetryCount_(autoRetryCount), autoRetryDelay_(autoRetryDelay), soundFileAlertPending_(soundFileAlertPending), startTime_(startTime) { mainDlg_.compareStatus_->init(*this, ignoreErrors_, autoRetryCount_); //clear old values before showing panel //showStatsPanel(); => delay and avoid GUI distraction for short-lived tasks mainDlg_.Update(); //don't wait until idle event! //register keys mainDlg_. Bind(wxEVT_CHAR_HOOK, &StatusHandlerTemporaryPanel::onLocalKeyEvent, this); mainDlg_.m_buttonCancel->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &StatusHandlerTemporaryPanel::onAbortCompare, this); } void StatusHandlerTemporaryPanel::showStatsPanel() { assert(!mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).IsShown()); { //------------------------------------------------------------------ const wxAuiPaneInfo& topPanel = mainDlg_.auiMgr_.GetPane(mainDlg_.m_panelTopButtons); wxAuiPaneInfo& statusPanel = mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()); //determine best status panel row near top panel switch (topPanel.dock_direction) { case wxAUI_DOCK_TOP: case wxAUI_DOCK_BOTTOM: statusPanel.Layer (topPanel.dock_layer); statusPanel.Direction(topPanel.dock_direction); statusPanel.Row (topPanel.dock_row + 1); break; case wxAUI_DOCK_LEFT: case wxAUI_DOCK_RIGHT: statusPanel.Layer (std::max(0, topPanel.dock_layer - 1)); statusPanel.Direction(wxAUI_DOCK_TOP); statusPanel.Row (0); break; //case wxAUI_DOCK_CENTRE: } const bool statusRowTaken = [&] { for (wxAuiPaneInfo& paneInfo : mainDlg_.auiMgr_.GetAllPanes()) //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 && paneInfo.dock_row == statusPanel.dock_row) return true; return false; }(); //move all rows that are in the way one step further if (statusRowTaken) for (wxAuiPaneInfo& paneInfo : mainDlg_.auiMgr_.GetAllPanes()) if (&paneInfo != &statusPanel && paneInfo.dock_layer == statusPanel.dock_layer && paneInfo.dock_direction == statusPanel.dock_direction && paneInfo.dock_row >= statusPanel.dock_row) ++paneInfo.dock_row; //------------------------------------------------------------------ statusPanel.Show(); mainDlg_.auiMgr_.Update(); mainDlg_.compareStatus_->getAsWindow()->Refresh(); //macOS: fix background corruption for the statistics boxes (call *after* wxAuiManager::Update() } } StatusHandlerTemporaryPanel::~StatusHandlerTemporaryPanel() { if (!errorLog_.empty()) //prepareResult() was not called! std::abort(); //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 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_.ProcessEvent(dummy); //trigger wxAuiManager::OnCaptureLost(); should be no-op if no mouse buttons are pressed if (wxWindow::GetCapture() == &mainDlg_) mainDlg_.ReleaseMouse(); mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).Hide(); mainDlg_.auiMgr_.Update(); //unregister keys [[maybe_unused]] bool ubOk1 = mainDlg_. Unbind(wxEVT_CHAR_HOOK, &StatusHandlerTemporaryPanel::onLocalKeyEvent, this); [[maybe_unused]] bool ubOk2 = mainDlg_.m_buttonCancel->Unbind(wxEVT_COMMAND_BUTTON_CLICKED, &StatusHandlerTemporaryPanel::onAbortCompare, this); assert(ubOk1 && ubOk2); mainDlg_.compareStatus_->teardown(); } StatusHandlerTemporaryPanel::Result StatusHandlerTemporaryPanel::prepareResult() //noexcept!! { const std::chrono::milliseconds totalTime = mainDlg_.compareStatus_->pauseAndGetTotalTime(); //append "extra" log for sync errors that could not otherwise be reported: if (const ErrorLog extraLog = fetchExtraLog(); !extraLog.empty()) { append(errorLog_, extraLog); std::stable_sort(errorLog_.begin(), errorLog_.end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; }); } //determine post-sync status irrespective of further errors during tear-down const TaskResult syncResult = [&] { if (taskCancelled()) { logMsg(errorLog_, _("Stopped"), MSG_TYPE_ERROR); //= user cancel return TaskResult::cancelled; } const ErrorLogStats logCount = getStats(errorLog_); if (logCount.errors > 0) return TaskResult::error; else if (logCount.warnings > 0) return TaskResult::warning; else return TaskResult::success; }(); const ProcessSummary summary { startTime_, syncResult, {} /*jobNames*/, getCurrentStats(), getTotalStats (), totalTime }; return {summary, makeSharedRef(std::exchange(errorLog_, {}))}; //see check in ~StatusHandlerTemporaryPanel() } void StatusHandlerTemporaryPanel::initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) { StatusHandler::initNewPhase(itemsTotal, bytesTotal, phaseID); mainDlg_.compareStatus_->initNewPhase(); //call after "StatusHandler::initNewPhase" //macOS needs a full yield to update GUI and get rid of "dummy" texts requestUiUpdate(true /*force*/); //throw CancelProcess } void StatusHandlerTemporaryPanel::logMessage(const std::wstring& msg, MsgType type) { logMsg(errorLog_, msg, [&] { switch (type) { case MsgType::info: return MSG_TYPE_INFO; case MsgType::warning: return MSG_TYPE_WARNING; case MsgType::error: return MSG_TYPE_ERROR; } assert(false); return MSG_TYPE_ERROR; }()); requestUiUpdate(false /*force*/); //throw CancelProcess } void StatusHandlerTemporaryPanel::reportWarning(const std::wstring& msg, bool& warningActive) { PauseTimers dummy(*mainDlg_.compareStatus_); logMsg(errorLog_, msg, MSG_TYPE_WARNING); if (!warningActive) //if errors are ignored, then warnings should also return; if (!mainDlg_.compareStatus_->getOptionIgnoreErrors()) { forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! bool dontWarnAgain = false; switch (showConfirmationDialog(&mainDlg_, DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(msg). alertWhenPending(soundFileAlertPending_). setCheckBox(dontWarnAgain, _("&Don't show this warning again")), _("&Ignore"))) { case ConfirmationButton::accept: warningActive = !dontWarnAgain; break; case ConfirmationButton::cancel: cancelProcessNow(CancelReason::user); //throw CancelProcess break; } } //else: if errors are ignored, then warnings should also } ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const ErrorInfo& errorInfo) { PauseTimers dummy(*mainDlg_.compareStatus_); //log actual fail time (not "now"!) const time_t failTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now() - std::chrono::duration_cast(std::chrono::steady_clock::now() - errorInfo.failTime)); //auto-retry if (errorInfo.retryNumber < autoRetryCount_) { logMsg(errorLog_, errorInfo.msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO, failTime); delayAndCountDown(errorInfo.failTime + autoRetryDelay_ - std::chrono::steady_clock::now(), [&, statusPrefix = _("Automatic retry") + (errorInfo.retryNumber == 0 ? L"" : L' ' + formatNumber(errorInfo.retryNumber + 1)) + SPACED_DASH, statusPostfix = SPACED_DASH + _("Error") + L": " + replaceCpy(errorInfo.msg, L'\n', L' ')](const std::wstring& timeRemMsg) { this->updateStatus(statusPrefix + timeRemMsg + statusPostfix); }); //throw CancelProcess return ProcessCallback::retry; } //always, except for "retry": auto guardWriteLog = makeGuard([&] { logMsg(errorLog_, errorInfo.msg, MSG_TYPE_ERROR, failTime); }); if (!mainDlg_.compareStatus_->getOptionIgnoreErrors()) { forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! switch (showConfirmationDialog(&mainDlg_, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(errorInfo.msg). alertWhenPending(soundFileAlertPending_), _("&Ignore"), _("Ignore &all"), _("&Retry"))) { case ConfirmationButton3::accept: //ignore return ProcessCallback::ignore; case ConfirmationButton3::accept2: //ignore all mainDlg_.compareStatus_->setOptionIgnoreErrors(true); return ProcessCallback::ignore; case ConfirmationButton3::decline: //retry guardWriteLog.dismiss(); logMsg(errorLog_, errorInfo.msg + L"\n-> " + _("Retrying operation..."), //explain why there are duplicate "doing operation X" info messages in the log! MSG_TYPE_INFO, failTime); return ProcessCallback::retry; case ConfirmationButton3::cancel: cancelProcessNow(CancelReason::user); //throw CancelProcess break; } } else return ProcessCallback::ignore; assert(false); return ProcessCallback::ignore; //dummy return value } void StatusHandlerTemporaryPanel::reportFatalError(const std::wstring& msg) { PauseTimers dummy(*mainDlg_.compareStatus_); logMsg(errorLog_, msg, MSG_TYPE_ERROR); if (!mainDlg_.compareStatus_->getOptionIgnoreErrors()) { forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! switch (showConfirmationDialog(&mainDlg_, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(msg). alertWhenPending(soundFileAlertPending_), _("&Ignore"), _("Ignore &all"))) { case ConfirmationButton2::accept: //ignore break; case ConfirmationButton2::accept2: //ignore all mainDlg_.compareStatus_->setOptionIgnoreErrors(true); break; case ConfirmationButton2::cancel: cancelProcessNow(CancelReason::user); //throw CancelProcess break; } } } Statistics::ErrorStats StatusHandlerTemporaryPanel::getErrorStats() const { //errorLog_ is an "append only" structure, so we can make getErrorStats() complexity "constant time": std::for_each(errorLog_.begin() + errorStatsRowsChecked_, errorLog_.end(), [&](const LogEntry& entry) { switch (entry.type) { case MSG_TYPE_INFO: break; case MSG_TYPE_WARNING: ++errorStatsBuf_.warningCount; break; case MSG_TYPE_ERROR: ++errorStatsBuf_.errorCount; break; } }); errorStatsRowsChecked_ = errorLog_.size(); return errorStatsBuf_; } void StatusHandlerTemporaryPanel::forceUiUpdateNoThrow() { if (!mainDlg_.auiMgr_.GetPane(mainDlg_.compareStatus_->getAsWindow()).IsShown() && std::chrono::steady_clock::now() > panelInitTime_ + TEMP_PANEL_DISPLAY_DELAY) showStatsPanel(); mainDlg_.compareStatus_->updateGui(); } void StatusHandlerTemporaryPanel::onLocalKeyEvent(wxKeyEvent& event) { const int keyCode = event.GetKeyCode(); if (keyCode == WXK_ESCAPE) return userRequestCancel(); event.Skip(); } void StatusHandlerTemporaryPanel::onAbortCompare(wxCommandEvent& event) { userRequestCancel(); } //######################################################################################################## StatusHandlerFloatingDialog::StatusHandlerFloatingDialog(wxFrame* parentDlg, const std::vector& jobNames, const std::chrono::system_clock::time_point& startTime, bool ignoreErrors, size_t autoRetryCount, std::chrono::seconds autoRetryDelay, const Zstring& soundFileSyncComplete, const Zstring& soundFileAlertPending, const WindowLayout::Dimensions& dim, bool autoCloseDialog) : jobNames_(jobNames), startTime_(startTime), autoRetryCount_(autoRetryCount), autoRetryDelay_(autoRetryDelay), soundFileSyncComplete_(soundFileSyncComplete), soundFileAlertPending_(soundFileAlertPending) { //set *after* initializer list => callbacks during construction to getErrorStats()! progressDlg_ = SyncProgressDialog::create(dim, [this] { userRequestCancel(); }, *this, parentDlg, true /*showProgress*/, autoCloseDialog, jobNames, std::chrono::system_clock::to_time_t(startTime), ignoreErrors, autoRetryCount, PostSyncAction::none); } StatusHandlerFloatingDialog::~StatusHandlerFloatingDialog() { if (progressDlg_) //prepareResult() was not called! std::abort(); } StatusHandlerFloatingDialog::Result StatusHandlerFloatingDialog::prepareResult() { //keep correct summary window stats considering count down timer, system sleep const std::chrono::milliseconds totalTime = progressDlg_->pauseAndGetTotalTime(); //append "extra" log for sync errors that could not otherwise be reported: if (const ErrorLog extraLog = fetchExtraLog(); !extraLog.empty()) { append(errorLog_.ref(), extraLog); std::stable_sort(errorLog_.ref().begin(), errorLog_.ref().end(), [](const LogEntry& lhs, const LogEntry& rhs) { return lhs.time < rhs.time; }); } //determine post-sync status irrespective of further errors during tear-down assert(!syncResult_); syncResult_ = [&] { if (taskCancelled()) //= user cancel { assert(*taskCancelled() == CancelReason::user); //"stop on first error" is ffs_batch-only logMsg(errorLog_.ref(), _("Stopped"), MSG_TYPE_ERROR); return TaskResult::cancelled; } const ErrorLogStats logCount = getStats(errorLog_.ref()); if (logCount.errors > 0) return TaskResult::error; else if (logCount.warnings > 0) return TaskResult::warning; if (getTotalStats() == ProgressStats()) logMsg(errorLog_.ref(), _("Nothing to synchronize"), MSG_TYPE_INFO); return TaskResult::success; }(); assert(*syncResult_ == TaskResult::cancelled || currentPhase() == ProcessPhase::sync); const ProcessSummary summary { startTime_, *syncResult_, jobNames_, getCurrentStats(), getTotalStats (), totalTime }; return {summary, errorLog_}; } StatusHandlerFloatingDialog::DlgOptions StatusHandlerFloatingDialog::showResult() { bool autoClose = false; bool suspend = false; FinalRequest finalRequest = FinalRequest::none; if (taskCancelled()) assert(*taskCancelled() == CancelReason::user); //"stop on first error" is only for ffs_batch else { //--------------------- post sync actions ---------------------- //give user chance to cancel shutdown; do *not* consider the sync itself cancelled auto proceedWithShutdown = [&](const std::wstring& operationName) { if (progressDlg_->getWindowIfVisible()) try { assert(!endsWith(operationName, L".")); auto notifyStatus = [&](const std::wstring& timeRemMsg) { updateStatus(operationName + L"... " + timeRemMsg); /*throw CancelProcess*/ }; delayAndCountDown(std::chrono::seconds(10), notifyStatus); //throw CancelProcess } catch (CancelProcess&) { return false; } return true; }; switch (progressDlg_->getAndFreezePostSyncAction()) { case PostSyncAction::none: autoClose = progressDlg_->getOptionAutoCloseDialog(); break; case PostSyncAction::exit: autoClose = true; finalRequest = FinalRequest::exit; //program exit must be handled by calling context! break; case PostSyncAction::sleep: if (proceedWithShutdown(_("System: Sleep"))) { autoClose = progressDlg_->getOptionAutoCloseDialog(); suspend = true; } break; case PostSyncAction::shutdown: if (proceedWithShutdown(_("System: Shut down"))) { autoClose = true; finalRequest = FinalRequest::shutdown; //system shutdown must be handled by calling context! } break; } } if (suspend) //*before* showing results dialog try { suspendSystem(); //throw FileError } catch (const FileError& e) { logMsg(errorLog_.ref(), e.toString(), MSG_TYPE_ERROR); } //--------------------- sound notification ---------------------- if (!taskCancelled() && !suspend && !autoClose && //only play when actually showing results dialog !soundFileSyncComplete_.empty()) { //wxWidgets shows modal error dialog by default => "no, wxWidgets, NO!" wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); wxSound::Play(utfTo(soundFileSyncComplete_), wxSOUND_ASYNC); } //if (::GetForegroundWindow() != GetHWND()) // RequestUserAttention(); -> probably too much since task bar is already colorized with Taskbar::Status::error or Status::normal const auto [autoCloseSelected, dim] = progressDlg_->destroy(autoClose, finalRequest == FinalRequest::none /*restoreParentFrame*/, *syncResult_, errorLog_); //caveat: calls back to getErrorStats() => *share* (and not move) errorLog_ progressDlg_ = nullptr; return {autoCloseSelected, dim, finalRequest}; } void StatusHandlerFloatingDialog::initNewPhase(int itemsTotal, int64_t bytesTotal, ProcessPhase phaseID) { assert(phaseID == ProcessPhase::sync); StatusHandler::initNewPhase(itemsTotal, bytesTotal, phaseID); progressDlg_->initNewPhase(); //call after "StatusHandler::initNewPhase" //macOS needs a full yield to update GUI and get rid of "dummy" texts requestUiUpdate(true /*force*/); //throw CancelProcess } void StatusHandlerFloatingDialog::logMessage(const std::wstring& msg, MsgType type) { logMsg(errorLog_.ref(), msg, [&] { switch (type) { case MsgType::info: return MSG_TYPE_INFO; case MsgType::warning: return MSG_TYPE_WARNING; case MsgType::error: return MSG_TYPE_ERROR; } assert(false); return MSG_TYPE_ERROR; }()); requestUiUpdate(false /*force*/); //throw CancelProcess } void StatusHandlerFloatingDialog::reportWarning(const std::wstring& msg, bool& warningActive) { PauseTimers dummy(*progressDlg_); logMsg(errorLog_.ref(), msg, MSG_TYPE_WARNING); if (!warningActive) return; if (!progressDlg_->getOptionIgnoreErrors()) { forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! bool dontWarnAgain = false; switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::warning, PopupDialogCfg().setDetailInstructions(msg). alertWhenPending(soundFileAlertPending_). setCheckBox(dontWarnAgain, _("&Don't show this warning again")), _("&Ignore"))) { case ConfirmationButton::accept: warningActive = !dontWarnAgain; break; case ConfirmationButton::cancel: cancelProcessNow(CancelReason::user); //throw CancelProcess break; } } //else: if errors are ignored, then warnings should be, too } ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const ErrorInfo& errorInfo) { PauseTimers dummy(*progressDlg_); //log actual fail time (not "now"!) const time_t failTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now() - std::chrono::duration_cast(std::chrono::steady_clock::now() - errorInfo.failTime)); //auto-retry if (errorInfo.retryNumber < autoRetryCount_) { logMsg(errorLog_.ref(), errorInfo.msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO, failTime); delayAndCountDown(errorInfo.failTime + autoRetryDelay_ - std::chrono::steady_clock::now(), [&, statusPrefix = _("Automatic retry") + (errorInfo.retryNumber == 0 ? L"" : L' ' + formatNumber(errorInfo.retryNumber + 1)) + SPACED_DASH, statusPostfix = SPACED_DASH + _("Error") + L": " + replaceCpy(errorInfo.msg, L'\n', L' ')](const std::wstring& timeRemMsg) { this->updateStatus(statusPrefix + timeRemMsg + statusPostfix); }); //throw CancelProcess return ProcessCallback::retry; } //always, except for "retry": auto guardWriteLog = makeGuard([&] { logMsg(errorLog_.ref(), errorInfo.msg, MSG_TYPE_ERROR, failTime); }); if (!progressDlg_->getOptionIgnoreErrors()) { forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::error, PopupDialogCfg().setDetailInstructions(errorInfo.msg). alertWhenPending(soundFileAlertPending_), _("&Ignore"), _("Ignore &all"), _("&Retry"))) { case ConfirmationButton3::accept: //ignore return ProcessCallback::ignore; case ConfirmationButton3::accept2: //ignore all progressDlg_->setOptionIgnoreErrors(true); return ProcessCallback::ignore; case ConfirmationButton3::decline: //retry guardWriteLog.dismiss(); logMsg(errorLog_.ref(), errorInfo.msg + L"\n-> " + _("Retrying operation..."), //explain why there are duplicate "doing operation X" info messages in the log! MSG_TYPE_INFO, failTime); return ProcessCallback::retry; case ConfirmationButton3::cancel: cancelProcessNow(CancelReason::user); //throw CancelProcess break; } } else return ProcessCallback::ignore; assert(false); return ProcessCallback::ignore; //dummy value } void StatusHandlerFloatingDialog::reportFatalError(const std::wstring& msg) { PauseTimers dummy(*progressDlg_); logMsg(errorLog_.ref(), msg, MSG_TYPE_ERROR); if (!progressDlg_->getOptionIgnoreErrors()) { forceUiUpdateNoThrow(); //noexcept! => don't throw here when error occurs during clean up! switch (showConfirmationDialog(progressDlg_->getWindowIfVisible(), DialogInfoType::error, PopupDialogCfg().setDetailInstructions(msg). alertWhenPending(soundFileAlertPending_), _("&Ignore"), _("Ignore &all"))) { case ConfirmationButton2::accept: //ignore break; case ConfirmationButton2::accept2: //ignore all progressDlg_->setOptionIgnoreErrors(true); break; case ConfirmationButton2::cancel: cancelProcessNow(CancelReason::user); //throw CancelProcess break; } } } Statistics::ErrorStats StatusHandlerFloatingDialog::getErrorStats() const { //errorLog_ is an "append only" structure, so we can make getErrorStats() complexity "constant time": std::for_each(errorLog_.ref().begin() + errorStatsRowsChecked_, errorLog_.ref().end(), [&](const LogEntry& entry) { switch (entry.type) { case MSG_TYPE_INFO: break; case MSG_TYPE_WARNING: ++errorStatsBuf_.warningCount; break; case MSG_TYPE_ERROR: ++errorStatsBuf_.errorCount; break; } }); errorStatsRowsChecked_ = errorLog_.ref().size(); return errorStatsBuf_; } void StatusHandlerFloatingDialog::updateDataProcessed(int itemsDelta, int64_t bytesDelta) //noexcept! { StatusHandler::updateDataProcessed(itemsDelta, bytesDelta); //note: this method should NOT throw in order to properly allow undoing setting of statistics! progressDlg_->notifyProgressChange(); //noexcept //for "curveDataBytes_->addRecord()" } void StatusHandlerFloatingDialog::forceUiUpdateNoThrow() { progressDlg_->updateGui(); }