diff options
Diffstat (limited to 'RealtimeSync/tray_menu.cpp')
-rw-r--r-- | RealtimeSync/tray_menu.cpp | 479 |
1 files changed, 262 insertions, 217 deletions
diff --git a/RealtimeSync/tray_menu.cpp b/RealtimeSync/tray_menu.cpp index e18101e0..4afef5e4 100644 --- a/RealtimeSync/tray_menu.cpp +++ b/RealtimeSync/tray_menu.cpp @@ -16,38 +16,42 @@ #include <wx/menu.h> #include "watcher.h" #include <wx/utils.h> -#include <wx/log.h> #include <wx/icon.h> //Linux needs this #include <wx/timer.h> +#include <wx+/mouse_move_dlg.h> #include "resources.h" #include <wx+/string_conv.h> #include <zen/assert_static.h> #include <zen/build_info.h> #include <wx+/shell_execute.h> +#include "gui_generated.h" using namespace rts; using namespace zen; -class TrayIconHolder : private wxEvtHandler + +namespace { -public: - TrayIconHolder(const wxString& jobname); - ~TrayIconHolder(); +struct AbortCallback //never throw exceptions through a C-Layer (GUI)! +{ + virtual ~AbortCallback() {} + virtual void requestResume() = 0; + virtual void requestAbort() = 0; +}; - void doUiRefreshNow(); - void showIconActive(); - void showIconWaiting(); - void requestAbort() +//RtsTrayIcon is a dumb class whose sole purpose is to enable wxWidgets deferred deletion +class RtsTrayIconRaw : public wxTaskBarIcon +{ +public: + RtsTrayIconRaw(AbortCallback& abortCb) : abortCb_(&abortCb) { - m_abortRequested = true; + Connect(wxEVT_TASKBAR_LEFT_DCLICK, wxCommandEventHandler(RtsTrayIconRaw::OnDoubleClick), nullptr, this); } - void OnRequestResume(wxCommandEvent& event) - { - m_resumeRequested = true; - } + void dontCallBackAnymore() { abortCb_ = nullptr; } //call before tray icon is marked for deferred deletion +private: enum Selection { CONTEXT_ABORT, @@ -55,34 +59,9 @@ public: CONTEXT_ABOUT }; - void OnContextMenuSelection(wxCommandEvent& event); - -private: - class RtsTrayIcon; - RtsTrayIcon* trayMenu; - - bool m_abortRequested; - bool m_resumeRequested; - - const wxString jobName_; //RTS job name, may be empty -}; - - -//RtsTrayIcon shall be a dumb class whose sole purpose is to enable wxWidgets deferred deletion -class TrayIconHolder::RtsTrayIcon : public wxTaskBarIcon -{ -public: - RtsTrayIcon(TrayIconHolder* parent) : parent_(parent) {} - - void parentHasDied() //call before tray icon is marked for deferred deletion - { - parent_ = nullptr; - } - -private: virtual wxMenu* CreatePopupMenu() { - if (!parent_) + if (!abortCb_) return nullptr; wxMenu* contextMenu = new wxMenu; @@ -91,169 +70,150 @@ private: contextMenu->AppendSeparator(); contextMenu->Append(CONTEXT_ABORT, _("&Exit")); //event handling - contextMenu->Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TrayIconHolder::OnContextMenuSelection), nullptr, parent_); + contextMenu->Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(RtsTrayIconRaw::OnContextMenuSelection), nullptr, this); return contextMenu; //ownership transferred to caller } - TrayIconHolder* parent_; -}; -//############################################################################################################## + void OnContextMenuSelection(wxCommandEvent& event) + { + if (!abortCb_) + return; + switch (static_cast<Selection>(event.GetId())) + { + case CONTEXT_ABORT: + abortCb_->requestAbort(); + break; -class AbortThisProcess //exception class -{ -public: - AbortThisProcess(MonitorResponse command) : command_(command) {} + case CONTEXT_RESTORE: + abortCb_->requestResume(); + break; + + case CONTEXT_ABOUT: + { + //build information + wxString build = __TDATE__; +#if wxUSE_UNICODE + build += L" - Unicode"; +#else + build += L" - ANSI"; +#endif //wxUSE_UNICODE + + //compile time info about 32/64-bit build + if (zen::is64BitBuild) + build += L" x64"; + else + build += L" x86"; + assert_static(zen::is32BitBuild || zen::is64BitBuild); + + wxMessageBox(L"RealtimeSync" L"\n\n" + replaceCpy(_("(Build: %x)"), L"%x", build), _("About"), wxOK); + } + break; + } + } - MonitorResponse getCommand() const + void OnDoubleClick(wxCommandEvent& event) { - return command_; + if (abortCb_) + abortCb_->requestResume(); } -private: - MonitorResponse command_; + AbortCallback* abortCb_; }; -//############################################################################################################## -TrayIconHolder::TrayIconHolder(const wxString& jobname) : - m_abortRequested(false), - m_resumeRequested(false), - jobName_(jobname) +class TrayIconHolder { - trayMenu = new RtsTrayIcon(this); //not in initialization list: give it a valid parent object! - - showIconActive(); - - //register double-click - trayMenu->Connect(wxEVT_TASKBAR_LEFT_DCLICK, wxCommandEventHandler(TrayIconHolder::OnRequestResume), nullptr, this); -} - +public: + TrayIconHolder(const wxString& jobname, AbortCallback& abortCb) : + jobName_(jobname) + { + trayMenu = new RtsTrayIconRaw(abortCb); //not in initialization list: give it a valid parent object! + showIconActive(); + } -TrayIconHolder::~TrayIconHolder() -{ - trayMenu->Disconnect(wxEVT_TASKBAR_LEFT_DCLICK, wxCommandEventHandler(TrayIconHolder::OnRequestResume), nullptr, this); - trayMenu->RemoveIcon(); //(try to) hide icon until final deletion takes place - trayMenu->parentHasDied(); + ~TrayIconHolder() + { + trayMenu->RemoveIcon(); //(try to) hide icon until final deletion takes place + trayMenu->dontCallBackAnymore(); - //use wxWidgets delayed destruction: delete during next idle loop iteration (handle late window messages, e.g. when double-clicking) - if (!wxPendingDelete.Member(trayMenu)) - wxPendingDelete.Append(trayMenu); -} + //use wxWidgets delayed destruction: delete during next idle loop iteration (handle late window messages, e.g. when double-clicking) + if (!wxPendingDelete.Member(trayMenu)) + wxPendingDelete.Append(trayMenu); + } + void doUiRefreshNow() + { + wxTheApp->Yield(); + } //yield is UI-layer which is represented by this tray icon -void TrayIconHolder::showIconActive() -{ - wxIcon realtimeIcon; + void showIconActive() + { + wxIcon realtimeIcon; #ifdef FFS_WIN - realtimeIcon.CopyFromBitmap(GlobalResources::getImage(wxT("RTS_tray_win.png"))); //use a 16x16 bitmap + realtimeIcon.CopyFromBitmap(GlobalResources::getImage(L"RTS_tray_win.png")); //use a 16x16 bitmap #elif defined FFS_LINUX - realtimeIcon.CopyFromBitmap(GlobalResources::getImage(wxT("RTS_tray_linux.png"))); //use a 22x22 bitmap for perfect fit + realtimeIcon.CopyFromBitmap(GlobalResources::getImage(L"RTS_tray_linux.png")); //use a 22x22 bitmap for perfect fit #endif - const wxString postFix = jobName_.empty() ? wxString() : (wxT("\n\"") + jobName_ + wxT("\"")); - trayMenu->SetIcon(realtimeIcon, _("Monitoring active...") + postFix); -} - + const wxString postFix = jobName_.empty() ? wxString() : (L"\n\"" + jobName_ + L"\""); + trayMenu->SetIcon(realtimeIcon, _("Monitoring active...") + postFix); + } -void TrayIconHolder::showIconWaiting() -{ - wxIcon realtimeIcon; + void showIconWaiting() + { + wxIcon realtimeIcon; #ifdef FFS_WIN - realtimeIcon.CopyFromBitmap(GlobalResources::getImage(wxT("RTS_tray_waiting_win.png"))); //use a 16x16 bitmap + realtimeIcon.CopyFromBitmap(GlobalResources::getImage(L"RTS_tray_waiting_win.png")); //use a 16x16 bitmap #elif defined FFS_LINUX - realtimeIcon.CopyFromBitmap(GlobalResources::getImage(wxT("RTS_tray_waiting_linux.png"))); //use a 22x22 bitmap for perfect fit + realtimeIcon.CopyFromBitmap(GlobalResources::getImage(L"RTS_tray_waiting_linux.png")); //use a 22x22 bitmap for perfect fit #endif - const wxString postFix = jobName_.empty() ? wxString() : (wxT("\n\"") + jobName_ + wxT("\"")); - trayMenu->SetIcon(realtimeIcon, _("Waiting for missing directories...") + postFix); -} - - -void TrayIconHolder::OnContextMenuSelection(wxCommandEvent& event) -{ - const int eventId = event.GetId(); - switch (static_cast<Selection>(eventId)) - { - case CONTEXT_ABORT: - requestAbort(); - break; - case CONTEXT_RESTORE: - OnRequestResume(event); //just remember: never throw exceptions through a C-Layer (GUI) ;) - break; - case CONTEXT_ABOUT: - { - //build information - wxString build = __TDATE__; -#if wxUSE_UNICODE - build += wxT(" - Unicode"); -#else - build += wxT(" - ANSI"); -#endif //wxUSE_UNICODE - - //compile time info about 32/64-bit build - if (zen::is64BitBuild) - build += wxT(" x64"); - else - build += wxT(" x86"); - assert_static(zen::is32BitBuild || zen::is64BitBuild); - - wxString buildFormatted = _("(Build: %x)"); - buildFormatted.Replace(wxT("%x"), build); - - wxMessageDialog aboutDlg(nullptr, wxString(wxT("RealtimeSync")) + wxT("\n\n") + buildFormatted, _("About"), wxOK); - aboutDlg.ShowModal(); - } - break; + const wxString postFix = jobName_.empty() ? wxString() : (L"\n\"" + jobName_ + L"\""); + trayMenu->SetIcon(realtimeIcon, _("Waiting for missing directories...") + postFix); } -} - -void TrayIconHolder::doUiRefreshNow() -{ - wxTheApp->Yield(); +private: + RtsTrayIconRaw* trayMenu; + const wxString jobName_; //RTS job name, may be empty +}; - if (m_abortRequested) - throw ::AbortThisProcess(QUIT); - if (m_resumeRequested) - throw ::AbortThisProcess(RESUME); -} //############################################################################################################## -namespace -{ -std::vector<Zstring> convert(const std::vector<wxString>& dirList) +struct AbortMonitoring//exception class { - std::set<Zstring, LessFilename> output; - std::transform(dirList.begin(), dirList.end(), - std::inserter(output, output.end()), [](const wxString & str) { return zen::toZ(str); }); - return std::vector<Zstring>(output.begin(), output.end()); -} -} - + AbortMonitoring(AbortReason reasonCode) : reasonCode_(reasonCode) {} + AbortReason reasonCode_; +}; class StartSyncNowException {}; +//############################################################################################################## -class WaitCallbackImpl : public rts::WaitCallback +class WaitCallbackImpl : public rts::WaitCallback, private AbortCallback { public: WaitCallbackImpl(const wxString& jobname) : - trayIcon(jobname), - nextSyncStart_(std::numeric_limits<long>::max()) {} + trayIcon(jobname, *this), + nextSyncStart_(std::numeric_limits<long>::max()), + resumeRequested(false), + abortRequested(false) {} - void notifyAllDirectoriesExist() - { - trayIcon.showIconActive(); - } + void notifyAllDirectoriesExist() { trayIcon.showIconActive(); } + void notifyDirectoryMissing () { trayIcon.showIconWaiting(); } - void notifyDirectoryMissing() - { - trayIcon.showIconWaiting(); - } + void scheduleNextSync(long nextSyncStart) { nextSyncStart_ = nextSyncStart; } + void clearSchedule() { nextSyncStart_ = std::numeric_limits<long>::max(); } - virtual void requestUiRefresh() //throw StartSyncNowException() + //implement WaitCallback + virtual void requestUiRefresh() //throw StartSyncNowException, AbortMonitoring { + if (resumeRequested) + throw AbortMonitoring(SHOW_GUI); + + if (abortRequested) + throw AbortMonitoring(EXIT_APP); + if (nextSyncStart_ <= wxGetLocalTime()) throw StartSyncNowException(); //abort wait and start sync @@ -261,16 +221,87 @@ public: trayIcon.doUiRefreshNow(); } - void scheduleNextSync(long nextSyncStart) +private: + //implement AbortCallback: used from C-GUI call stack + virtual void requestResume() { resumeRequested = true; } + virtual void requestAbort () { abortRequested = true; } + + TrayIconHolder trayIcon; + long nextSyncStart_; + bool resumeRequested; + bool abortRequested; +}; + + + +class ErrorDlgWithTimeout : public ErrorDlgGenerated +{ +public: + ErrorDlgWithTimeout(wxWindow* parent, const wxString& messageText) : + ErrorDlgGenerated(parent), + secondsLeft(15) //give user some time to read msg!? { - nextSyncStart_ = nextSyncStart; +#ifdef FFS_WIN + new zen::MouseMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere...; ownership passed to "this" +#endif + m_bitmap10->SetBitmap(GlobalResources::getImage(L"error")); + m_textCtrl8->SetValue(messageText); + m_buttonRetry->SetFocus(); + + //count down X seconds then automatically press "retry" + timer.Connect(wxEVT_TIMER, wxEventHandler(ErrorDlgWithTimeout::OnTimerEvent), nullptr, this); + timer.Start(1000); //timer interval in ms + updateButtonLabel(); } + enum ButtonPressed + { + BUTTON_RETRY, + BUTTON_ABORT + }; + private: - TrayIconHolder trayIcon; - long nextSyncStart_; + void OnTimerEvent(wxEvent& event) + { + --secondsLeft; + if (secondsLeft < 0) + { + EndModal(BUTTON_RETRY); + return; + } + updateButtonLabel(); + } + + void updateButtonLabel() + { + m_buttonRetry->SetLabel(_("&Retry") + L" (" + replaceCpy(_P("1 sec", "%x sec", secondsLeft), L"%x", numberTo<std::wstring>(secondsLeft)) + L")"); + Layout(); + } + + void OnClose(wxCloseEvent& event) { EndModal(BUTTON_ABORT); } + void OnRetry(wxCommandEvent& event) { EndModal(BUTTON_RETRY); } + void OnAbort(wxCommandEvent& event) { EndModal(BUTTON_ABORT); } + + int secondsLeft; + wxTimer timer; }; + +bool reportErrorTimeout(const std::wstring& msg) //return true if timeout or user selected "retry", else abort +{ + ErrorDlgWithTimeout errorDlg(nullptr, msg); + //errorDlg.Raise(); -> don't steal focus every X seconds + switch (static_cast<ErrorDlgWithTimeout::ButtonPressed>(errorDlg.ShowModal())) + { + case ErrorDlgWithTimeout::BUTTON_RETRY: + return true; + case ErrorDlgWithTimeout::BUTTON_ABORT: + return false; + } + return false; +} +} + /* Data Flow: ---------- @@ -278,82 +309,96 @@ Data Flow: TrayIconHolder (GUI output) /|\ | -WaitCallbackImpl (higher level "interface") +WaitCallbackImpl /|\ | startDirectoryMonitor() (wire dir-changes and execution of commandline) - /|\ - | -watcher.h (low level wait for directory changes) */ -rts::MonitorResponse rts::startDirectoryMonitor(const xmlAccess::XmlRealConfig& config, const wxString& jobname) +rts::AbortReason rts::startDirectoryMonitor(const xmlAccess::XmlRealConfig& config, const wxString& jobname) { - Zstring lastFileChanged; + const std::vector<Zstring> dirList = toZ(config.directories); + + auto cmdLine = config.commandline; + trim(cmdLine); + + if (cmdLine.empty()) + { + wxMessageBox(replaceCpy(_("Invalid command line: %x"), L"%x", L"\"\""), _("Error"), wxOK | wxICON_ERROR); + return SHOW_GUI; + } + if (dirList.empty() || std::any_of(dirList.begin(), dirList.end(), [](Zstring str) -> bool { trim(str); return str.empty(); })) + { + wxMessageBox(_("A directory input field is empty."), _("Error"), wxOK | wxICON_ERROR); + return SHOW_GUI; + } - const std::vector<Zstring> dirList = convert(config.directories); try { + Zstring lastFileChanged; WaitCallbackImpl callback(jobname); - if (config.commandline.empty()) - { - std::wstring errorMsg = _("Invalid command line: %x"); - replace(errorMsg, L"%x", L"\"" + config.commandline + L"\""); - throw FileError(errorMsg); - } - - callback.notifyDirectoryMissing(); - waitForMissingDirs(dirList, &callback); - callback.notifyAllDirectoriesExist(); - - while (true) + auto execMonitoring = [&] //throw FileError, AbortMonitoring { - ::wxSetEnv(L"changed_file", utf8CvrtTo<wxString>(lastFileChanged)); //some way to output what file changed to the user - lastFileChanged.clear(); //make sure old name is not shown again after a directory reappears - - //execute command - zen::shellExecute(config.commandline, zen::EXEC_TYPE_SYNC); + callback.clearSchedule(); - wxLog::FlushActive(); //show wxWidgets error messages (if any) + callback.notifyDirectoryMissing(); + waitForMissingDirs(dirList, callback); //throw FileError, StartSyncNowException(not scheduled yet), AbortMonitoring + callback.notifyAllDirectoriesExist(); - callback.scheduleNextSync(std::numeric_limits<long>::max()); //next sync not scheduled (yet) + //schedule initial execution only AFTER waitForMissingDirs(), else StartSyncNowException might be thrown while directory checking hangs + callback.scheduleNextSync(wxGetLocalTime() + static_cast<long>(config.delay)); - try + while (true) { - while (true) + try { - //wait for changes (and for all directories to become available) - WaitResult res = waitForChanges(dirList, &callback); - switch (res.type) + while (true) { - case CHANGE_DIR_MISSING: //don't execute the commandline before all directories are available! - callback.scheduleNextSync(std::numeric_limits<long>::max()); //next sync not scheduled (yet) - callback.notifyDirectoryMissing(); - waitForMissingDirs(dirList, &callback); - callback.notifyAllDirectoriesExist(); - break; - case CHANGE_DETECTED: - lastFileChanged = res.filename; - break; + //wait for changes (and for all directories to become available) + WaitResult res = waitForChanges(dirList, callback); //throw FileError, StartSyncNowException, AbortMonitoring + switch (res.type) + { + case CHANGE_DIR_MISSING: //don't execute the commandline before all directories are available! + callback.clearSchedule(); + + callback.notifyDirectoryMissing(); + waitForMissingDirs(dirList, callback); //throw FileError, StartSyncNowException(not scheduled yet), AbortMonitoring + callback.notifyAllDirectoriesExist(); + break; + + case CHANGE_DETECTED: + lastFileChanged = res.filename; + break; + } + callback.scheduleNextSync(wxGetLocalTime() + static_cast<long>(config.delay)); } - - callback.scheduleNextSync(wxGetLocalTime() + static_cast<long>(config.delay)); } + catch (StartSyncNowException&) {} + + ::wxSetEnv(L"changed_file", utf8CvrtTo<wxString>(lastFileChanged)); //some way to output what file changed to the user + lastFileChanged.clear(); //make sure old name is not shown again after a directory reappears + + //execute command + zen::shellExecute(cmdLine, zen::EXEC_TYPE_SYNC); + callback.clearSchedule(); + } + }; + + while (true) + try + { + execMonitoring(); //throw FileError, AbortMonitoring + } + catch (const zen::FileError& e) + { + if (!reportErrorTimeout(e.toString())) //return true if timeout or user selected "retry", else abort + return SHOW_GUI; } - catch (StartSyncNowException&) {} - } - } - catch (const ::AbortThisProcess& ab) - { - return ab.getCommand(); } - catch (const zen::FileError& error) + catch (const AbortMonitoring& ab) { - wxMessageBox(error.toString(), _("Error"), wxOK | wxICON_ERROR); - return RESUME; + return ab.reasonCode_; } - - return RESUME; } |