diff options
Diffstat (limited to 'RealtimeSync/tray_menu.cpp')
-rw-r--r-- | RealtimeSync/tray_menu.cpp | 440 |
1 files changed, 219 insertions, 221 deletions
diff --git a/RealtimeSync/tray_menu.cpp b/RealtimeSync/tray_menu.cpp index 676904f1..33758ad2 100644 --- a/RealtimeSync/tray_menu.cpp +++ b/RealtimeSync/tray_menu.cpp @@ -5,28 +5,20 @@ // ************************************************************************** #include "tray_menu.h" -#include <algorithm> -#include <iterator> -#include <limits> -#include <set> -#include <zen/assert_static.h> #include <zen/build_info.h> +#include <zen/tick_count.h> +#include <zen/thread.h> #include <wx+/mouse_move_dlg.h> #include <wx+/image_tools.h> -#include <wx+/string_conv.h> +//#include <wx+/string_conv.h> #include <wx+/shell_execute.h> #include <wx+/std_button_order.h> -#include <wx/msgdlg.h> #include <wx/taskbar.h> -#include <wx/app.h> -#include <wx/utils.h> -#include <wx/menu.h> -#include <wx/utils.h> #include <wx/icon.h> //Linux needs this -#include <wx/timer.h> +#include <wx/app.h> #include "resources.h" #include "gui_generated.h" -#include "watcher.h" +#include "monitor.h" #include "../lib/resolve_path.h" using namespace rts; @@ -35,67 +27,149 @@ using namespace zen; namespace { -struct AbortCallback //never throw exceptions through a C-Layer (GUI)! +const std::int64_t TICKS_UPDATE_INTERVAL = rts::UI_UPDATE_INTERVAL* ticksPerSec() / 1000; +TickVal lastExec = getTicks(); + +bool updateUiIsAllowed() +{ + const TickVal now = getTicks(); //0 on error + if (dist(lastExec, now) >= TICKS_UPDATE_INTERVAL) //perform ui updates not more often than necessary + { + lastExec = now; + return true; + } + return false; +} + + +enum TrayMode { - virtual ~AbortCallback() {} - virtual void requestResume() = 0; - virtual void requestAbort() = 0; + TRAY_MODE_ACTIVE, + TRAY_MODE_WAITING, + TRAY_MODE_ERROR, }; -//RtsTrayIcon is a dumb class whose sole purpose is to enable wxWidgets deferred deletion -class RtsTrayIconRaw : public wxTaskBarIcon +class TrayIconObject : public wxTaskBarIcon { public: - RtsTrayIconRaw(AbortCallback& abortCb) : abortCb_(&abortCb) + TrayIconObject(const wxString& jobname) : + resumeRequested(false), + abortRequested(false), + showErrorMsgRequested(false), + mode(TRAY_MODE_ACTIVE), + iconFlashStatusLast(false), + jobName_(jobname), +#if defined ZEN_WIN || defined ZEN_MAC //16x16 seems to be the only size that is shown correctly on OS X + trayBmp(getResourceImage(L"RTS_tray_16x16")) //use a 16x16 bitmap +#elif defined ZEN_LINUX + trayBmp(getResourceImage(L"RTS_tray_24x24")) //use a 24x24 bitmap for perfect fit +#endif { - Connect(wxEVT_TASKBAR_LEFT_DCLICK, wxCommandEventHandler(RtsTrayIconRaw::OnDoubleClick), nullptr, this); + Connect(wxEVT_TASKBAR_LEFT_DCLICK, wxCommandEventHandler(TrayIconObject::OnDoubleClick), nullptr, this); + setMode(mode); } - void dontCallBackAnymore() { abortCb_ = nullptr; } //call before tray icon is marked for deferred deletion + //require polling: + bool resumeIsRequested() const { return resumeRequested; } + bool abortIsRequested () const { return abortRequested; } + + //during TRAY_MODE_ERROR those two functions are available: + void clearShowErrorRequested() { assert(mode == TRAY_MODE_ERROR); showErrorMsgRequested = false; } + bool getShowErrorRequested() const { assert(mode == TRAY_MODE_ERROR); return showErrorMsgRequested; } + + void setMode(TrayMode m) + { + mode = m; + timer.Stop(); + timer.Disconnect(wxEVT_TIMER, wxEventHandler(TrayIconObject::OnErrorFlashIcon), nullptr, this); + switch (m) + { + case TRAY_MODE_ACTIVE: + setTrayIcon(trayBmp, _("Directory monitoring active")); + break; + + case TRAY_MODE_WAITING: + setTrayIcon(greyScale(trayBmp), _("Waiting until all directories are available...")); + break; + + case TRAY_MODE_ERROR: + timer.Connect(wxEVT_TIMER, wxEventHandler(TrayIconObject::OnErrorFlashIcon), nullptr, this); + timer.Start(500); //timer interval in [ms] + break; + } + } private: + void OnErrorFlashIcon(wxEvent& event) + { + iconFlashStatusLast = !iconFlashStatusLast; + setTrayIcon(iconFlashStatusLast ? trayBmp : greyScale(trayBmp), _("Error")); + } + + void setTrayIcon(const wxBitmap& bmp, const wxString& statusTxt) + { + wxIcon realtimeIcon; + realtimeIcon.CopyFromBitmap(bmp); + wxString tooltip = L"RealtimeSync\n" + statusTxt; + if (!jobName_.empty()) + tooltip += L"\n\"" + jobName_ + L"\""; + SetIcon(realtimeIcon, tooltip); + } + enum Selection { - CONTEXT_RESTORE = 1, //wxWidgets: "A MenuItem ID of Zero does not work under Mac" + CONTEXT_RESTORE = 1, //wxWidgets: "A MenuItem ID of zero does not work under Mac" + CONTEXT_SHOW_ERROR, CONTEXT_ABORT = wxID_EXIT, CONTEXT_ABOUT = wxID_ABOUT }; virtual wxMenu* CreatePopupMenu() { - if (!abortCb_) - return nullptr; - wxMenu* contextMenu = new wxMenu; - contextMenu->Append(CONTEXT_RESTORE, _("&Restore")); + switch (mode) + { + case TRAY_MODE_ACTIVE: + case TRAY_MODE_WAITING: + contextMenu->Append(CONTEXT_RESTORE, _("&Restore")); + break; + case TRAY_MODE_ERROR: + contextMenu->Append(CONTEXT_SHOW_ERROR, _("&Show error")); + break; + } contextMenu->Append(CONTEXT_ABOUT, _("&About")); contextMenu->AppendSeparator(); contextMenu->Append(CONTEXT_ABORT, _("&Exit")); //event handling - contextMenu->Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(RtsTrayIconRaw::OnContextMenuSelection), nullptr, this); + contextMenu->Connect(wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(TrayIconObject::OnContextMenuSelection), nullptr, this); return contextMenu; //ownership transferred to caller } void OnContextMenuSelection(wxCommandEvent& event) { - if (!abortCb_) - return; - switch (static_cast<Selection>(event.GetId())) { case CONTEXT_ABORT: - abortCb_->requestAbort(); + abortRequested = true; break; case CONTEXT_RESTORE: - abortCb_->requestResume(); + resumeRequested = true; + break; + + case CONTEXT_SHOW_ERROR: + showErrorMsgRequested = true; break; case CONTEXT_ABOUT: { - //build information + //ATTENTION: the modal dialog below does NOT disable all GUI input, e.g. user may still double-click on tray icon + //no crash in this context, but the double-click is remembered and executed after the modal dialog quits + SetEvtHandlerEnabled(false); + ZEN_ON_SCOPE_EXIT(SetEvtHandlerEnabled(true)); + wxString build = __TDATE__; #if wxUSE_UNICODE build += L" - Unicode"; @@ -103,7 +177,6 @@ private: build += L" - ANSI"; #endif //wxUSE_UNICODE - //compile time info about 32/64-bit build if (zen::is64BitBuild) build += L" x64"; else @@ -118,136 +191,86 @@ private: void OnDoubleClick(wxCommandEvent& event) { - if (abortCb_) - abortCb_->requestResume(); - } - - AbortCallback* abortCb_; -}; - - -class TrayIconHolder -{ -public: - TrayIconHolder(const wxString& jobname, AbortCallback& abortCb) : - jobName_(jobname), - trayMenu(new RtsTrayIconRaw(abortCb)) - { - showIconActive(); + switch (mode) + { + case TRAY_MODE_ACTIVE: + case TRAY_MODE_WAITING: + resumeRequested = true; //never throw exceptions through a C-Layer call stack (GUI)! + break; + case TRAY_MODE_ERROR: + showErrorMsgRequested = true; + break; + } } - ~TrayIconHolder() - { - trayMenu->RemoveIcon(); //(try to) hide icon until final deletion takes place - trayMenu->dontCallBackAnymore(); + bool resumeRequested; + bool abortRequested; + bool showErrorMsgRequested; - //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); - } + TrayMode mode; - void doUiRefreshNow() - { - wxTheApp->Yield(); - } //yield is UI-layer which is represented by this tray icon + bool iconFlashStatusLast; //flash try icon for TRAY_MODE_ERROR + wxTimer timer; // - void showIconActive() - { - wxIcon realtimeIcon; -#if defined ZEN_WIN || defined ZEN_MAC //16x16 seems to be the only size that is shown correctly on OS X - realtimeIcon.CopyFromBitmap(getResourceImage(L"RTS_tray_16x16")); //use a 16x16 bitmap -#elif defined ZEN_LINUX - realtimeIcon.CopyFromBitmap(getResourceImage(L"RTS_tray_24x24")); //use a 24x24 bitmap for perfect fit -#endif - wxString tooltip = L"RealtimeSync"; - if (!jobName_.empty()) - tooltip += L"\n\"" + jobName_ + L"\""; - trayMenu->SetIcon(realtimeIcon, tooltip); - } - - void showIconWaiting() - { - wxIcon realtimeIcon; -#if defined ZEN_WIN || defined ZEN_MAC - realtimeIcon.CopyFromBitmap(greyScale(getResourceImage(L"RTS_tray_16x16"))); -#elif defined ZEN_LINUX - realtimeIcon.CopyFromBitmap(greyScale(getResourceImage(L"RTS_tray_24x24"))); -#endif - wxString tooltip = _("Waiting for missing directories..."); - if (!jobName_.empty()) - tooltip += L"\n\"" + jobName_ + L"\""; - trayMenu->SetIcon(realtimeIcon, tooltip); - } - -private: const wxString jobName_; //RTS job name, may be empty - RtsTrayIconRaw* trayMenu; + const wxBitmap trayBmp; }; -//############################################################################################################## - -struct AbortMonitoring//exception class +struct AbortMonitoring //exception class { AbortMonitoring(AbortReason reasonCode) : reasonCode_(reasonCode) {} AbortReason reasonCode_; }; -class StartSyncNowException {}; - -//############################################################################################################## -class WaitCallbackImpl : public rts::WaitCallback, private AbortCallback +//=> 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: - WaitCallbackImpl(const wxString& jobname) : - trayIcon(jobname, *this), - nextSyncStart_(std::numeric_limits<long>::max()), - resumeRequested(false), - abortRequested(false) {} - - void notifyAllDirectoriesExist() { trayIcon.showIconActive(); } - void notifyDirectoryMissing () { trayIcon.showIconWaiting(); } + TrayIconHolder(const wxString& jobname) : + trayObj(new TrayIconObject(jobname)) {} - void scheduleNextSync(long nextSyncStart) { nextSyncStart_ = nextSyncStart; } - void clearSchedule() { nextSyncStart_ = std::numeric_limits<long>::max(); } + ~TrayIconHolder() + { + //harmonize with tray_icon.cpp!!! + trayObj->RemoveIcon(); + //use wxWidgets delayed destruction: delete during next idle loop iteration (handle late window messages, e.g. when double-clicking) + wxPendingDelete.Append(trayObj); + } - //implement WaitCallback - virtual void requestUiRefresh(bool readyForSync) //throw StartSyncNowException, AbortMonitoring + void doUiRefreshNow() //throw AbortMonitoring { - if (resumeRequested) + wxTheApp->Yield(); //yield is UI-layer which is represented by this tray icon + + //advantage of polling vs callbacks: we can throw exceptions! + if (trayObj->resumeIsRequested()) throw AbortMonitoring(SHOW_GUI); - if (abortRequested) + if (trayObj->abortIsRequested()) throw AbortMonitoring(EXIT_APP); + } - if (readyForSync) - if (nextSyncStart_ <= wxGetLocalTime()) - throw StartSyncNowException(); //abort wait and start sync + void setMode(TrayMode m) { trayObj->setMode(m); } - if (updateUiIsAllowed()) - trayIcon.doUiRefreshNow(); - } + bool getShowErrorRequested() const { return trayObj->getShowErrorRequested(); } + void clearShowErrorRequested() { trayObj->clearShowErrorRequested(); } 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; + TrayIconObject* trayObj; }; +//############################################################################################################## - +//#define ERROR_DLG_ENABLE_TIMEOUT class ErrorDlgWithTimeout : public ErrorDlgGenerated { public: ErrorDlgWithTimeout(wxWindow* parent, const wxString& messageText) : - ErrorDlgGenerated(parent), - secondsLeft(15) //give user some time to read msg!? + ErrorDlgGenerated(parent) +#ifdef ERROR_DLG_ENABLE_TIMEOUT + , secondsLeft(15) //give user some time to read msg!? +#endif { #ifdef ZEN_WIN new zen::MouseMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere...; ownership passed to "this" @@ -257,11 +280,12 @@ public: m_bitmap10->SetBitmap(getResourceImage(L"msg_error")); m_textCtrl8->SetValue(messageText); +#ifdef ERROR_DLG_ENABLE_TIMEOUT //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(); - +#endif Fit(); //child-element widths have changed: image was set m_buttonRetry->SetFocus(); } @@ -273,6 +297,7 @@ public: }; private: +#ifdef ERROR_DLG_ENABLE_TIMEOUT void OnTimerEvent(wxEvent& event) { if (secondsLeft <= 0) @@ -289,20 +314,23 @@ private: m_buttonRetry->SetLabel(_("&Retry") + L" (" + replaceCpy(_P("1 sec", "%x sec", secondsLeft), L"%x", numberTo<std::wstring>(secondsLeft)) + L")"); Layout(); } +#endif void OnClose(wxCloseEvent& event) { EndModal(BUTTON_ABORT); } void OnRetry(wxCommandEvent& event) { EndModal(BUTTON_RETRY); } void OnAbort(wxCommandEvent& event) { EndModal(BUTTON_ABORT); } +#ifdef ERROR_DLG_ENABLE_TIMEOUT int secondsLeft; wxTimer timer; +#endif }; -bool reportErrorTimeout(const std::wstring& msg) //return true if timeout or user selected "retry", else abort +bool reportErrorTimeout(const std::wstring& msg) //return true: "retry"; false: "abort" { ErrorDlgWithTimeout errorDlg(nullptr, msg); - //errorDlg.Raise(); -> don't steal focus every X seconds + errorDlg.Raise(); switch (static_cast<ErrorDlgWithTimeout::ButtonPressed>(errorDlg.ShowModal())) { case ErrorDlgWithTimeout::BUTTON_RETRY: @@ -312,120 +340,90 @@ bool reportErrorTimeout(const std::wstring& msg) //return true if timeout or use } return false; } - - -inline -wxString toString(DirWatcher::ActionType type) -{ - switch (type) - { - case DirWatcher::ACTION_CREATE: - return L"CREATE"; - case DirWatcher::ACTION_UPDATE: - return L"UPDATE"; - case DirWatcher::ACTION_DELETE: - return L"DELETE"; - } - return L"ERROR"; -} } -/* -Data Flow: ----------- - -TrayIconHolder (GUI output) - /|\ - | -WaitCallbackImpl - /|\ - | -startDirectoryMonitor() (wire dir-changes and execution of commandline) -*/ - rts::AbortReason rts::startDirectoryMonitor(const xmlAccess::XmlRealConfig& config, const wxString& jobname) { - std::vector<Zstring> dirList = toZ(config.directories); - vector_remove_if(dirList, [](Zstring str) -> bool { trim(str); return str.empty(); }); //remove empty entries WITHOUT formatting dirList yet! + std::vector<Zstring> dirNamesNonFmt = config.directories; + vector_remove_if(dirNamesNonFmt, [](Zstring str) -> bool { trim(str); return str.empty(); }); //remove empty entries WITHOUT formatting paths yet! - if (dirList.empty()) + if (dirNamesNonFmt.empty()) { - wxMessageBox(_("A folder input field is empty."), _("Error"), wxOK | wxICON_ERROR); + wxMessageBox(_("A folder input field is empty."), L"RealtimeSync" + _("Error"), wxOK | wxICON_ERROR); return SHOW_GUI; } - wxString cmdLine = config.commandline; + Zstring cmdLine = config.commandline; trim(cmdLine); if (cmdLine.empty()) { - wxMessageBox(_("Invalid command line:") + L" \"\"", _("Error"), wxOK | wxICON_ERROR); + wxMessageBox(_("Invalid command line:") + L" \"\"", L"RealtimeSync" + _("Error"), wxOK | wxICON_ERROR); return SHOW_GUI; } - try + struct MonitorCallbackImpl : public MonitorCallback { - DirWatcher::Entry lastChangeDetected; - WaitCallbackImpl callback(jobname); + MonitorCallbackImpl(const wxString& jobname, + const Zstring& cmdLine) : trayIcon(jobname), cmdLine_(cmdLine) {} + + virtual void setPhase(WatchPhase mode) + { + switch (mode) + { + case MONITOR_PHASE_ACTIVE: + trayIcon.setMode(TRAY_MODE_ACTIVE); + break; + case MONITOR_PHASE_WAITING: + trayIcon.setMode(TRAY_MODE_WAITING); + break; + } + } - auto execMonitoring = [&] //throw FileError, AbortMonitoring + virtual void executeExternalCommand() { - callback.notifyDirectoryMissing(); - callback.clearSchedule(); - waitForMissingDirs(dirList, callback); //throw FileError, StartSyncNowException(not scheduled yet), AbortMonitoring - callback.notifyAllDirectoriesExist(); + auto cmdLineExp = expandMacros(cmdLine_); + zen::shellExecute(cmdLineExp, zen::EXEC_TYPE_SYNC); + } - //schedule initial execution (*after* all directories have arrived, which could take some time which we don't want to include) - callback.scheduleNextSync(wxGetLocalTime() + static_cast<long>(config.delay)); + virtual void requestUiRefresh() + { + if (updateUiIsAllowed()) + trayIcon.doUiRefreshNow(); //throw AbortMonitoring + } + + virtual void reportError(const std::wstring& msg) + { + trayIcon.setMode(TRAY_MODE_ERROR); + trayIcon.clearShowErrorRequested(); - while (true) + //wait for some time, then return to retry + assert_static(15 * 1000 % UI_UPDATE_INTERVAL == 0); + for (int i = 0; i < 15 * 1000 / UI_UPDATE_INTERVAL; ++i) { - try + trayIcon.doUiRefreshNow(); //throw AbortMonitoring + + if (trayIcon.getShowErrorRequested()) { - while (true) - { - //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.notifyDirectoryMissing(); - callback.clearSchedule(); - waitForMissingDirs(dirList, callback); //throw FileError, StartSyncNowException(not scheduled yet), AbortMonitoring - callback.notifyAllDirectoriesExist(); - break; - - case CHANGE_DETECTED: - lastChangeDetected = res.changedItem_; - break; - } - callback.scheduleNextSync(wxGetLocalTime() + static_cast<long>(config.delay)); - } + if (reportErrorTimeout(msg)) //return true: "retry"; false: "abort" + return; + else + throw AbortMonitoring(SHOW_GUI); } - catch (StartSyncNowException&) {} - - ::wxSetEnv(L"change_path", utfCvrtTo<wxString>(lastChangeDetected.filename_)); //some way to output what file changed to the user - ::wxSetEnv(L"change_action", toString(lastChangeDetected.action_)); // - lastChangeDetected = DirWatcher::Entry(); //make sure old name is not shown again after a directory reappears - - //execute command - auto cmdLineExp = expandMacros(utfCvrtTo<Zstring>(cmdLine)); - zen::shellExecute(cmdLineExp, zen::EXEC_TYPE_SYNC); - callback.clearSchedule(); + boost::this_thread::sleep(boost::posix_time::milliseconds(UI_UPDATE_INTERVAL)); } - }; + } - 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; - } + TrayIconHolder trayIcon; + const Zstring cmdLine_; + } cb(jobname, cmdLine); + + try + { + monitorDirectories(dirNamesNonFmt, config.delay, cb); //cb: throw AbortMonitoring + assert(false); + return SHOW_GUI; } catch (const AbortMonitoring& ab) { |