diff --git a/docs/changes.txt b/docs/changes.txt index 5e464c7632..45ac3db7ad 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -223,6 +223,7 @@ wxMSW: - Fix updating radio groups when non-radio item is inserted to wxMenu. - Fix autoselecting the contents of wxTextCtrl with wxWANTS_CHARS style. - Implement SetIcon(), SetPosition(), GetPosition() for native wxProgressDialog. +- Fix focus-related problems when using native wxProgressDialog. - Fix crash when reparenting the currently focused window to another TLW. wxOSX: diff --git a/include/wx/generic/progdlgg.h b/include/wx/generic/progdlgg.h index 77b90f76f4..ba8fb0bcc5 100644 --- a/include/wx/generic/progdlgg.h +++ b/include/wx/generic/progdlgg.h @@ -12,6 +12,7 @@ #define __PROGDLGH_G__ #include "wx/dialog.h" +#include "wx/weakref.h" class WXDLLIMPEXP_FWD_CORE wxButton; class WXDLLIMPEXP_FWD_CORE wxEventLoop; @@ -43,18 +44,18 @@ public: virtual bool Update(int value, const wxString& newmsg = wxEmptyString, bool *skip = NULL); virtual bool Pulse(const wxString& newmsg = wxEmptyString, bool *skip = NULL); - void Resume(); + virtual void Resume(); - int GetValue() const; - int GetRange() const; - wxString GetMessage() const; + virtual int GetValue() const; + virtual int GetRange() const; + virtual wxString GetMessage() const; - void SetRange(int maximum); + virtual void SetRange(int maximum); // Return whether "Cancel" or "Skip" button was pressed, always return // false if the corresponding button is not shown. - bool WasCancelled() const; - bool WasSkipped() const; + virtual bool WasCancelled() const; + virtual bool WasSkipped() const; // Must provide overload to avoid hiding it (and warnings about it) virtual void Update() wxOVERRIDE { wxDialog::Update(); } @@ -104,6 +105,9 @@ protected: // Converts seconds to HH:mm:ss format. static wxString GetFormattedTime(unsigned long timeInSec); + // Create a new event loop if there is no currently running one. + void EnsureActiveEventLoopExists(); + // callback for optional abort button void OnCancel(wxCommandEvent&); @@ -120,8 +124,8 @@ protected: // the dialog was shown void ReenableOtherWindows(); - // Set the top level parent we store from the parent window provided when - // creating the dialog. + // Store the parent window as wxWindow::m_parent and also set the top level + // parent reference we store in this class itself. void SetTopParent(wxWindow* parent); // return the top level parent window of this dialog (may be NULL) @@ -183,8 +187,9 @@ private: *m_estimated, *m_remaining; - // parent top level window (may be NULL) - wxWindow *m_parentTop; + // Reference to the parent top level window, automatically becomes NULL if + // it it is destroyed and could be always NULL if it's not given at all. + wxWindowRef m_parentTop; // Progress dialog styles: this is not the same as m_windowStyle because // wxPD_XXX constants clash with the existing TLW styles so to be sure we diff --git a/include/wx/msw/progdlg.h b/include/wx/msw/progdlg.h index 4c9dd94d54..c22a79db91 100644 --- a/include/wx/msw/progdlg.h +++ b/include/wx/msw/progdlg.h @@ -26,17 +26,17 @@ public: virtual bool Update(int value, const wxString& newmsg = wxEmptyString, bool *skip = NULL) wxOVERRIDE; virtual bool Pulse(const wxString& newmsg = wxEmptyString, bool *skip = NULL) wxOVERRIDE; - void Resume(); + virtual void Resume() wxOVERRIDE; - int GetValue() const; - wxString GetMessage() const; + virtual int GetValue() const wxOVERRIDE; + virtual wxString GetMessage() const wxOVERRIDE; - void SetRange(int maximum); + virtual void SetRange(int maximum) wxOVERRIDE; // Return whether "Cancel" or "Skip" button was pressed, always return // false if the corresponding button is not shown. - bool WasSkipped() const; - bool WasCancelled() const; + virtual bool WasSkipped() const wxOVERRIDE; + virtual bool WasCancelled() const wxOVERRIDE; virtual void SetTitle(const wxString& title) wxOVERRIDE; virtual wxString GetTitle() const wxOVERRIDE; @@ -44,6 +44,8 @@ public: virtual void SetIcons(const wxIconBundle& icons) wxOVERRIDE; virtual void DoMoveWindow(int x, int y, int width, int height) wxOVERRIDE; virtual void DoGetPosition(int *x, int *y) const wxOVERRIDE; + virtual void DoGetSize(int *width, int *height) const wxOVERRIDE; + virtual void Fit() wxOVERRIDE; virtual bool Show( bool show = true ) wxOVERRIDE; @@ -53,15 +55,24 @@ public: virtual WXWidget GetHandle() const wxOVERRIDE; private: - // Performs common routines to Update() and Pulse(). Requires the - // shared object to have been entered. + // Common part of Update() and Pulse(). + // + // Returns false if the user requested cancelling the dialog. bool DoNativeBeforeUpdate(bool *skip); + // Dispatch the pending events to let the windows to update, just as the + // generic version does. This is done as part of DoNativeBeforeUpdate(). + void DispatchEvents(); + // Updates the various timing informations for both determinate // and indeterminate modes. Requires the shared object to have // been entered. void UpdateExpandedInformation(int value); + // Get the task dialog geometry when using the native dialog. + wxRect GetTaskDialogRect() const; + + wxProgressDialogTaskRunner *m_taskDialogRunner; wxProgressDialogSharedData *m_sharedData; diff --git a/include/wx/msw/toplevel.h b/include/wx/msw/toplevel.h index 42c667b9a2..c79754f722 100644 --- a/include/wx/msw/toplevel.h +++ b/include/wx/msw/toplevel.h @@ -90,6 +90,9 @@ public: // NULL if getting the system menu failed. wxMenu *MSWGetSystemMenu() const; + // Enable or disable the close button of the specified window. + static bool MSWEnableCloseButton(WXHWND hwnd, bool enable = true); + // implementation from now on // -------------------------- diff --git a/interface/wx/progdlg.h b/interface/wx/progdlg.h index 0c8f60eb1f..589976076d 100644 --- a/interface/wx/progdlg.h +++ b/interface/wx/progdlg.h @@ -33,6 +33,26 @@ wxProgressDialog in a multi-threaded application you should be sure to use wxThreadEvent for your inter-threads communications). + Although wxProgressDialog is not really modal, it should be created on the + stack, and not the heap, as other modal dialogs, e.g. use it like this: + @code + void MyFrame::SomeFunc() + { + wxProgressDialog dialog(...); + for ( int i = 0; i < 100; ++i ) { + if ( !dialog.Update(i)) { + // Cancelled by user. + break; + } + + ... do something time-consuming (but not too much) ... + } + } + @endcode + Note that this becomes even more important if the dialog is instantiated + during the program initialization, e.g. from wxApp::OnInit(): the dialog + must be destroyed before the main event loop is started in this case. + @beginStyleTable @style{wxPD_APP_MODAL} Make the progress dialog modal. If this flag is not given, it is @@ -192,9 +212,19 @@ public: for the user to dismiss it, meaning that this function does not return until this happens. - Notice that you may want to call Fit() to change the dialog size to - conform to the length of the new message if desired. The dialog does - not do this automatically. + Notice that if @a newmsg is longer than the currently shown message, + the dialog will be automatically made wider to account for it. However + if the new message is shorter than the previous one, the dialog doesn't + shrink back to avoid constant resizes if the message is changed often. + To do this and fit the dialog to its current contents you may call + Fit() explicitly. However the native MSW implementation of this class + does make the dialog shorter if the new text has fewer lines of text + than the old one, so it is recommended to keep the number of lines of + text constant in order to avoid jarring dialog size changes. You may + also want to make the initial message, specified when creating the + dialog, wide enough to avoid having to resize the dialog later, e.g. by + appending a long string of unbreakable spaces (@c wxString(L'\u00a0', + 100)) to it. @param value The new value of the progress meter. It should be less than or equal to diff --git a/samples/dialogs/dialogs.cpp b/samples/dialogs/dialogs.cpp index 7ddc6701a5..6935218d87 100644 --- a/samples/dialogs/dialogs.cpp +++ b/samples/dialogs/dialogs.cpp @@ -233,6 +233,9 @@ wxBEGIN_EVENT_TABLE(MyFrame, wxFrame) #if wxUSE_PROGRESSDLG EVT_MENU(DIALOGS_PROGRESS, MyFrame::ShowProgress) +#ifdef wxHAS_NATIVE_PROGRESSDIALOG + EVT_MENU(DIALOGS_PROGRESS_GENERIC, MyFrame::ShowProgressGeneric) +#endif // wxHAS_NATIVE_PROGRESSDIALOG #endif // wxUSE_PROGRESSDLG EVT_MENU(DIALOGS_APP_PROGRESS, MyFrame::ShowAppProgress) @@ -349,7 +352,25 @@ bool MyApp::OnInit() ); for ( int i = 0; i <= PROGRESS_COUNT; i++ ) { - if ( !dlg.Update(i) ) + wxString msg; + switch ( i ) + { + case 15: + msg = "And the same dialog but with a very, very, very long" + " message, just to test how it appears in this case."; + break; + + case 30: + msg = "Back to brevity"; + break; + + case 80: + msg = "Back and adjusted"; + dlg.Fit(); + break; + } + + if ( !dlg.Update(i, msg) ) break; wxMilliSleep(50); @@ -490,6 +511,10 @@ bool MyApp::OnInit() #if wxUSE_PROGRESSDLG info_menu->Append(DIALOGS_PROGRESS, wxT("Pro&gress dialog\tCtrl-G")); + #ifdef wxHAS_NATIVE_PROGRESSDIALOG + info_menu->Append(DIALOGS_PROGRESS_GENERIC, + wxT("Generic progress dialog\tCtrl-Alt-G")); + #endif // wxHAS_NATIVE_PROGRESSDIALOG #endif // wxUSE_PROGRESSDLG info_menu->Append(DIALOGS_APP_PROGRESS, wxT("&App progress\tShift-Ctrl-G")); @@ -2652,10 +2677,10 @@ void MyFrame::OnExit(wxCommandEvent& WXUNUSED(event) ) #if wxUSE_PROGRESSDLG +static const int max = 100; + void MyFrame::ShowProgress( wxCommandEvent& WXUNUSED(event) ) { - static const int max = 100; - wxProgressDialog dialog("Progress dialog example", // "Reserve" enough space for the multiline // messages below, we'll change it anyhow @@ -2673,6 +2698,30 @@ void MyFrame::ShowProgress( wxCommandEvent& WXUNUSED(event) ) wxPD_SMOOTH // - makes indeterminate mode bar on WinXP very small ); + DoShowProgress(dialog); +} + +#ifdef wxHAS_NATIVE_PROGRESSDIALOG +void MyFrame::ShowProgressGeneric( wxCommandEvent& WXUNUSED(event) ) +{ + wxGenericProgressDialog dialog("Generic progress dialog example", + wxString(' ', 100) + "\n\n\n\n", + max, + this, + wxPD_CAN_ABORT | + wxPD_CAN_SKIP | + wxPD_APP_MODAL | + wxPD_ELAPSED_TIME | + wxPD_ESTIMATED_TIME | + wxPD_REMAINING_TIME | + wxPD_SMOOTH); + + DoShowProgress(dialog); +} +#endif // wxHAS_NATIVE_PROGRESSDIALOG + +void MyFrame::DoShowProgress(wxGenericProgressDialog& dialog) +{ bool cont = true; for ( int i = 0; i <= max; i++ ) { @@ -2721,8 +2770,8 @@ void MyFrame::ShowProgress( wxCommandEvent& WXUNUSED(event) ) { i += max/4; - if ( i >= 100 ) - i = 99; + if ( i >= max ) + i = max - 1; } if ( !cont ) @@ -2736,7 +2785,7 @@ void MyFrame::ShowProgress( wxCommandEvent& WXUNUSED(event) ) dialog.Resume(); } - wxMilliSleep(200); + wxMilliSleep(100); } if ( !cont ) @@ -2768,7 +2817,7 @@ void MyFrame::ShowAppProgress( wxCommandEvent& WXUNUSED(event) ) { progress.SetValue(i); - wxMilliSleep(500); + wxMilliSleep(200); } wxLogStatus("Progress finished"); diff --git a/samples/dialogs/dialogs.h b/samples/dialogs/dialogs.h index e611a355db..482d636e3a 100644 --- a/samples/dialogs/dialogs.h +++ b/samples/dialogs/dialogs.h @@ -452,6 +452,10 @@ public: #if wxUSE_PROGRESSDLG void ShowProgress(wxCommandEvent& event); +#ifdef wxHAS_NATIVE_PROGRESSDIALOG + void ShowProgressGeneric(wxCommandEvent& event); +#endif // wxHAS_NATIVE_PROGRESSDIALOG + void DoShowProgress(wxGenericProgressDialog& dialog); #endif // wxUSE_PROGRESSDLG void ShowAppProgress(wxCommandEvent& event); @@ -596,6 +600,7 @@ enum DIALOGS_ONTOP, DIALOGS_MODELESS_BTN, DIALOGS_PROGRESS, + DIALOGS_PROGRESS_GENERIC, DIALOGS_APP_PROGRESS, DIALOGS_ABOUTDLG_SIMPLE, DIALOGS_ABOUTDLG_FANCY, diff --git a/src/generic/progdlgg.cpp b/src/generic/progdlgg.cpp index e61586b2c9..03e1e7f933 100644 --- a/src/generic/progdlgg.cpp +++ b/src/generic/progdlgg.cpp @@ -133,6 +133,7 @@ wxGenericProgressDialog::wxGenericProgressDialog(const wxString& title, void wxGenericProgressDialog::SetTopParent(wxWindow* parent) { + m_parent = parent; m_parentTop = GetParentForModalDialog(parent, GetWindowStyle()); } @@ -144,13 +145,9 @@ bool wxGenericProgressDialog::Create( const wxString& title, { SetTopParent(parent); - m_parentTop = wxGetTopLevelParent(parent); m_pdStyle = style; - wxWindow* const - realParent = GetParentForModalDialog(parent, GetWindowStyle()); - - if (!wxDialog::Create(realParent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, GetWindowStyle())) + if (!wxDialog::Create(m_parentTop, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, GetWindowStyle())) return false; SetMaximum(maximum); @@ -160,11 +157,7 @@ bool wxGenericProgressDialog::Create( const wxString& title, // even if this means we have to start it ourselves (this happens most // commonly during the program initialization, e.g. for the progress // dialogs shown from overridden wxApp::OnInit()). - if ( !wxEventLoopBase::GetActive() ) - { - m_tempEventLoop = new wxEventLoop; - wxEventLoop::SetActive(m_tempEventLoop); - } + EnsureActiveEventLoopExists(); #if defined(__WXMSW__) && !defined(__WXUNIVERSAL__) // we have to remove the "Close" button from the title bar then as it is @@ -363,6 +356,15 @@ wxString wxGenericProgressDialog::GetFormattedTime(unsigned long timeInSec) return timeAsHMS; } +void wxGenericProgressDialog::EnsureActiveEventLoopExists() +{ + if ( !wxEventLoopBase::GetActive() ) + { + m_tempEventLoop = new wxEventLoop; + wxEventLoop::SetActive(m_tempEventLoop); + } +} + wxStaticText * wxGenericProgressDialog::CreateLabel(const wxString& text, wxSizer *sizer) { @@ -695,6 +697,21 @@ wxGenericProgressDialog::~wxGenericProgressDialog() if ( m_tempEventLoop ) { + // If another event loop has been installed as active during the life + // time of this object, we shouldn't deactivate it, but we also can't + // delete our m_tempEventLoop in this case because it risks leaving the + // new event loop with a dangling pointer, which it will set back as + // the active loop when it exits, resulting in a crash. So we have no + // choice but to just leak this pointer then, which is, of course, bad + // and usually easily avoidable by just destroying the progress dialog + // sooner, so warn the programmer about it. + wxCHECK_RET + ( + wxEventLoopBase::GetActive() == m_tempEventLoop, + "current event loop must not be changed during " + "wxGenericProgressDialog lifetime" + ); + wxEventLoopBase::SetActive(NULL); delete m_tempEventLoop; } @@ -765,8 +782,17 @@ void wxGenericProgressDialog::UpdateMessage(const wxString &newmsg) { if ( !newmsg.empty() && newmsg != m_msg->GetLabel() ) { + const wxSize sizeOld = m_msg->GetSize(); + m_msg->SetLabel(newmsg); + if ( m_msg->GetSize().x > sizeOld.x ) + { + // Resize the dialog to fit its new, longer contents instead of + // just truncating it. + Fit(); + } + // allow the window to repaint: // NOTE: since we yield only for UI events with this call, there // should be no side-effects diff --git a/src/msw/progdlg.cpp b/src/msw/progdlg.cpp index 462903ec35..46416402c5 100644 --- a/src/msw/progdlg.cpp +++ b/src/msw/progdlg.cpp @@ -57,7 +57,6 @@ const int wxSPDD_EXPINFO_CHANGED = 0x0020; const int wxSPDD_ENABLE_SKIP = 0x0040; const int wxSPDD_ENABLE_ABORT = 0x0080; const int wxSPDD_DISABLE_SKIP = 0x0100; -const int wxSPDD_DISABLE_ABORT = 0x0200; const int wxSPDD_FINISHED = 0x0400; const int wxSPDD_DESTROYED = 0x0800; const int wxSPDD_ICON_CHANGED = 0x1000; @@ -81,6 +80,7 @@ public: m_value = 0; m_progressBarMarquee = false; m_skipped = false; + m_msgChangeElementText = TDM_UPDATE_ELEMENT_TEXT; m_notifications = 0; m_parent = NULL; } @@ -105,9 +105,48 @@ public: bool m_progressBarMarquee; bool m_skipped; + // The task dialog message to use for changing the text of its elements: + // it is set to TDM_SET_ELEMENT_TEXT by Fit() to let the dialog adjust + // itself to the size of its elements during the next update, but otherwise + // TDM_UPDATE_ELEMENT_TEXT is used in order to prevent the dialog from + // performing a layout on each update, which is annoying as it can result + // in its size constantly changing. + int m_msgChangeElementText; + // Bit field that indicates fields that have been modified by the // main thread so the task dialog runner knows what to update. int m_notifications; + + + // Helper function to split a single message, passed via our public API, + // into the title and the main content body used by the native dialog. + // + // Note that it uses m_message and so must be called with m_cs locked. + void SplitMessageIntoTitleAndBody(wxString& title, wxString& body) const + { + title = m_message; + + const size_t posNL = title.find('\n'); + if ( posNL != wxString::npos ) + { + // There can an extra new line between the first and subsequent + // lines to separate them as it looks better with the generic + // version -- but in this one, they're already separated by the use + // of different dialog elements, so suppress the extra new line. + int numNLs = 1; + if ( posNL < title.length() - 1 && title[posNL + 1] == '\n' ) + numNLs++; + + body.assign(title, posNL + numNLs, wxString::npos); + title.erase(posNL); + } + else // A single line + { + // Don't use title without the body, this doesn't make sense. + body.clear(); + title.swap(body); + } + } }; // Runner thread that takes care of displaying and updating the @@ -182,6 +221,16 @@ BOOL CALLBACK DisplayCloseButton(HWND hwnd, LPARAM lParam) return TRUE; } +// This function enables or disables both the cancel button in the task dialog +// and the close button in its title bar, as they perform the same function and +// so should be kept in the same state. +void EnableCloseButtons(HWND hwnd, bool enable) +{ + ::SendMessage(hwnd, TDM_ENABLE_BUTTON, IDCANCEL, enable ? TRUE : FALSE); + + wxTopLevelWindow::MSWEnableCloseButton(hwnd, enable); +} + void PerformNotificationUpdates(HWND hwnd, wxProgressDialogSharedData *sharedData) { @@ -196,6 +245,33 @@ void PerformNotificationUpdates(HWND hwnd, if ( sharedData->m_notifications & wxSPDD_VALUE_CHANGED ) { + // Use a hack to avoid progress bar animation: we can't afford to use + // it because animating the gauge movement smoothly requires a + // constantly running message loop and while it does run in this (task + // dialog) thread, it is often blocked from proceeding by some lock + // held by the main thread which is busy doing something and may not + // dispatch events frequently enough. So, in practice, the animation + // can lag far behind the real value and results in showing a wrong + // value in the progress bar. + // + // To prevent this from happening, set the progress bar value to + // something greater than its maximal value and then move it back: in + // the current implementations of the progress bar control, moving its + // position backwards does it directly, without using the animation, + // which is exactly what we want here. + // + // Finally notice that this hack doesn't really work for the last + // value, but while we could use a nested hack and temporarily increase + // the progress bar range when the value is equal to it, it isn't + // actually necessary in practice because when we reach the end of the + // bar the dialog is either hidden immediately anyhow or the main + // thread enters a modal event loop which does dispatch events and so + // it's not a problem to have an animated transition in this particular + // case. + ::SendMessage( hwnd, + TDM_SET_PROGRESS_BAR_POS, + sharedData->m_value + 1, + 0 ); ::SendMessage( hwnd, TDM_SET_PROGRESS_BAR_POS, sharedData->m_value, @@ -235,39 +311,31 @@ void PerformNotificationUpdates(HWND hwnd, { // Split the message in the title string and the rest if it has // multiple lines. - wxString - title = sharedData->m_message, - body; - - const size_t posNL = title.find('\n'); - if ( posNL != wxString::npos ) - { - // There can an extra new line between the first and subsequent - // lines to separate them as it looks better with the generic - // version -- but in this one, they're already separated by the use - // of different dialog elements, so suppress the extra new line. - int numNLs = 1; - if ( posNL < title.length() - 1 && title[posNL + 1] == '\n' ) - numNLs++; - - body.assign(title, posNL + numNLs, wxString::npos); - title.erase(posNL); - } - else // A single line - { - // Don't use title without the body, this doesn't make sense. - title.swap(body); - } + wxString title, body; + sharedData->SplitMessageIntoTitleAndBody(title, body); ::SendMessage( hwnd, - TDM_SET_ELEMENT_TEXT, + sharedData->m_msgChangeElementText, TDE_MAIN_INSTRUCTION, wxMSW_CONV_LPARAM(title) ); ::SendMessage( hwnd, - TDM_SET_ELEMENT_TEXT, + sharedData->m_msgChangeElementText, TDE_CONTENT, wxMSW_CONV_LPARAM(body) ); + + // After using TDM_SET_ELEMENT_TEXT once, we don't want to use it for + // the subsequent updates as it could result in dialog size changing + // unexpectedly, so reset it (which does nothing if we had already done + // it, of course, but it's not a problem). + // + // Notice that, contrary to its documentation, even using this message + // still increases the dialog size if the new text is longer (at least + // under Windows 7), but it doesn't shrink back if the text becomes + // shorter later and stays at the bigger size which is still a big gain + // as it prevents jumping back and forth between the smaller and larger + // sizes. + sharedData->m_msgChangeElementText = TDM_UPDATE_ELEMENT_TEXT; } if ( sharedData->m_notifications & wxSPDD_EXPINFO_CHANGED ) @@ -276,8 +344,15 @@ void PerformNotificationUpdates(HWND hwnd, sharedData->m_expandedInformation; if ( !expandedInformation.empty() ) { + // Here we never need to use TDM_SET_ELEMENT_TEXT as the size of + // the expanded information doesn't change drastically. + // + // Notice that TDM_UPDATE_ELEMENT_TEXT for this element only works + // when using TDF_EXPAND_FOOTER_AREA, as we do. Without this flag, + // only TDM_SET_ELEMENT_TEXT could be used as otherwise the dialog + // layout becomes completely mangled (at least under Windows 7). ::SendMessage( hwnd, - TDM_SET_ELEMENT_TEXT, + TDM_UPDATE_ELEMENT_TEXT, TDE_EXPANDED_INFORMATION, wxMSW_CONV_LPARAM(expandedInformation) ); } @@ -287,14 +362,11 @@ void PerformNotificationUpdates(HWND hwnd, ::SendMessage( hwnd, TDM_ENABLE_BUTTON, Id_SkipBtn, TRUE ); if ( sharedData->m_notifications & wxSPDD_ENABLE_ABORT ) - ::SendMessage( hwnd, TDM_ENABLE_BUTTON, IDCANCEL, TRUE ); + EnableCloseButtons(hwnd, true); if ( sharedData->m_notifications & wxSPDD_DISABLE_SKIP ) ::SendMessage( hwnd, TDM_ENABLE_BUTTON, Id_SkipBtn, FALSE ); - if ( sharedData->m_notifications & wxSPDD_DISABLE_ABORT ) - ::SendMessage( hwnd, TDM_ENABLE_BUTTON, IDCANCEL, FALSE ); - // Is the progress finished? if ( sharedData->m_notifications & wxSPDD_FINISHED ) { @@ -304,7 +376,7 @@ void PerformNotificationUpdates(HWND hwnd, { // Change Cancel into Close and activate the button. ::SendMessage( hwnd, TDM_ENABLE_BUTTON, Id_SkipBtn, FALSE ); - ::SendMessage( hwnd, TDM_ENABLE_BUTTON, IDCANCEL, TRUE ); + EnableCloseButtons(hwnd, true); ::EnumChildWindows( hwnd, DisplayCloseButton, (LPARAM) sharedData ); } @@ -337,6 +409,7 @@ wxProgressDialog::wxProgressDialog( const wxString& title, SetPDStyle(style); SetMaximum(maximum); + EnsureActiveEventLoopExists(); Show(); DisableOtherWindows(); @@ -359,14 +432,49 @@ wxProgressDialog::~wxProgressDialog() m_sharedData->m_notifications |= wxSPDD_DESTROYED; } - m_taskDialogRunner->Wait(); + // We can't use simple wxThread::Wait() here as we would deadlock because + // the task dialog thread expects this thread to process some messages + // (presumably those the task dialog sends to its parent during its + // destruction). + const WXHANDLE hThread = m_taskDialogRunner->MSWGetHandle(); + for ( bool cont = true; cont; ) + { + DWORD rc = ::MsgWaitForMultipleObjects + ( + 1, // number of objects to wait for + (HANDLE *)&hThread, // the objects + false, // wait for any objects, not all + INFINITE, // no timeout + QS_ALLINPUT | // return as soon as there are any events + QS_ALLPOSTMESSAGE + ); - delete m_taskDialogRunner; + switch ( rc ) + { + case 0xFFFFFFFF: + // This is unexpected, but we can't do anything about it and + // probably shouldn't continue waiting as we risk doing it + // forever. + wxLogLastError("MsgWaitForMultipleObjectsEx"); + cont = false; + break; + case WAIT_OBJECT_0: + // Thread has terminated. + cont = false; + break; + + default: + // An event has arrive, so dispatch it. + wxEventLoop::GetActive()->Dispatch(); + } + } + + // Enable the windows before deleting the task dialog to ensure that we + // can regain the activation. ReenableOtherWindows(); - if ( GetTopParent() ) - GetTopParent()->Raise(); + delete m_taskDialogRunner; #endif // wxHAS_MSW_TASKDIALOG } @@ -375,21 +483,26 @@ bool wxProgressDialog::Update(int value, const wxString& newmsg, bool *skip) #ifdef wxHAS_MSW_TASKDIALOG if ( HasNativeTaskDialog() ) { + if ( !DoNativeBeforeUpdate(skip) ) + { + // Dialog was cancelled. + return false; + } + + value /= m_factor; + + wxASSERT_MSG( value <= m_maximum, wxT("invalid progress value") ); + { wxCriticalSectionLocker locker(m_sharedData->m_cs); - // Do nothing in canceled state. - if ( !DoNativeBeforeUpdate(skip) ) - return false; + if ( value != m_sharedData->m_value ) + { + m_sharedData->m_value = value; + m_sharedData->m_notifications |= wxSPDD_VALUE_CHANGED; + } - value /= m_factor; - - wxASSERT_MSG( value <= m_maximum, wxT("invalid progress value") ); - - m_sharedData->m_value = value; - m_sharedData->m_notifications |= wxSPDD_VALUE_CHANGED; - - if ( !newmsg.empty() ) + if ( !newmsg.empty() && newmsg != m_message ) { m_message = newmsg; m_sharedData->m_message = newmsg; @@ -445,11 +558,13 @@ bool wxProgressDialog::Pulse(const wxString& newmsg, bool *skip) #ifdef wxHAS_MSW_TASKDIALOG if ( HasNativeTaskDialog() ) { - wxCriticalSectionLocker locker(m_sharedData->m_cs); - - // Do nothing in canceled state. if ( !DoNativeBeforeUpdate(skip) ) + { + // Dialog was cancelled. return false; + } + + wxCriticalSectionLocker locker(m_sharedData->m_cs); if ( !m_sharedData->m_progressBarMarquee ) { @@ -457,15 +572,15 @@ bool wxProgressDialog::Pulse(const wxString& newmsg, bool *skip) m_sharedData->m_notifications |= wxSPDD_PBMARQUEE_CHANGED; } - if ( !newmsg.empty() ) + if ( !newmsg.empty() && newmsg != m_message ) { m_message = newmsg; m_sharedData->m_message = newmsg; m_sharedData->m_notifications |= wxSPDD_MESSAGE_CHANGED; } - // The value passed here doesn't matter, only elapsed time makes sense - // in indeterminate mode anyhow. + // Value of 0 is special and is used when we can't estimate the + // remaining and total times, which is exactly what we need here. UpdateExpandedInformation(0); return m_sharedData->m_state != Canceled; @@ -475,32 +590,49 @@ bool wxProgressDialog::Pulse(const wxString& newmsg, bool *skip) return wxGenericProgressDialog::Pulse( newmsg, skip ); } +void wxProgressDialog::DispatchEvents() +{ +#ifdef wxHAS_MSW_TASKDIALOG + // No need for HasNativeTaskDialog() check, we're only called when this is + // the case. + + // We don't need to dispatch the user input events as the task dialog + // handles its own ones in its thread and we shouldn't react to any + // other user actions while the dialog is shown. + wxEventLoop::GetActive()-> + YieldFor(wxEVT_CATEGORY_ALL & ~wxEVT_CATEGORY_USER_INPUT); +#else // !wxHAS_MSW_TASKDIALOG + wxFAIL_MSG( "unreachable" ); +#endif // wxHAS_MSW_TASKDIALOG/!wxHAS_MSW_TASKDIALOG +} + bool wxProgressDialog::DoNativeBeforeUpdate(bool *skip) { #ifdef wxHAS_MSW_TASKDIALOG - if ( HasNativeTaskDialog() ) + DispatchEvents(); + + wxCriticalSectionLocker locker(m_sharedData->m_cs); + + if ( m_sharedData->m_skipped ) { - if ( m_sharedData->m_skipped ) + if ( skip && !*skip ) { - if ( skip && !*skip ) - { - *skip = true; - m_sharedData->m_skipped = false; - m_sharedData->m_notifications |= wxSPDD_ENABLE_SKIP; - } + *skip = true; + m_sharedData->m_skipped = false; + m_sharedData->m_notifications |= wxSPDD_ENABLE_SKIP; } - - if ( m_sharedData->m_state == Canceled ) - m_timeStop = m_sharedData->m_timeStop; - - return m_sharedData->m_state != Canceled; } -#endif // wxHAS_MSW_TASKDIALOG + if ( m_sharedData->m_state == Canceled ) + m_timeStop = m_sharedData->m_timeStop; + + return m_sharedData->m_state != Canceled; +#else // !wxHAS_MSW_TASKDIALOG wxUnusedVar(skip); wxFAIL_MSG( "unreachable" ); return false; +#endif // wxHAS_MSW_TASKDIALOG/!wxHAS_MSW_TASKDIALOG } void wxProgressDialog::Resume() @@ -514,7 +646,7 @@ void wxProgressDialog::Resume() { wxCriticalSectionLocker locker(m_sharedData->m_cs); - m_sharedData->m_state = m_state; + m_sharedData->m_state = Continue; // "Skip" was disabled when "Cancel" had been clicked, so re-enable // it now. @@ -542,20 +674,16 @@ void wxProgressDialog::Resume() #endif // wxHAS_MSW_TASKDIALOG } -WXWidget wxProgressDialog::GetHandle() const -{ +WXWidget wxProgressDialog::GetHandle() const +{ #ifdef wxHAS_MSW_TASKDIALOG if ( HasNativeTaskDialog() ) { - HWND hwnd; - { - wxCriticalSectionLocker locker(m_sharedData->m_cs); - m_sharedData->m_state = m_state; - hwnd = m_sharedData->m_hwnd; - } - return hwnd; + wxCriticalSectionLocker locker(m_sharedData->m_cs); + return m_sharedData->m_hwnd; } -#endif +#endif // wxHAS_MSW_TASKDIALOG + return wxGenericProgressDialog::GetHandle(); } @@ -715,21 +843,33 @@ void wxProgressDialog::DoMoveWindow(int x, int y, int width, int height) wxGenericProgressDialog::DoMoveWindow(x, y, width, height); } +wxRect wxProgressDialog::GetTaskDialogRect() const +{ + wxRect r; + +#ifdef wxHAS_MSW_TASKDIALOG + if ( m_sharedData ) + { + wxCriticalSectionLocker locker(m_sharedData->m_cs); + r = wxRectFromRECT(wxGetWindowRect(m_sharedData->m_hwnd)); + } +#else // !wxHAS_MSW_TASKDIALOG + wxFAIL_MSG( "unreachable" ); +#endif // wxHAS_MSW_TASKDIALOG/!wxHAS_MSW_TASKDIALOG + + return r; +} + void wxProgressDialog::DoGetPosition(int *x, int *y) const { #ifdef wxHAS_MSW_TASKDIALOG if ( HasNativeTaskDialog() ) { - wxPoint pos; - { - wxCriticalSectionLocker locker(m_sharedData->m_cs); - m_sharedData->m_state = m_state; - pos = m_sharedData->m_winPosition; - } + const wxRect r = GetTaskDialogRect(); if (x) - *x = pos.x; + *x = r.x; if (y) - *y = pos.y; + *y = r.y; return; } @@ -738,6 +878,42 @@ void wxProgressDialog::DoGetPosition(int *x, int *y) const wxGenericProgressDialog::DoGetPosition(x, y); } +void wxProgressDialog::DoGetSize(int *width, int *height) const +{ +#ifdef wxHAS_MSW_TASKDIALOG + if ( HasNativeTaskDialog() ) + { + const wxRect r = GetTaskDialogRect(); + if ( width ) + *width = r.width; + if ( height ) + *height = r.height; + + return; + } +#endif // wxHAS_MSW_TASKDIALOG + + wxGenericProgressDialog::DoGetSize(width, height); +} + +void wxProgressDialog::Fit() +{ +#ifdef wxHAS_MSW_TASKDIALOG + if ( HasNativeTaskDialog() ) + { + wxCriticalSectionLocker locker(m_sharedData->m_cs); + + // Force the task dialog to use this message to adjust it layout. + m_sharedData->m_msgChangeElementText = TDM_SET_ELEMENT_TEXT; + + // Don't change the message, but pretend that it did change. + m_sharedData->m_notifications |= wxSPDD_MESSAGE_CHANGED; + } +#endif // wxHAS_MSW_TASKDIALOG + + wxGenericProgressDialog::Fit(); +} + bool wxProgressDialog::Show(bool show) { #ifdef wxHAS_MSW_TASKDIALOG @@ -777,8 +953,9 @@ bool wxProgressDialog::Show(bool show) wxPD_ESTIMATED_TIME | wxPD_REMAINING_TIME) ) { - // Use a non-empty string just to have the collapsible pane shown. - m_sharedData->m_expandedInformation = " "; + // Set the expanded information field from the beginning to avoid + // having to re-layout the dialog later when it changes. + UpdateExpandedInformation(0); } // Do launch the thread. @@ -794,6 +971,16 @@ bool wxProgressDialog::Show(bool show) return false; } + // Wait until the dialog is shown as the program may need some time + // before it calls Update() and we want to show something to the user + // in the meanwhile. + while ( wxEventLoop::GetActive()->Dispatch() ) + { + wxCriticalSectionLocker locker(m_sharedData->m_cs); + if ( m_sharedData->m_hwnd ) + break; + } + // Do not show the underlying dialog. return false; } @@ -810,14 +997,15 @@ void wxProgressDialog::UpdateExpandedInformation(int value) unsigned long remainingTime; UpdateTimeEstimates(value, elapsedTime, estimatedTime, remainingTime); - int realEstimatedTime = estimatedTime, - realRemainingTime = remainingTime; - if ( m_sharedData->m_progressBarMarquee ) + // The value of 0 is special, we can't estimate anything before we have at + // least one update, so leave the times dependent on it indeterminate. + // + // This value is also used by Pulse(), as in the indeterminate mode we can + // never estimate anything. + if ( !value ) { - // In indeterminate mode we don't have any estimation neither for the - // remaining nor for estimated time. - realEstimatedTime = - realRemainingTime = -1; + estimatedTime = + remainingTime = static_cast(-1); } wxString expandedInformation; @@ -837,7 +1025,7 @@ void wxProgressDialog::UpdateExpandedInformation(int value) expandedInformation << GetEstimatedLabel() << " " - << GetFormattedTime(realEstimatedTime); + << GetFormattedTime(estimatedTime); } if ( HasPDFlag(wxPD_REMAINING_TIME) ) @@ -847,7 +1035,7 @@ void wxProgressDialog::UpdateExpandedInformation(int value) expandedInformation << GetRemainingLabel() << " " - << GetFormattedTime(realRemainingTime); + << GetFormattedTime(remainingTime); } // Update with new timing information. @@ -875,8 +1063,20 @@ void* wxProgressDialogTaskRunner::Entry() { wxCriticalSectionLocker locker(m_sharedData.m_cs); + // If we have a parent, we must use it to have correct Z-order and + // icon, even if this comes at the price of attaching this thread input + // to the thread that created the parent window, i.e. the main thread. + wxTdc.parent = m_sharedData.m_parent; wxTdc.caption = m_sharedData.m_title.wx_str(); - wxTdc.message = m_sharedData.m_message.wx_str(); + + // Split the message into the title and main body text in the same way + // as it's done later in PerformNotificationUpdates() when the message + // is changed by Update() or Pulse(). + m_sharedData.SplitMessageIntoTitleAndBody + ( + wxTdc.message, + wxTdc.extendedMessage + ); // MSWCommonTaskDialogInit() will add an IDCANCEL button but we need to // give it the correct label. @@ -887,10 +1087,6 @@ void* wxProgressDialogTaskRunner::Entry() tdc.pfCallback = TaskDialogCallbackProc; tdc.lpCallbackData = (LONG_PTR) &m_sharedData; - // Undo some of the effects of MSWCommonTaskDialogInit(). - tdc.dwFlags &= ~TDF_EXPAND_FOOTER_AREA; // Expand in content area. - tdc.dwCommonButtons = 0; // Don't use common buttons. - if ( m_sharedData.m_style & wxPD_CAN_SKIP ) wxTdc.AddTaskDialogButton( tdc, Id_SkipBtn, 0, _("Skip") ); @@ -900,6 +1096,13 @@ void* wxProgressDialogTaskRunner::Entry() { tdc.pszExpandedInformation = m_sharedData.m_expandedInformation.t_str(); + + // If we have elapsed/estimated/... times to show, show them from + // the beginning for consistency with the generic version and also + // because showing them later may be very sluggish if the main + // thread doesn't update the dialog sufficiently frequently, while + // hiding them still works reasonably well. + tdc.dwFlags |= TDF_EXPANDED_BY_DEFAULT; } } @@ -930,6 +1133,10 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc LONG_PTR dwRefData ) { + bool endDialog = false; + + // Block for shared data critical section. + { wxProgressDialogSharedData * const sharedData = (wxProgressDialogSharedData *) dwRefData; @@ -941,45 +1148,20 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc // Store the HWND for the main thread use. sharedData->m_hwnd = hwnd; + // The main thread is sitting in an event dispatching loop waiting + // for this dialog to be shown, so make sure it does get an event. + wxWakeUpIdle(); + // Set the maximum value and disable Close button. ::SendMessage( hwnd, TDM_SET_PROGRESS_BAR_RANGE, 0, MAKELPARAM(0, sharedData->m_range) ); - // We always create this task dialog with NULL parent because our - // parent in wx sense is a window created from a different thread - // and so can't be used as our real parent. However we still center - // this window on the parent one as the task dialogs do with their - // real parent usually. - if ( sharedData->m_parent ) - { - wxRect rect(wxRectFromRECT(wxGetWindowRect(hwnd))); - rect = rect.CentreIn(sharedData->m_parent->GetRect()); - ::SetWindowPos(hwnd, - NULL, - rect.x, - rect.y, - -1, - -1, - SWP_NOACTIVATE | - SWP_NOOWNERZORDER | - SWP_NOSIZE | - SWP_NOZORDER); - } - - // Store current position for the main thread use - // if no position update is pending. - if ( !(sharedData->m_notifications & wxSPDD_WINDOW_MOVED) ) - { - RECT r = wxGetWindowRect(hwnd); - sharedData->m_winPosition = wxPoint(r.left, r.top); - } - // If we can't be aborted, the "Close" button will only be enabled // when the progress ends (and not even then with wxPD_AUTO_HIDE). if ( !(sharedData->m_style & wxPD_CAN_ABORT) ) - ::SendMessage( hwnd, TDM_ENABLE_BUTTON, IDCANCEL, FALSE ); + EnableCloseButtons(hwnd, false); break; case TDN_BUTTON_CLICKED: @@ -988,7 +1170,7 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc case Id_SkipBtn: ::SendMessage(hwnd, TDM_ENABLE_BUTTON, Id_SkipBtn, FALSE); sharedData->m_skipped = true; - return TRUE; + return S_FALSE; case IDCANCEL: if ( sharedData->m_state == wxProgressDialog::Finished ) @@ -998,7 +1180,7 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc sharedData->m_state = wxProgressDialog::Dismissed; // Let Windows close the dialog. - return FALSE; + return S_OK; } // Close button on the window triggers an IDCANCEL press, @@ -1006,26 +1188,48 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc // a finished dialog. if ( sharedData->m_style & wxPD_CAN_ABORT ) { - wxCHECK_MSG - ( - sharedData->m_state == wxProgressDialog::Continue, - TRUE, - "Dialog not in a cancelable state!" - ); + switch ( sharedData->m_state ) + { + case wxProgressDialog::Canceled: + // It can happen that we receive a second + // cancel request before we had time to process + // the first one, in which case simply do + // nothing for the subsequent one. + break; - ::SendMessage(hwnd, TDM_ENABLE_BUTTON, Id_SkipBtn, FALSE); - ::SendMessage(hwnd, TDM_ENABLE_BUTTON, IDCANCEL, FALSE); + case wxProgressDialog::Continue: + ::SendMessage(hwnd, TDM_ENABLE_BUTTON, Id_SkipBtn, FALSE); + EnableCloseButtons(hwnd, false); - sharedData->m_timeStop = wxGetCurrentTime(); - sharedData->m_state = wxProgressDialog::Canceled; + sharedData->m_timeStop = wxGetCurrentTime(); + sharedData->m_state = wxProgressDialog::Canceled; + break; + + // States which shouldn't be possible here: + + // We shouldn't have an (enabled) cancel button at + // all then. + case wxProgressDialog::Uncancelable: + + // This one was already dealt with above. + case wxProgressDialog::Finished: + + // Normally it shouldn't be possible to get any + // notifications after switching to this state. + case wxProgressDialog::Dismissed: + wxFAIL_MSG( "unreachable" ); + break; + } } - return TRUE; + return S_FALSE; } break; case TDN_TIMER: - PerformNotificationUpdates(hwnd, sharedData); + // Don't perform updates if nothing needs to be done. + if ( sharedData->m_notifications ) + PerformNotificationUpdates(hwnd, sharedData); /* Decide whether we should end the dialog. This is done if either @@ -1041,21 +1245,30 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc (sharedData->m_state == wxProgressDialog::Finished && sharedData->m_style & wxPD_AUTO_HIDE) ) { - ::EndDialog( hwnd, IDCLOSE ); + // Don't call EndDialog() from here as it could deadlock + // because we are inside the shared data critical section, do + // it below after leaving it instead. + endDialog = true; } sharedData->m_notifications = 0; - { - // Update current position for the main thread use. - RECT r = wxGetWindowRect(hwnd); - sharedData->m_winPosition = wxPoint(r.left, r.top); - } - return TRUE; + if ( endDialog ) + break; + + return S_FALSE; + } + + } // Leave shared data critical section. + + if ( endDialog ) + { + ::EndDialog( hwnd, IDCLOSE ); + return S_FALSE; } // Return anything. - return 0; + return S_OK; } #endif // wxHAS_MSW_TASKDIALOG diff --git a/src/msw/toplevel.cpp b/src/msw/toplevel.cpp index 2b815c74f6..4d7e00cc54 100644 --- a/src/msw/toplevel.cpp +++ b/src/msw/toplevel.cpp @@ -986,10 +986,11 @@ void wxTopLevelWindowMSW::SetIcons(const wxIconBundle& icons) DoSelectAndSetIcon(icons, SM_CXICON, SM_CYICON, ICON_BIG); } -bool wxTopLevelWindowMSW::EnableCloseButton(bool enable) +// static +bool wxTopLevelWindowMSW::MSWEnableCloseButton(WXHWND hwnd, bool enable) { // get system (a.k.a. window) menu - HMENU hmenu = GetSystemMenu(GetHwnd(), FALSE /* get it */); + HMENU hmenu = GetSystemMenu(hwnd, FALSE /* get it */); if ( !hmenu ) { // no system menu at all -- ok if we want to remove the close button @@ -1008,7 +1009,7 @@ bool wxTopLevelWindowMSW::EnableCloseButton(bool enable) return false; } // update appearance immediately - if ( !::DrawMenuBar(GetHwnd()) ) + if ( !::DrawMenuBar(hwnd) ) { wxLogLastError(wxT("DrawMenuBar")); } @@ -1016,6 +1017,11 @@ bool wxTopLevelWindowMSW::EnableCloseButton(bool enable) return true; } +bool wxTopLevelWindowMSW::EnableCloseButton(bool enable) +{ + return MSWEnableCloseButton(GetHwnd(), enable); +} + // Window must have wxCAPTION and either wxCLOSE_BOX or wxSYSTEM_MENU for the // button to be visible. Also check for wxMAXIMIZE_BOX because we don't want // to enable a button that is excluded from the current style.