Files
2025-12-10 14:38:26 -08:00

315 lines
11 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 "tray_menu.h"
#include <chrono>
#include <zen/resolve_path.h>
#include <wx/taskbar.h>
#include <wx/icon.h> //Linux needs this
#include <wx/app.h>
#include <wx/menu.h>
#include <wx/timer.h>
#include <wx+/dc.h>
#include <wx+/image_tools.h>
#include <zen/process_exec.h>
#include <wx+/popup_dlg.h>
#include <wx+/image_resources.h>
#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<Zstring> 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<wxString>(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<std::wstring>(exitCode)), utfTo<std::wstring>(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_;
}
}