// Display the Progress Dialog for the progress on the completion of some // generic operation. This is most often used for Deleting, Uploading, Copying, // Moving and Downloading large numbers of files. #include "priv.h" #include "resource.h" #include "mluisupp.h" // this is how long we wait for the UI thread to create the progress hwnd before giving up #define WAIT_PROGRESS_HWND 10*1000 // ten seconds STDAPI CProgressDialog_CreateInstance(IUnknown* pUnkOuter, IUnknown** ppunk, LPCOBJECTINFO poi); class CProgressDialog : public IProgressDialog , public IOleWindow , public IActionProgressDialog , public IActionProgress , public IObjectWithSite { public: CProgressDialog(); // IUnknown STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP QueryInterface(REFIID riid, LPVOID * ppvObj); // IProgressDialog STDMETHODIMP StartProgressDialog(HWND hwndParent, IUnknown * punkEnableModless, DWORD dwFlags, LPCVOID pvResevered); STDMETHODIMP StopProgressDialog(void); STDMETHODIMP SetTitle(LPCWSTR pwzTitle); STDMETHODIMP SetAnimation(HINSTANCE hInstAnimation, UINT idAnimation); STDMETHODIMP_(BOOL) HasUserCancelled(void); STDMETHODIMP SetProgress(DWORD dwCompleted, DWORD dwTotal); STDMETHODIMP SetProgress64(ULONGLONG ullCompleted, ULONGLONG ullTotal); STDMETHODIMP SetLine(DWORD dwLineNum, LPCWSTR pwzString, BOOL fCompactPath, LPCVOID pvResevered); STDMETHODIMP SetCancelMsg(LPCWSTR pwzCancelMsg, LPCVOID pvResevered); STDMETHODIMP Timer(DWORD dwAction, LPCVOID pvResevered); // IOleWindow STDMETHODIMP GetWindow(HWND * phwnd); STDMETHODIMP ContextSensitiveHelp(BOOL fEnterMode) { return E_NOTIMPL; } // IActionProgressDialog STDMETHODIMP Initialize(SPINITF flags, LPCWSTR pszTitle, LPCWSTR pszCancel); STDMETHODIMP Stop(); // IActionProgress STDMETHODIMP Begin(SPACTION action, SPBEGINF flags); STDMETHODIMP UpdateProgress(ULONGLONG ulCompleted, ULONGLONG ulTotal); STDMETHODIMP UpdateText(SPTEXT sptext, LPCWSTR pszText, BOOL fMayCompact); STDMETHODIMP QueryCancel(BOOL * pfCancelled); STDMETHODIMP ResetCancel(); STDMETHODIMP End(); // IObjectWithSite STDMETHODIMP SetSite(IUnknown *punk) { IUnknown_Set(&_punkSite, punk); return S_OK; } STDMETHODIMP GetSite(REFIID riid, void **ppv) { *ppv = 0; return _punkSite ? _punkSite->QueryInterface(riid, ppv) : E_FAIL;} // Other Public Methods static INT_PTR CALLBACK ProgressDialogProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam); static DWORD CALLBACK ThreadProc(LPVOID pvThis) { return ((CProgressDialog *) pvThis)->_ThreadProc(); }; static DWORD CALLBACK SyncThreadProc(LPVOID pvThis) { return ((CProgressDialog *) pvThis)->_SyncThreadProc(); }; private: ~CProgressDialog(void); LONG _cRef; // State Accessible thru IProgressDialog LPWSTR _pwzTitle; // This will be used to cache the value passed to IProgressDialog::SetTitle() until the dialog is displayed UINT _idAnimation; HINSTANCE _hInstAnimation; LPWSTR _pwzLine1; // NOTE: LPWSTR _pwzLine2; // these are only used to init the dialog, otherwise, we just LPWSTR _pwzLine3; // call through on the main thread to update the dialog directly. LPWSTR _pwzCancelMsg; // If the user cancels, Line 1 & 2 will be cleared and Line 3 will get this msg. // Other internal state. HWND _hwndDlgParent; // parent window for message boxes HWND _hwndProgress; // dialog/progress window DWORD _dwFirstShowTime; // tick count when the dialog was first shown (needed so we don't flash it up for an instant) SPINITF _spinitf; SPBEGINF _spbeginf; IUnknown *_punkSite; HINSTANCE _hinstFree; BOOL _fCompletedChanged; // has the _dwCompleted changed since last time? BOOL _fTotalChanged; // has the _dwTotal changed since last time? BOOL _fChangePosted; // is there a change pending? BOOL _fCancel; BOOL _fTermThread; BOOL _fThreadRunning; BOOL _fInAction; BOOL _fMinimized; BOOL _fScaleBug; // Comctl32's PBM_SETRANGE32 msg will still cast it to an (int), so don't let the high bit be set. BOOL _fNoTime; BOOL _fReleaseSelf; BOOL _fInitialized; // Progress Values and Calculations DWORD _dwCompleted; // progress completed DWORD _dwTotal; // total progress DWORD _dwPrevRate; // previous progress rate (used for computing time remaining) DWORD _dwPrevTickCount; // the tick count when we last updated the progress time DWORD _dwPrevCompleted; // the ammount we had completed when we last updated the progress time DWORD _dwLastUpdatedTimeRemaining;// tick count when we last update the "Time remaining" field, we only update it every 5 seconds DWORD _dwLastUpdatedTickCount; // tick count when SetProgress was last called, used to calculate the rate UINT _iNumTimesSetProgressCalled;// how many times has the user called SetProgress? // Private Member Functions DWORD _ThreadProc(void); DWORD _SyncThreadProc(void); BOOL _OnInit(HWND hDlg); BOOL _ProgressDialogProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam); void _PauseAnimation(BOOL bStop); void _UpdateProgressDialog(void); void _AsyncUpdate(void); HRESULT _SetProgressTime(void); void _SetProgressTimeEst(DWORD dwSecondsLeft); void _UserCancelled(void); HRESULT _DisplayDialog(void); void _CompactProgressPath(LPCWSTR pwzStrIn, BOOL fCompactPath, UINT idDlgItem, LPWSTR pwzStrOut, DWORD cchSize); HRESULT _SetLineHelper(LPCWSTR pwzNew, LPWSTR * ppwzDest, UINT idDlgItem, BOOL fCompactPath); HRESULT _SetTitleBarProgress(DWORD dwCompleted, DWORD dwTotal); HRESULT _BeginAction(SPBEGINF flags); void _SetModeless(BOOL fModeless); void _ShowProgressBar(HWND hwnd); }; //#define TF_PROGRESS 0xFFFFFFFF #define TF_PROGRESS 0x00000000 // REVIEW, we should tune this size down as small as we can // to get smoother multitasking (without effecting performance) #define MIN_MINTIME4FEEDBACK 5 // is it worth showing estimated time to completion feedback? #define MS_TIMESLICE 2000 // ms, (MUST be > 1000!) first average time to completion estimate #define SHOW_PROGRESS_TIMEOUT 1000 // 1 second #define MINSHOWTIME 2000 // 2 seconds // progress dialog message #define PDM_SHUTDOWN WM_APP #define PDM_TERMTHREAD (WM_APP + 1) #define PDM_UPDATE (WM_APP + 2) // progress dialog timer messages #define ID_SHOWTIMER 1 #ifndef UNICODE #error "This code will only compile UNICODE for perf reasons. If you really need an ANSI browseui, write all the code to convert." #endif // !UNICODE // compacts path strings to fit into the Text1 and Text2 fields void CProgressDialog::_CompactProgressPath(LPCWSTR pwzStrIn, BOOL fCompactPath, UINT idDlgItem, LPWSTR pwzStrOut, DWORD cchSize) { WCHAR wzFinalPath[MAX_PATH]; LPWSTR pwzPathToUse = (LPWSTR)pwzStrIn; // We don't compact the path if the dialog isn't displayed yet. if (fCompactPath && _hwndProgress) { RECT rc; int cxWidth; StrCpyNW(wzFinalPath, (pwzStrIn ? pwzStrIn : L""), ARRAYSIZE(wzFinalPath)); // get the size of the text boxes HWND hwnd = GetDlgItem(_hwndProgress, idDlgItem); if (EVAL(hwnd)) { HDC hdc; HFONT hfont; HFONT hfontSave; hdc = GetDC(_hwndProgress); hfont = (HFONT)SendMessage(_hwndProgress, WM_GETFONT, 0, 0); hfontSave = (HFONT)SelectObject(hdc, hfont); GetWindowRect(hwnd, &rc); cxWidth = rc.right - rc.left; ASSERT(cxWidth >= 0); PathCompactPathW(hdc, wzFinalPath, cxWidth); SelectObject(hdc, hfontSave); ReleaseDC(_hwndProgress, hdc); } pwzPathToUse = wzFinalPath; } StrCpyNW(pwzStrOut, (pwzPathToUse ? pwzPathToUse : L""), cchSize); } HRESULT CProgressDialog::_SetLineHelper(LPCWSTR pwzNew, LPWSTR * ppwzDest, UINT idDlgItem, BOOL fCompactPath) { WCHAR wzFinalPath[MAX_PATH]; _CompactProgressPath(pwzNew, fCompactPath, idDlgItem, wzFinalPath, ARRAYSIZE(wzFinalPath)); Str_SetPtrW(ppwzDest, wzFinalPath); // No, so cache the value for later. // Does the dialog exist? if (_hwndProgress) SetDlgItemText(_hwndProgress, idDlgItem, wzFinalPath); return S_OK; } HRESULT CProgressDialog::_DisplayDialog(void) { TraceMsg(TF_PROGRESS, "CProgressDialog::_DisplayDialog()"); // Don't force ourselves into the foreground if a window we parented is already in the foreground: // This is part of the fix for NT bug 298163 (the confirm replace dialog was deactivated // by the progress dialog) HWND hwndCurrent = GetForegroundWindow(); BOOL fChildIsForeground = FALSE; while (NULL != (hwndCurrent = GetParent(hwndCurrent))) { if (_hwndProgress == hwndCurrent) { fChildIsForeground = TRUE; break; } } if (fChildIsForeground) { ShowWindow(_hwndProgress, SW_SHOWNOACTIVATE); } else { ShowWindow(_hwndProgress, SW_SHOW); SetForegroundWindow(_hwndProgress); } SetFocus(GetDlgItem(_hwndProgress, IDCANCEL)); return S_OK; } DWORD CProgressDialog::_SyncThreadProc() { _InitComCtl32(); // Get ready for the Native Font Control _hwndProgress = CreateDialogParam(MLGetHinst(), MAKEINTRESOURCE(DLG_PROGRESSDIALOG), _hwndDlgParent, ProgressDialogProc, (LPARAM)this); _fThreadRunning = (_hwndProgress != NULL); return TRUE; } DWORD CProgressDialog::_ThreadProc(void) { if (_hwndProgress) { // WARNING - copy perf goes way down if this is normal or // better priority. the default thread pri should be low. // however if there are situations in which it should be higher, // we can add SPBEGINF bits to support it. SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); SetTimer(_hwndProgress, ID_SHOWTIMER, SHOW_PROGRESS_TIMEOUT, NULL); // Did if finish while we slept? if (!_fTermThread) { // No, so display the dialog. MSG msg; while(GetMessage(&msg, NULL, 0, 0)) { if (_fTermThread && (GetTickCount() - _dwFirstShowTime) > MINSHOWTIME) { // we were signaled to finish and we have been visible MINSHOWTIME, // so its ok to quit break; } if (!IsDialogMessage(_hwndProgress, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } } DestroyWindow(_hwndProgress); _hwndProgress = NULL; } // this is for callers that dont call stop ENTERCRITICAL; _fThreadRunning = FALSE; if (_fReleaseSelf) Release(); LEAVECRITICAL; return 0; } DWORD FormatMessageWrapW(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageID, DWORD dwLangID, LPWSTR pwzBuffer, DWORD cchSize, ...) { va_list vaParamList; va_start(vaParamList, cchSize); DWORD dwResult = FormatMessageW(dwFlags, lpSource, dwMessageID, dwLangID, pwzBuffer, cchSize, &vaParamList); va_end(vaParamList); return dwResult; } DWORD FormatMessageWrapA(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageID, DWORD dwLangID, LPSTR pszBuffer, DWORD cchSize, ...) { va_list vaParamList; va_start(vaParamList, cchSize); DWORD dwResult = FormatMessageA(dwFlags, lpSource, dwMessageID, dwLangID, pszBuffer, cchSize, &vaParamList); va_end(vaParamList); return dwResult; } #define TIME_DAYS_IN_YEAR 365 #define TIME_HOURS_IN_DAY 24 #define TIME_MINUTES_IN_HOUR 60 #define TIME_SECONDS_IN_MINUTE 60 void _FormatMessageWrapper(LPCTSTR pszTemplate, DWORD dwNum1, DWORD dwNum2, LPTSTR pszOut, DWORD cchSize) { // Is FormatMessageWrapW implemented? if (g_bRunOnNT5) { FormatMessageWrapW(FORMAT_MESSAGE_FROM_STRING, pszTemplate, 0, 0, pszOut, cchSize, dwNum1, dwNum2); } else { CHAR szOutAnsi[MAX_PATH]; CHAR szTemplateAnsi[MAX_PATH]; SHTCharToAnsi(pszTemplate, szTemplateAnsi, ARRAYSIZE(szTemplateAnsi)); FormatMessageWrapA(FORMAT_MESSAGE_FROM_STRING, szTemplateAnsi, 0, 0, szOutAnsi, ARRAYSIZE(szOutAnsi), dwNum1, dwNum2); SHAnsiToTChar(szOutAnsi, pszOut, cchSize); } } #define CCH_TIMET_TEMPLATE_SIZE 120 // Should be good enough, even with localization bloat. #define CCH_TIME_SIZE 170 // Should be good enough, even with localization bloat. void _SetProgressLargeTimeEst(DWORD dwSecondsLeft, LPTSTR pszOut, DWORD cchSize) { // Yes. TCHAR szTemplate[CCH_TIMET_TEMPLATE_SIZE]; DWORD dwMinutes = (dwSecondsLeft / TIME_SECONDS_IN_MINUTE); DWORD dwHours = (dwMinutes / TIME_MINUTES_IN_HOUR); DWORD dwDays = (dwHours / TIME_HOURS_IN_DAY); if (dwDays) { dwHours %= TIME_HOURS_IN_DAY; // It's more than a day, so display days and hours. if (1 == dwDays) { if (1 == dwHours) LoadString(MLGetHinst(), IDS_TIMEEST_DAYHOUR, szTemplate, ARRAYSIZE(szTemplate)); else LoadString(MLGetHinst(), IDS_TIMEEST_DAYHOURS, szTemplate, ARRAYSIZE(szTemplate)); } else { if (1 == dwHours) LoadString(MLGetHinst(), IDS_TIMEEST_DAYSHOUR, szTemplate, ARRAYSIZE(szTemplate)); else LoadString(MLGetHinst(), IDS_TIMEEST_DAYSHOURS, szTemplate, ARRAYSIZE(szTemplate)); } _FormatMessageWrapper(szTemplate, dwDays, dwHours, pszOut, cchSize); } else { // It's let than a day, so display hours and minutes. dwMinutes %= TIME_MINUTES_IN_HOUR; // It's more than a day, so display days and hours. if (1 == dwHours) { if (1 == dwMinutes) LoadString(MLGetHinst(), IDS_TIMEEST_HOURMIN, szTemplate, ARRAYSIZE(szTemplate)); else LoadString(MLGetHinst(), IDS_TIMEEST_HOURMINS, szTemplate, ARRAYSIZE(szTemplate)); } else { if (1 == dwMinutes) LoadString(MLGetHinst(), IDS_TIMEEST_HOURSMIN, szTemplate, ARRAYSIZE(szTemplate)); else LoadString(MLGetHinst(), IDS_TIMEEST_HOURSMINS, szTemplate, ARRAYSIZE(szTemplate)); } _FormatMessageWrapper(szTemplate, dwHours, dwMinutes, pszOut, cchSize); } } // This sets the "Seconds Left" text in the progress dialog void CProgressDialog::_SetProgressTimeEst(DWORD dwSecondsLeft) { TCHAR szFmt[CCH_TIMET_TEMPLATE_SIZE]; TCHAR szOut[CCH_TIME_SIZE]; DWORD dwTime; DWORD dwTickCount = GetTickCount(); // Since the progress time has either a 1 minute or 5 second granularity (depending on whether the total time // remaining is greater or less than 1 minute), we only update it every 20 seconds if the total time is > 1 minute, // and ever 4 seconds if the time is < 1 minute. This keeps the time from flashing back and forth between // boundaries (eg 45 secondsand 40 seconds remaining). if (dwTickCount - _dwLastUpdatedTimeRemaining < (DWORD)((dwSecondsLeft > 60) ? 20000 : 4000)) return; if (_fNoTime) { szOut[0] = TEXT('\0'); } else { // Is it more than an hour? if (dwSecondsLeft > (TIME_SECONDS_IN_MINUTE * TIME_MINUTES_IN_HOUR)) _SetProgressLargeTimeEst(dwSecondsLeft, szOut, ARRAYSIZE(szOut)); else { // No. if (dwSecondsLeft > TIME_SECONDS_IN_MINUTE) { // Note that dwTime is at least 2, so we only need a plural form LoadString(MLGetHinst(), IDS_TIMEEST_MINUTES, szFmt, ARRAYSIZE(szFmt)); dwTime = (dwSecondsLeft / TIME_SECONDS_IN_MINUTE) + 1; } else { LoadString(MLGetHinst(), IDS_TIMEEST_SECONDS, szFmt, ARRAYSIZE(szFmt)); // Round up to 5 seconds so it doesn't look so random dwTime = ((dwSecondsLeft + 4) / 5) * 5; } wnsprintf(szOut, ARRAYSIZE(szOut), szFmt, dwTime); } } // we are updating now, so set the _dwLastUpdatedTimeRemaining to now _dwLastUpdatedTimeRemaining = dwTickCount; // update the Time remaining field SetDlgItemText(_hwndProgress, IDD_PROGDLG_LINE3, szOut); } #define MAX(x, y) ((x) > (y) ? (x) : (y)) // // This function updates the ProgressTime field (aka Line3) // HRESULT CProgressDialog::_SetProgressTime(void) { DWORD dwSecondsLeft; DWORD dwTotal; DWORD dwCompleted; DWORD dwCurrentRate; DWORD dwTickDelta; DWORD dwLeft; DWORD dwCurrentTickCount; _iNumTimesSetProgressCalled++; // grab these in the crit sec (because they can change, and we need a matched set) ENTERCRITICAL; dwTotal = _dwTotal; dwCompleted = _dwCompleted; dwCurrentTickCount = _dwLastUpdatedTickCount; LEAVECRITICAL; dwLeft = dwTotal - dwCompleted; dwTickDelta = dwCurrentTickCount - _dwPrevTickCount; if (!dwTotal || !dwCompleted) return dwTotal ? S_FALSE : E_FAIL; // we divide the TickDelta by 100 to give tenths of seconds, so if we have recieved an // update faster than that, just skip it if (dwTickDelta < 100) { return S_FALSE; } TraceMsg(TF_PROGRESS, "Current tick count = %lu", dwCurrentTickCount); TraceMsg(TF_PROGRESS, "Total work = %lu", dwTotal); TraceMsg(TF_PROGRESS, "Completed work = %lu", dwCompleted); TraceMsg(TF_PROGRESS, "Prev. comp work= %lu", _dwPrevCompleted); TraceMsg(TF_PROGRESS, "Work left = %lu", dwLeft); TraceMsg(TF_PROGRESS, "Tick delta = %lu", dwTickDelta); if (dwTotal < dwCompleted) { // we can get into this case if we are applying attributes to sparse files // on a volume. As we add up the file sizes, we end up with a number that is bigger // than the drive size. We get rid of the time so that we wont show the user something // completely bogus _fNoTime = TRUE; dwTotal = dwCompleted + (dwCompleted >> 3); // fudge dwTotal forward a bit TraceMsg(TF_PROGRESS, "!! (Total < Completed), fudging Total work to = %lu", dwTotal); } if(dwCompleted <= _dwPrevCompleted) { // woah, we are going backwards, we dont deal w/ negative or zero rates so... dwCurrentRate = (_dwPrevRate ? _dwPrevRate : 2); } else { // calculate the current rate in points per tenth of a second dwTickDelta /= 100; if (0 == dwTickDelta) dwTickDelta = 1; // Protect from divide by zero dwCurrentRate = (dwCompleted - _dwPrevCompleted) / dwTickDelta; } TraceMsg(TF_PROGRESS, "Current rate = %lu", dwCurrentRate); TraceMsg(TF_PROGRESS, "Prev. rate = %lu", _dwPrevRate); // time remaining in seconds (we take a REAL average to smooth out random fluxuations) DWORD dwAverageRate = (DWORD)((dwCurrentRate + (_int64)_dwPrevRate * _iNumTimesSetProgressCalled) / (_iNumTimesSetProgressCalled + 1)); TraceMsg(TF_PROGRESS, "Average rate= %lu", dwAverageRate); dwAverageRate = MAX(dwAverageRate, 1); // Protect from divide by zero dwSecondsLeft = (dwLeft / dwAverageRate) / 10; TraceMsg(TF_PROGRESS, "Seconds left = %lu", dwSecondsLeft); TraceMsg(TF_PROGRESS, ""); // It would be odd to show "1 second left" and then immediately clear it, and to avoid showing // rediculous early estimates, we dont show anything until we have at least 5 data points if ((dwSecondsLeft >= MIN_MINTIME4FEEDBACK) && (_iNumTimesSetProgressCalled >= 5)) { // display new estimate of time left _SetProgressTimeEst(dwSecondsLeft); } // set all the _dwPrev stuff for next time _dwPrevRate = dwAverageRate; _dwPrevTickCount = dwCurrentTickCount; _dwPrevCompleted = dwCompleted; return S_OK; } INT_PTR CALLBACK CProgressDialog::ProgressDialogProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam) { CProgressDialog * ppd = (CProgressDialog *)GetWindowLongPtr(hDlg, DWLP_USER); if (WM_INITDIALOG == wMsg) { SetWindowLongPtr(hDlg, DWLP_USER, lParam); ppd = (CProgressDialog *)lParam; } if (ppd) return ppd->_ProgressDialogProc(hDlg, wMsg, wParam, lParam); return DefWindowProc(hDlg, wMsg, wParam, lParam); } // apithk.c entry STDAPI_(void) ProgressSetMarqueeMode(HWND hwndProgress, BOOL bOn); void CProgressDialog::_ShowProgressBar(HWND hwnd) { if (hwnd) { HWND hwndPrgress = GetDlgItem(hwnd, IDD_PROGDLG_PROGRESSBAR); ProgressSetMarqueeMode(hwndPrgress, (SPBEGINF_MARQUEEPROGRESS & _spbeginf)); UINT swp = SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE; if (SPBEGINF_NOPROGRESSBAR & _spbeginf) swp |= SWP_HIDEWINDOW; else swp |= SWP_SHOWWINDOW; SetWindowPos(hwndPrgress, NULL, 0, 0, 0, 0, swp); } } BOOL CProgressDialog::_OnInit(HWND hDlg) { // dont minimize if the caller requests or is modal if ((SPINITF_MODAL | SPINITF_NOMINIMIZE) & _spinitf) { // The caller wants us to remove the Minimize Box or button in the caption bar. SHSetWindowBits(hDlg, GWL_STYLE, WS_MINIMIZEBOX, 0); } _ShowProgressBar(hDlg); return FALSE; } BOOL CProgressDialog::_ProgressDialogProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam) { BOOL fHandled = TRUE; // handled switch (wMsg) { case WM_INITDIALOG: return _OnInit(hDlg); case WM_SHOWWINDOW: if (wParam) { _SetModeless(FALSE); ASSERT(_hwndProgress); SetAnimation(_hInstAnimation, _idAnimation); // set the initial text values if (_pwzTitle) SetTitle(_pwzTitle); if (_pwzLine1) SetLine(1, _pwzLine1, FALSE, NULL); if (_pwzLine2) SetLine(2, _pwzLine2, FALSE, NULL); if (_pwzLine3) SetLine(3, _pwzLine3, FALSE, NULL); } break; case WM_DESTROY: _SetModeless(TRUE); if (_hwndDlgParent) { if (SHIsChildOrSelf(_hwndProgress, GetFocus())) SetForegroundWindow(_hwndDlgParent); } break; case WM_ENABLE: if (wParam) { // we assume that we were previously disabled and thus restart our tick counter // because we also naively assume that no work was being done while we were disabled _dwPrevTickCount = GetTickCount(); } _PauseAnimation(wParam == 0); break; case WM_TIMER: if (wParam == ID_SHOWTIMER) { KillTimer(hDlg, ID_SHOWTIMER); _DisplayDialog(); _dwFirstShowTime = GetTickCount(); } break; case WM_COMMAND: if (IDCANCEL == GET_WM_COMMAND_ID(wParam, lParam)) _UserCancelled(); break; case PDM_SHUTDOWN: // Make sure this window is shown before telling the user there // is a problem. Ignore FOF_NOERRORUI here because of the // nature of the situation MLShellMessageBox(hDlg, MAKEINTRESOURCE(IDS_CANTSHUTDOWN), NULL, (MB_OK | MB_ICONEXCLAMATION | MB_SETFOREGROUND)); break; case PDM_TERMTHREAD: // a dummy id that we can take so that folks can post to us and make // us go through the main loop break; case WM_SYSCOMMAND: switch(wParam) { case SC_MINIMIZE: _fMinimized = TRUE; break; case SC_RESTORE: SetTitle(_pwzTitle); // Restore title to original text. _fMinimized = FALSE; break; } fHandled = FALSE; break; case PDM_UPDATE: if (!_fCancel && IsWindowEnabled(hDlg)) { _SetProgressTime(); _UpdateProgressDialog(); } // we are done processing the update _fChangePosted = FALSE; break; case WM_QUERYENDSESSION: // Post a message telling the dialog to show the "We can't shutdown now" // dialog and return to USER right away, so we don't have to worry about // the user not clicking the OK button before USER puts up its "this // app didn't respond" dialog PostMessage(hDlg, PDM_SHUTDOWN, 0, 0); // Make sure the dialog box procedure returns FALSE SetWindowLongPtr(hDlg, DWLP_MSGRESULT, FALSE); return(TRUE); default: fHandled = FALSE; // Not handled } return fHandled; } // This is used to asyncronously update the progess dialog. void CProgressDialog::_AsyncUpdate(void) { if (!_fChangePosted && _hwndProgress) // Prevent from posting too many messages. { // set the flag first because with async threads // the progress window could handle it and clear the // bit before we set it.. then we'd lose further messages // thinking that one was still pending _fChangePosted = TRUE; if (!PostMessage(_hwndProgress, PDM_UPDATE, 0, 0)) { _fChangePosted = FALSE; } } } void CProgressDialog::_UpdateProgressDialog(void) { if (_fTotalChanged) { _fTotalChanged = FALSE; if (0x80000000 & _dwTotal) _fScaleBug = TRUE; SendMessage(GetDlgItem(_hwndProgress, IDD_PROGDLG_PROGRESSBAR), PBM_SETRANGE32, 0, (_fScaleBug ? (_dwTotal >> 1) : _dwTotal)); } if (_fCompletedChanged) { _fCompletedChanged = FALSE; SendMessage(GetDlgItem(_hwndProgress, IDD_PROGDLG_PROGRESSBAR), PBM_SETPOS, (WPARAM) (_fScaleBug ? (_dwCompleted >> 1) : _dwCompleted), 0); } } void CProgressDialog::_PauseAnimation(BOOL bStop) { // only called from within the hwndProgress wndproc so assum it's there if (_hwndProgress) { if (bStop) { Animate_Stop(GetDlgItem(_hwndProgress, IDD_PROGDLG_ANIMATION)); } else { Animate_Play(GetDlgItem(_hwndProgress, IDD_PROGDLG_ANIMATION), -1, -1, -1); } } } void CProgressDialog::_UserCancelled(void) { // Don't hide the dialog because the caller may not pole // ::HasUserCancelled() for quite a while. // ShowWindow(hDlg, SW_HIDE); _fCancel = TRUE; // give minimal feedback that the cancel click was accepted EnableWindow(GetDlgItem(_hwndProgress, IDCANCEL), FALSE); // If the user cancels, Line 1 & 2 will be cleared and Line 3 will get this msg. if (!_pwzCancelMsg) { WCHAR wzDefaultMsg[MAX_PATH]; LoadStringW(MLGetHinst(), IDS_DEFAULT_CANCELPROG, wzDefaultMsg, ARRAYSIZE(wzDefaultMsg)); Str_SetPtr(&_pwzCancelMsg, wzDefaultMsg); } SetLine(1, L"", FALSE, NULL); SetLine(2, L"", FALSE, NULL); SetLine(3, _pwzCancelMsg, FALSE, NULL); } HRESULT CProgressDialog::Initialize(SPINITF flags, LPCWSTR pszTitle, LPCWSTR pszCancel) { if (!_fInitialized) { _spinitf = flags; if (pszTitle) SetTitle(pszTitle); if (pszCancel) SetCancelMsg(pszCancel, NULL); _fInitialized = TRUE; return S_OK; } return E_UNEXPECTED; } void CProgressDialog::_SetModeless(BOOL fModeless) { // if the user is requesting a modal window, disable the parent now. if (_spinitf & SPINITF_MODAL) { if (FAILED(IUnknown_EnableModless(_punkSite, fModeless)) && _hwndDlgParent) { EnableWindow(_hwndDlgParent, fModeless); } } } HRESULT CProgressDialog::_BeginAction(SPBEGINF flags) { _spbeginf = flags; _fTermThread = FALSE; _fTotalChanged = TRUE; if (!_fThreadRunning) { SHCreateThread(CProgressDialog::ThreadProc, this, CTF_FREELIBANDEXIT, CProgressDialog::SyncThreadProc); // _fThreadRunning is set in _SyncThreadProc() } if (_fThreadRunning) { _fInAction = TRUE; _ShowProgressBar(_hwndProgress); // initialize the _dwPrev counters _dwPrevRate = 0; _dwPrevCompleted = 0; _dwPrevTickCount = GetTickCount(); TraceMsg(TF_PROGRESS, "Initial tick count = %lu", _dwPrevTickCount); return S_OK; } return E_OUTOFMEMORY; } #define ACTIONENTRY(a, dll, id) {a, dll, id} #define c_szShell32 "shell32.dll" #define c_szShdocvw "shdocvw.dll" const static struct { SPACTION action; LPCSTR pszDll; UINT id; } c_spActions[] = { ACTIONENTRY(SPACTION_MOVING, c_szShell32, 160), // IDA_FILEMOVE ACTIONENTRY(SPACTION_COPYING, c_szShell32, 161), // IDA_FILECOPY ACTIONENTRY(SPACTION_RECYCLING, c_szShell32, 162), // IDA_FILEDEL ACTIONENTRY(SPACTION_APPLYINGATTRIBS, c_szShell32, 165), // IDA_APPLYATTRIBS ACTIONENTRY(SPACTION_DOWNLOADING, c_szShdocvw, 0x100), ACTIONENTRY(SPACTION_SEARCHING_INTERNET, c_szShell32, 166), // IDA_ISEARCH ACTIONENTRY(SPACTION_SEARCHING_FILES, c_szShell32, 150) // IDA_SEARCH }; HRESULT CProgressDialog::Begin(SPACTION action, SPBEGINF flags) { if (_fInAction || !_fInitialized) return E_FAIL; HRESULT hr = S_OK; for (int i = 0; i < ARRAYSIZE(c_spActions); i++) { if (c_spActions[i].action == action) { HINSTANCE hinst = LoadLibraryA(c_spActions[i].pszDll); if (hinst) { hr = SetAnimation(hinst, c_spActions[i].id); if (_hinstFree) FreeLibrary(_hinstFree); _hinstFree = hinst; } break; } } if (SUCCEEDED(hr)) { if (!_hwndDlgParent) IUnknown_GetWindow(_punkSite, &_hwndDlgParent); hr = _BeginAction(flags); } return hr; } #define SPINIT_MASK (SPINITF_MODAL | SPINITF_NOMINIMIZE) #define SPBEGIN_MASK 0x1F // IProgressDialog HRESULT CProgressDialog::StartProgressDialog(HWND hwndParent, IUnknown * punkNotUsed, DWORD dwFlags, LPCVOID pvResevered) { if (_fInAction) return S_OK; HRESULT hr = Initialize(dwFlags & SPINIT_MASK, NULL, NULL); if (SUCCEEDED(hr)) { _fNoTime = dwFlags & PROGDLG_NOTIME; // we dont Save punkNotUsed _hwndDlgParent = hwndParent; hr = _BeginAction(dwFlags & SPBEGIN_MASK); } return hr; } HRESULT CProgressDialog::End() { ASSERT(_fInitialized && _fInAction); // possibly need to pop stack or change state _fInAction = FALSE; _spbeginf = 0; return S_OK; } HRESULT CProgressDialog::Stop() { ASSERT(!_fInAction); BOOL fFocusParent = FALSE; // shut down the progress dialog if (_fThreadRunning) { ASSERT(_hwndProgress); _fTermThread = TRUE; PostMessage(_hwndProgress, PDM_TERMTHREAD, 0, 0); } return S_OK; } HRESULT CProgressDialog::StopProgressDialog(void) { // callers can call this over and over if (_fInAction) End(); return Stop(); } HRESULT CProgressDialog::SetTitle(LPCWSTR pwzTitle) { HRESULT hr = S_OK; // Does the dialog exist? if (_hwndProgress) { // Yes, so put the value directly into the dialog. if (!SetWindowTextW(_hwndProgress, (pwzTitle ? pwzTitle : L""))) hr = E_FAIL; } else Str_SetPtrW(&_pwzTitle, pwzTitle); return hr; } HRESULT CProgressDialog::SetAnimation(HINSTANCE hInstAnimation, UINT idAnimation) { HRESULT hr = S_OK; _hInstAnimation = hInstAnimation; _idAnimation = idAnimation; // Does the dialog exist? if (_hwndProgress) { if (!Animate_OpenEx(GetDlgItem(_hwndProgress, IDD_PROGDLG_ANIMATION), _hInstAnimation, IntToPtr(_idAnimation))) hr = E_FAIL; } return hr; } HRESULT CProgressDialog::UpdateText(SPTEXT sptext, LPCWSTR pszText, BOOL fMayCompact) { if (_fInitialized) return SetLine((DWORD)sptext, pszText, fMayCompact, NULL); else return E_UNEXPECTED; } HRESULT CProgressDialog::SetLine(DWORD dwLineNum, LPCWSTR pwzString, BOOL fCompactPath, LPCVOID pvResevered) { HRESULT hr = E_INVALIDARG; switch (dwLineNum) { case 1: hr = _SetLineHelper(pwzString, &_pwzLine1, IDD_PROGDLG_LINE1, fCompactPath); break; case 2: hr = _SetLineHelper(pwzString, &_pwzLine2, IDD_PROGDLG_LINE2, fCompactPath); break; case 3: if (_spbeginf & SPBEGINF_AUTOTIME) { // you cant change line3 directly if you want PROGDLG_AUTOTIME, because // this is updated by the progress dialog automatically // unless we're cancelling ASSERT(_fCancel); hr = _fCancel ? S_OK : E_INVALIDARG; break; } hr = _SetLineHelper(pwzString, &_pwzLine3, IDD_PROGDLG_LINE3, fCompactPath); break; default: ASSERT(0); } return hr; } HRESULT CProgressDialog::SetCancelMsg(LPCWSTR pwzCancelMsg, LPCVOID pvResevered) { Str_SetPtr(&_pwzCancelMsg, pwzCancelMsg); // If the user cancels, Line 1 & 2 will be cleared and Line 3 will get this msg. return S_OK; } HRESULT CProgressDialog::Timer(DWORD dwAction, LPCVOID pvResevered) { HRESULT hr = E_NOTIMPL; switch (dwAction) { case PDTIMER_RESET: _dwPrevTickCount = GetTickCount(); hr = S_OK; break; } return hr; } HRESULT CProgressDialog::SetProgress(DWORD dwCompleted, DWORD dwTotal) { DWORD dwTickCount = GetTickCount(); // get the tick count before taking the critical section // we grab the crit section in case the UI thread is trying to access // _dwCompleted, _dwTotal or _dwLastUpdatedTickCount to do its time update. ENTERCRITICAL; if (_dwCompleted != dwCompleted) { _dwCompleted = dwCompleted; _fCompletedChanged = TRUE; } if (_dwTotal != dwTotal) { _dwTotal = dwTotal; _fTotalChanged = TRUE; } if (_fCompletedChanged || _fTotalChanged) { _dwLastUpdatedTickCount = dwTickCount; } LEAVECRITICAL; #ifdef DEBUG if (_dwCompleted > _dwTotal) { TraceMsg(TF_WARNING, "CProgressDialog::SetProgress(_dwCompleted > _dwTotal ?!?!)"); } #endif if (_fCompletedChanged || _fTotalChanged) { // something changed, so update the progress dlg _AsyncUpdate(); } TraceMsg(TF_PROGRESS, "CProgressDialog::SetProgress(Complete=%#08lx, Total=%#08lx)", dwCompleted, dwTotal); if (_fMinimized) { _SetTitleBarProgress(dwCompleted, dwTotal); } return S_OK; } HRESULT CProgressDialog::UpdateProgress(ULONGLONG ulCompleted, ULONGLONG ulTotal) { if (_fInitialized && _fInAction) return SetProgress64(ulCompleted, ulTotal); else return E_UNEXPECTED; } HRESULT CProgressDialog::SetProgress64(ULONGLONG ullCompleted, ULONGLONG ullTotal) { ULARGE_INTEGER uliCompleted, uliTotal; uliCompleted.QuadPart = ullCompleted; uliTotal.QuadPart = ullTotal; // If we are using the top 32 bits, scale both numbers down. // Note that I'm using the attribute that dwTotalHi is always // larger than dwCompletedHi ASSERT(uliTotal.HighPart >= uliCompleted.HighPart); while (uliTotal.HighPart) { uliCompleted.QuadPart >>= 1; uliTotal.QuadPart >>= 1; } ASSERT((0 == uliCompleted.HighPart) && (0 == uliTotal.HighPart)); // Make sure we finished scaling down. return SetProgress(uliCompleted.LowPart, uliTotal.LowPart); } HRESULT CProgressDialog::_SetTitleBarProgress(DWORD dwCompleted, DWORD dwTotal) { TCHAR szTemplate[MAX_PATH]; TCHAR szTitle[MAX_PATH]; int nPercent = 0; if (dwTotal) // Disallow divide by zero. { // Will scaling it up cause a wrap? if ((100 * 100) <= dwTotal) { // Yes, so scale down. nPercent = (dwCompleted / (dwTotal / 100)); } else { // No, so scale up. nPercent = ((100 * dwCompleted) / dwTotal); } } LoadString(MLGetHinst(), IDS_TITLEBAR_PROGRESS, szTemplate, ARRAYSIZE(szTemplate)); wnsprintf(szTitle, ARRAYSIZE(szTitle), szTemplate, nPercent); SetWindowText(_hwndProgress, szTitle); return S_OK; } HRESULT CProgressDialog::ResetCancel() { _fCancel = FALSE; if (_pwzLine1) SetLine(1, _pwzLine1, FALSE, NULL); if (_pwzLine2) SetLine(2, _pwzLine2, FALSE, NULL); if (_pwzLine3) SetLine(3, _pwzLine3, FALSE, NULL); return S_OK; } HRESULT CProgressDialog::QueryCancel(BOOL * pfCancelled) { *pfCancelled = HasUserCancelled(); return S_OK; } /****************************************************\ DESCRIPTION: This queries the progress dialog for a cancel and yields. it also will show the progress dialog if a certain amount of time has passed returns: TRUE cacnel was pressed, abort the operation FALSE continue \****************************************************/ BOOL CProgressDialog::HasUserCancelled(void) { if (!_fCancel && _hwndProgress) { MSG msg; // win95 handled messages in here. // we need to do the same in order to flush the input queue as well as // for backwards compatability. // we need to flush the input queue now because hwndProgress is // on a different thread... which means it has attached thread inputs // inorder to unlock the attached threads, we need to remove some // sort of message until there's none left... any type of message.. while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (!IsDialogMessage(_hwndProgress, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } if (_fTotalChanged || _fCompletedChanged) _AsyncUpdate(); } return _fCancel; } // IOleWindow HRESULT CProgressDialog::GetWindow(HWND * phwnd) { HRESULT hr = E_FAIL; *phwnd = _hwndProgress; if (_hwndProgress) hr = S_OK; return hr; } HRESULT CProgressDialog::QueryInterface(REFIID riid, void **ppvObj) { static const QITAB qit[] = { QITABENT(CProgressDialog, IProgressDialog), QITABENT(CProgressDialog, IActionProgressDialog), QITABENT(CProgressDialog, IActionProgress), QITABENT(CProgressDialog, IObjectWithSite), QITABENT(CProgressDialog, IOleWindow), { 0 }, }; return QISearch(this, qit, riid, ppvObj); } ULONG CProgressDialog::AddRef(void) { return InterlockedIncrement(&_cRef); } ULONG CProgressDialog::Release(void) { if (InterlockedDecrement(&_cRef)) return _cRef; if (_fThreadRunning) { // need to keep this thread's ref around // for a while longer to avoid the race // to destroy this object on the dialog thread AddRef(); ENTERCRITICAL; if (_fThreadRunning) { // we call addref AddRef(); _fReleaseSelf = TRUE; } LEAVECRITICAL; Stop(); Release(); } else delete this; return 0; } CProgressDialog::CProgressDialog() : _cRef(1) { DllAddRef(); // ASSERT zero initialized because we can only be created in the heap. (Private destructor) ASSERT(!_pwzLine1); ASSERT(!_pwzLine2); ASSERT(!_pwzLine3); ASSERT(!_fCancel); ASSERT(!_fTermThread); ASSERT(!_fInAction); ASSERT(!_hwndProgress); ASSERT(!_hwndDlgParent); ASSERT(!_fChangePosted); ASSERT(!_dwLastUpdatedTimeRemaining); ASSERT(!_dwCompleted); ASSERT(!_fCompletedChanged); ASSERT(!_fTotalChanged); ASSERT(!_fMinimized); _dwTotal = 1; // Init to Completed=0, Total=1 so we are at 0%. } CProgressDialog::~CProgressDialog() { ASSERT(!_fInAction); ASSERT(!_fThreadRunning); Str_SetPtrW(&_pwzTitle, NULL); Str_SetPtrW(&_pwzLine1, NULL); Str_SetPtrW(&_pwzLine2, NULL); Str_SetPtrW(&_pwzLine3, NULL); if (_hinstFree) FreeLibrary(_hinstFree); DllRelease(); } STDAPI CProgressDialog_CreateInstance(IUnknown* pUnkOuter, IUnknown** ppunk, LPCOBJECTINFO poi) { // aggregation checking is handled in class factory *ppunk = NULL; CProgressDialog * pProgDialog = new CProgressDialog(); if (pProgDialog) { *ppunk = SAFECAST(pProgDialog, IProgressDialog *); return S_OK; } return E_OUTOFMEMORY; }