// ***************************************************************************** // * 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 "tray_menu.h" #include #include #include #include //Linux needs this #include #include #include #include #include #include #include #include #include "monitor.h" using namespace zen; using namespace rts; namespace { constexpr std::chrono::seconds RETRY_AFTER_ERROR_INTERVAL(15); constexpr std::chrono::milliseconds UI_UPDATE_INTERVAL(100); //perform ui updates not more often than necessary, 100 seems to be a good value with only a minimal performance loss std::chrono::steady_clock::time_point lastExec; bool uiUpdateDue() { const auto now = std::chrono::steady_clock::now(); if (now > lastExec + UI_UPDATE_INTERVAL) { lastExec = now; return true; } return false; } enum TrayMode { active, waiting, error, }; class TrayIcon : public wxTaskBarIcon { public: explicit TrayIcon(const wxString& jobname) : jobName_(jobname) { Bind(wxEVT_TASKBAR_LEFT_UP, [this](wxTaskBarIconEvent& event) { onMouseClick(event); }); assert(mode_ != TrayMode::active); //setMode() supports polling! setMode(TrayMode::active, Zstring()); timer_.Bind(wxEVT_TIMER, [this](wxTimerEvent& event) { onErrorFlashIcon(event); }); } //require polling: bool resumeIsRequested() const { return resumeRequested_; } bool abortIsRequested () const { return exitRequested_; } //during TrayMode::error those two functions are available: void clearShowErrorRequested() { assert(mode_ == TrayMode::error); showErrorMsgRequested_ = false; } bool getShowErrorRequested() const { assert(mode_ == TrayMode::error); return showErrorMsgRequested_; } void setMode(TrayMode m, const Zstring& missingFolderPath) { if (mode_ == m && missingFolderPath_ == missingFolderPath) return; //support polling mode_ = m; missingFolderPath_ = missingFolderPath; timer_.Stop(); switch (m) { case TrayMode::active: setTrayIcon(trayImg_, _("Directory monitoring active")); break; case TrayMode::waiting: assert(!missingFolderPath.empty()); setTrayIcon(greyScale(trayImg_), _("Waiting until directory is available:") + L' ' + fmtPath(missingFolderPath)); break; case TrayMode::error: timer_.Start(500); //timer interval in [ms] break; } } private: void onErrorFlashIcon(wxEvent& event) { iconFlashStatusLast_ = !iconFlashStatusLast_; setTrayIcon(greyScaleIfDisabled(trayImg_, iconFlashStatusLast_), _("Error")); } void setTrayIcon(const wxImage& img, const wxString& statusTxt) { wxString tooltip = L"RealTimeSync"; if (!jobName_.empty()) tooltip += SPACED_DASH + jobName_; tooltip += L"\n" + statusTxt; SetIcon(toScaledBitmap(img), tooltip); } wxMenu* CreatePopupMenu() override { wxMenu* contextMenu = new wxMenu; wxMenuItem* defaultItem = nullptr; switch (mode_) { case TrayMode::active: case TrayMode::waiting: defaultItem = new wxMenuItem(contextMenu, wxID_ANY, _("&Configure")); //better than "Restore"? https://freefilesync.org/forum/viewtopic.php?t=2044&p=20391#p20391 contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { resumeRequested_ = true; }, defaultItem->GetId()); break; case TrayMode::error: defaultItem = new wxMenuItem(contextMenu, wxID_ANY, _("&Show error message")); contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { showErrorMsgRequested_ = true; }, defaultItem->GetId()); break; } contextMenu->Append(defaultItem); contextMenu->AppendSeparator(); wxMenuItem* itemAbort = contextMenu->Append(wxID_ANY, _("&Quit")); contextMenu->Bind(wxEVT_COMMAND_MENU_SELECTED, [this](wxCommandEvent& event) { exitRequested_ = true; }, itemAbort->GetId()); return contextMenu; //ownership transferred to caller } void onMouseClick(wxEvent& event) { switch (mode_) { case TrayMode::active: case TrayMode::waiting: resumeRequested_ = true; //never throw exceptions through a C-Layer call stack (GUI)! break; case TrayMode::error: showErrorMsgRequested_ = true; break; } } bool resumeRequested_ = false; bool exitRequested_ = false; bool showErrorMsgRequested_ = false; TrayMode mode_ = TrayMode::waiting; Zstring missingFolderPath_; bool iconFlashStatusLast_ = false; //flash try icon for TrayMode::error wxTimer timer_; // const wxString jobName_; //RTS job name, may be empty const wxImage trayImg_ = loadImage("start_rts", dipToScreen(24)); //use 24x24 bitmap for perfect fit }; struct AbortMonitoring //exception class { AbortMonitoring(CancelReason reasonCode) : reasonCode_(reasonCode) {} CancelReason reasonCode_; }; //=> don't derive from wxEvtHandler or any other wxWidgets object unless instance is safely deleted (deferred) during idle event!!tray_icon.h class TrayIconHolder { public: explicit TrayIconHolder(const wxString& jobname) : trayIcon_(new TrayIcon(jobname)) {} ~TrayIconHolder() { //harmonize with tray_icon.cpp!!! trayIcon_->RemoveIcon(); //*schedule* for destruction: delete during next idle event (handle late window messages, e.g. when double-clicking) trayIcon_->Destroy(); //uses wxPendingDelete } void doUiRefreshNow() //throw AbortMonitoring { wxTheApp->Yield(); //yield is UI-layer which is represented by this tray icon //advantage of polling vs callbacks: we can throw exceptions! if (trayIcon_->resumeIsRequested()) throw AbortMonitoring(CancelReason::requestGui); if (trayIcon_->abortIsRequested()) throw AbortMonitoring(CancelReason::requestExit); } void setMode(TrayMode m, const Zstring& missingFolderPath) { trayIcon_->setMode(m, missingFolderPath); } bool getShowErrorRequested() const { return trayIcon_->getShowErrorRequested(); } void clearShowErrorRequested() { trayIcon_->clearShowErrorRequested(); } private: TrayIcon* const trayIcon_; }; //############################################################################################################## } rts::CancelReason rts::runFolderMonitor(const FfsRealConfig& config, const wxString& jobname) { std::vector dirNamesNonFmt = config.directories; std::erase_if(dirNamesNonFmt, [](const Zstring& str) { return trimCpy(str).empty(); }); //remove empty entries WITHOUT formatting paths yet! if (dirNamesNonFmt.empty()) { showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(_("A folder input field is empty."))); return CancelReason::requestGui; } const Zstring cmdLine = trimCpy(config.commandline); if (cmdLine.empty()) { showNotificationDialog(nullptr, DialogInfoType::error, PopupDialogCfg().setMainInstructions(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLine)))); return CancelReason::requestGui; } TrayIconHolder trayIcon(jobname); auto executeExternalCommand = [&](const Zstring& changedItemPath, const std::wstring& actionName) //throw FileError { ::wxSetEnv(L"change_path", utfTo(changedItemPath)); //crude way to report changed file ::wxSetEnv(L"change_action", actionName); // auto cmdLineExp = expandMacros(cmdLine); try { if (const auto& [exitCode, output] = consoleExecute(cmdLineExp, std::nullopt /*timeoutMs*/); //throw SysError, (SysErrorTimeOut) exitCode != 0) throw SysError(formatSystemError("", replaceCpy(_("Exit code %x"), L"%x", numberTo(exitCode)), utfTo(output))); } catch (const SysError& e) { throw FileError(replaceCpy(_("Command %x failed."), L"%x", fmtPath(cmdLineExp)), e.toString()); } }; auto requestUiUpdate = [&](const Zstring* missingFolderPath) { if (missingFolderPath) trayIcon.setMode(TrayMode::waiting, *missingFolderPath); else trayIcon.setMode(TrayMode::active, Zstring()); if (uiUpdateDue()) trayIcon.doUiRefreshNow(); //throw AbortMonitoring }; auto reportError = [&](const std::wstring& msg) { trayIcon.setMode(TrayMode::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.doUiRefreshNow(); //throw AbortMonitoring if (trayIcon.getShowErrorRequested()) switch (showConfirmationDialog(nullptr, DialogInfoType::error, PopupDialogCfg(). setDetailInstructions(msg), _("&Retry"))) { case ConfirmationButton::accept: //retry return; case ConfirmationButton::cancel: throw AbortMonitoring(CancelReason::requestGui); } std::this_thread::sleep_for(UI_UPDATE_INTERVAL); } }; try { monitorDirectories(dirNamesNonFmt, std::chrono::seconds(config.delay), executeExternalCommand /*throw FileError*/, requestUiUpdate, //throw AbortMonitoring reportError, // UI_UPDATE_INTERVAL / 2); assert(false); return CancelReason::requestGui; } catch (const AbortMonitoring& ab) { return ab.reasonCode_; } }