summaryrefslogtreecommitdiff
path: root/RealtimeSync/tray_menu.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'RealtimeSync/tray_menu.cpp')
-rw-r--r--RealtimeSync/tray_menu.cpp440
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)
{
bgstack15