1 /*
2  *	Progress dialog
3  *
4  *  Copyright 2007  Mikolaj Zalewski
5  *	Copyright 2014	Huw Campbell
6  *
7  * this library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * this library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20  */
21 
22 #include <precomp.h>
23 
24 #define COBJMACROS
25 
26 #define CANCEL_MSG_LINE 2
27 
28 /* Note: to avoid a deadlock we don't want to send messages to the dialog
29  * with the critical section held. Instead we only mark what fields should be
30  * updated and the dialog proc does the update */
31 #define UPDATE_PROGRESS         0x1
32 #define UPDATE_TITLE            0x2
33 #define UPDATE_LINE1            0x4
34 #define UPDATE_LINE2            (UPDATE_LINE1<<1)
35 #define UPDATE_LINE3            (UPDATE_LINE1<<2)
36 
37 
38 #define WM_DLG_UPDATE   (WM_APP+1)  /* set to the dialog when it should update */
39 #define WM_DLG_DESTROY  (WM_APP+2)  /* DestroyWindow must be called from the owning thread */
40 
41 #define ID_3SECONDS 101
42 
43 #define BUFFER_SIZE 256
44 
CProgressDialog()45 CProgressDialog::CProgressDialog()
46 {
47     this->hwnd = NULL;
48     this->lines[0]  = (LPWSTR) HeapAlloc(GetProcessHeap(), 0, BUFFER_SIZE);
49     this->lines[1]  = (LPWSTR) HeapAlloc(GetProcessHeap(), 0, BUFFER_SIZE);
50     this->lines[2]  = (LPWSTR) HeapAlloc(GetProcessHeap(), 0, BUFFER_SIZE);
51     this->cancelMsg = (LPWSTR) HeapAlloc(GetProcessHeap(), 0, BUFFER_SIZE);
52     this->title     = (LPWSTR) HeapAlloc(GetProcessHeap(), 0, BUFFER_SIZE);
53 
54     this->lines[0][0] = this->lines[1][0] = this->lines[2][0] = UNICODE_NULL;
55     this->cancelMsg[0] = this->title[0] = UNICODE_NULL;
56 
57     this->clockHand = -1;
58     this->progressClock[29].ullMark = 0ull;
59     this->dwStartTime = GetTickCount();
60 
61     InitializeCriticalSection(&this->cs);
62 }
63 
~CProgressDialog()64 CProgressDialog::~CProgressDialog()
65 {
66     if (this->hwnd)
67         this->end_dialog();
68     HeapFree(GetProcessHeap(), 0, this->lines[0]);
69     HeapFree(GetProcessHeap(), 0, this->lines[1]);
70     HeapFree(GetProcessHeap(), 0, this->lines[2]);
71     HeapFree(GetProcessHeap(), 0, this->cancelMsg);
72     HeapFree(GetProcessHeap(), 0, this->title);
73     DeleteCriticalSection(&this->cs);
74 }
75 
set_buffer(LPWSTR * buffer,LPCWSTR string)76 static void set_buffer(LPWSTR *buffer, LPCWSTR string)
77 {
78     if (!string)
79     {
80         (*buffer)[0] = UNICODE_NULL;
81         return;
82     }
83 
84     StringCbCopyW(*buffer, BUFFER_SIZE, string);
85 }
86 
87 struct create_params
88 {
89     CProgressDialog *This;
90     HANDLE hEvent;
91     HWND hwndParent;
92 };
93 
load_string(LPWSTR * buffer,HINSTANCE hInstance,UINT uiResourceId)94 static void load_string(LPWSTR *buffer, HINSTANCE hInstance, UINT uiResourceId)
95 {
96     WCHAR string[256];
97 
98     LoadStringW(hInstance, uiResourceId, string, sizeof(string)/sizeof(string[0]));
99 
100     set_buffer(buffer, string);
101 }
102 
set_progress_marquee()103 void CProgressDialog::set_progress_marquee()
104 {
105     HWND hProgress = GetDlgItem(this->hwnd, IDC_PROGRESS_BAR);
106     SetWindowLongW(hProgress, GWL_STYLE,
107         GetWindowLongW(hProgress, GWL_STYLE)|PBS_MARQUEE);
108 }
109 
update_dialog(DWORD dwUpdate)110 void CProgressDialog::update_dialog(DWORD dwUpdate)
111 {
112     WCHAR empty[] = {0};
113 
114     if (dwUpdate & UPDATE_TITLE)
115         SetWindowTextW(this->hwnd, this->title);
116 
117     if (dwUpdate & UPDATE_LINE1)
118         SetDlgItemTextW(this->hwnd, IDC_TEXT_LINE, (this->isCancelled ? empty : this->lines[0]));
119     if (dwUpdate & UPDATE_LINE2)
120         SetDlgItemTextW(this->hwnd, IDC_TEXT_LINE+1, (this->isCancelled ? empty : this->lines[1]));
121     if (dwUpdate & UPDATE_LINE3)
122         SetDlgItemTextW(this->hwnd, IDC_TEXT_LINE+2, (this->isCancelled ? this->cancelMsg : this->lines[2]));
123 
124     if (dwUpdate & UPDATE_PROGRESS)
125     {
126         ULONGLONG ullTotal = this->ullTotal;
127         ULONGLONG ullCompleted = this->ullCompleted;
128 
129         /* progress bar requires 32-bit coordinates */
130         while (ullTotal >> 32)
131         {
132             ullTotal >>= 1;
133             ullCompleted >>= 1;
134         }
135 
136         SendDlgItemMessageW(this->hwnd, IDC_PROGRESS_BAR, PBM_SETRANGE32, 0, (DWORD)ullTotal);
137         SendDlgItemMessageW(this->hwnd, IDC_PROGRESS_BAR, PBM_SETPOS, (DWORD)ullCompleted, 0);
138     }
139 }
140 
end_dialog()141 void CProgressDialog::end_dialog()
142 {
143     SendMessageW(this->hwnd, WM_DLG_DESTROY, 0, 0);
144     /* native doesn't re-enable the window? */
145     if (this->hwndDisabledParent)
146         EnableWindow(this->hwndDisabledParent, TRUE);
147     this->hwnd = NULL;
148 }
149 
dialog_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)150 static INT_PTR CALLBACK dialog_proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
151 {
152     CProgressDialog *This = (CProgressDialog *)GetWindowLongPtrW(hwnd, DWLP_USER);
153 
154     switch (msg)
155     {
156         case WM_INITDIALOG:
157         {
158             struct create_params *params = (struct create_params *)lParam;
159 
160             /* Note: until we set the hEvent, the object is protected by
161              * the critical section held by StartProgress */
162             SetWindowLongPtrW(hwnd, DWLP_USER, (LONG_PTR)params->This);
163             This = params->This;
164             This->hwnd = hwnd;
165 
166             if (This->dwFlags & PROGDLG_NOPROGRESSBAR)
167                 ShowWindow(GetDlgItem(hwnd, IDC_PROGRESS_BAR), SW_HIDE);
168             if (This->dwFlags & PROGDLG_NOCANCEL)
169                 ShowWindow(GetDlgItem(hwnd, IDCANCEL), SW_HIDE);
170             if (This->dwFlags & PROGDLG_MARQUEEPROGRESS)
171                 This->set_progress_marquee();
172             if (This->dwFlags & PROGDLG_NOMINIMIZE)
173                 SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & (~WS_MINIMIZEBOX));
174 
175             This->update_dialog(0xffffffff);
176             This->dwUpdate = 0;
177             This->isCancelled = FALSE;
178 
179             SetTimer(hwnd, ID_3SECONDS, 3 * 1000, NULL);
180 
181             SetEvent(params->hEvent);
182             return TRUE;
183         }
184 
185         case WM_DLG_UPDATE:
186             EnterCriticalSection(&This->cs);
187             This->update_dialog(This->dwUpdate);
188             This->dwUpdate = 0;
189             LeaveCriticalSection(&This->cs);
190             return TRUE;
191 
192         case WM_DLG_DESTROY:
193             DestroyWindow(hwnd);
194             PostThreadMessageW(GetCurrentThreadId(), WM_NULL, 0, 0); /* wake up the GetMessage */
195             KillTimer(hwnd, ID_3SECONDS);
196 
197             return TRUE;
198 
199         case WM_CLOSE:
200         case WM_COMMAND:
201             if (msg == WM_CLOSE || wParam == IDCANCEL)
202             {
203                 EnterCriticalSection(&This->cs);
204                 This->isCancelled = TRUE;
205 
206                 if (!This->cancelMsg[0]) {
207                     load_string(&This->cancelMsg, _AtlBaseModule.GetResourceInstance(), IDS_CANCELLING);
208                 }
209 
210                 This->set_progress_marquee();
211                 EnableWindow(GetDlgItem(This->hwnd, IDCANCEL), FALSE);
212                 This->update_dialog(UPDATE_LINE1|UPDATE_LINE2|UPDATE_LINE3);
213                 LeaveCriticalSection(&This->cs);
214             }
215             return TRUE;
216 
217         case WM_TIMER:
218             EnterCriticalSection(&This->cs);
219             if (This->progressClock[29].ullMark != 0ull) {
220                 // We have enough info to take a guess
221                 ULONGLONG sizeDiff = This->progressClock[This->clockHand].ullMark -
222                                      This->progressClock[(This->clockHand + 29) % 30].ullMark;
223                 DWORD     timeDiff = This->progressClock[This->clockHand].dwTime -
224                                      This->progressClock[(This->clockHand + 29) % 30].dwTime;
225                 DWORD      runDiff = This->progressClock[This->clockHand].dwTime -
226                                      This->dwStartTime;
227                 ULONGLONG sizeLeft = This->ullTotal - This->progressClock[This->clockHand].ullMark;
228 
229                 // A guess for time remaining based on the recent slope.
230                 DWORD timeLeftD = (DWORD) timeDiff * ((double) sizeLeft) / ((double) sizeDiff);
231                 // A guess for time remaining based on the start time and current position
232                 DWORD timeLeftI = (DWORD) runDiff * ((double) sizeLeft) / ((double) This->progressClock[This->clockHand].ullMark);
233 
234                 StrFromTimeIntervalW(This->lines[2], 128, timeLeftD * 0.3 + timeLeftI * 0.7 , 2);
235                 This->update_dialog( UPDATE_LINE1 << 2 );
236             }
237             LeaveCriticalSection(&This->cs);
238 
239             return TRUE;
240     }
241     return FALSE;
242 }
243 
dialog_thread(LPVOID lpParameter)244 static DWORD WINAPI dialog_thread(LPVOID lpParameter)
245 {
246     /* Note: until we set the hEvent in WM_INITDIALOG, the ProgressDialog object
247      * is protected by the critical section held by StartProgress */
248     struct create_params *params = (struct create_params *) lpParameter;
249     HWND hwnd;
250     MSG msg;
251 
252     hwnd = CreateDialogParamW(_AtlBaseModule.GetResourceInstance(),
253                               MAKEINTRESOURCEW(IDD_PROGRESS_DLG),
254                               params->hwndParent,
255                               dialog_proc,
256                              (LPARAM)params);
257 
258     while (GetMessageW(&msg, NULL, 0, 0) > 0)
259     {
260         if (!IsWindow(hwnd))
261             break;
262         if(!IsDialogMessageW(hwnd, &msg))
263         {
264             TranslateMessage(&msg);
265             DispatchMessageW(&msg);
266         }
267     }
268 
269     return 0;
270 }
271 
StartProgressDialog(HWND hwndParent,IUnknown * punkEnableModeless,DWORD dwFlags,LPCVOID reserved)272 HRESULT WINAPI CProgressDialog::StartProgressDialog(HWND hwndParent, IUnknown *punkEnableModeless, DWORD dwFlags, LPCVOID reserved)
273 {
274     static const INITCOMMONCONTROLSEX init = { sizeof(init), ICC_ANIMATE_CLASS };
275 
276     struct create_params params;
277     HANDLE hThread;
278 
279     // TRACE("(%p, %p, %x, %p)\n", this, punkEnableModeless, dwFlags, reserved);
280     if (punkEnableModeless || reserved)
281         FIXME("Reserved parameters not null (%p, %p)\n", punkEnableModeless, reserved);
282     if (dwFlags & PROGDLG_AUTOTIME)
283         FIXME("Flags PROGDLG_AUTOTIME not supported\n");
284     if (dwFlags & PROGDLG_NOTIME)
285         FIXME("Flags PROGDLG_NOTIME not supported\n");
286 
287     InitCommonControlsEx( &init );
288 
289     EnterCriticalSection(&this->cs);
290 
291     if (this->hwnd)
292     {
293         LeaveCriticalSection(&this->cs);
294         return S_OK;  /* as on XP */
295     }
296     this->dwFlags = dwFlags;
297     params.This = this;
298     params.hwndParent = hwndParent;
299     params.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
300 
301     hThread = CreateThread(NULL, 0, dialog_thread, &params, 0, NULL);
302     WaitForSingleObject(params.hEvent, INFINITE);
303     CloseHandle(params.hEvent);
304     CloseHandle(hThread);
305 
306     this->hwndDisabledParent = NULL;
307     if (hwndParent && (dwFlags & PROGDLG_MODAL))
308     {
309         HWND hwndDisable = GetAncestor(hwndParent, GA_ROOT);
310         if (EnableWindow(hwndDisable, FALSE))
311             this->hwndDisabledParent = hwndDisable;
312     }
313 
314     LeaveCriticalSection(&this->cs);
315 
316     return S_OK;
317 }
318 
StopProgressDialog()319 HRESULT WINAPI CProgressDialog::StopProgressDialog()
320 {
321     EnterCriticalSection(&this->cs);
322     if (this->hwnd)
323         this->end_dialog();
324     LeaveCriticalSection(&this->cs);
325 
326     return S_OK;
327 }
328 
SetTitle(LPCWSTR pwzTitle)329 HRESULT WINAPI CProgressDialog::SetTitle(LPCWSTR pwzTitle)
330 {
331     HWND hwnd;
332 
333     EnterCriticalSection(&this->cs);
334     set_buffer(&this->title, pwzTitle);
335     this->dwUpdate |= UPDATE_TITLE;
336     hwnd = this->hwnd;
337     LeaveCriticalSection(&this->cs);
338 
339     if (hwnd)
340         SendMessageW(hwnd, WM_DLG_UPDATE, 0, 0);
341 
342     return S_OK;
343 }
344 
SetAnimation(HINSTANCE hInstance,UINT uiResourceId)345 HRESULT WINAPI CProgressDialog::SetAnimation(HINSTANCE hInstance, UINT uiResourceId)
346 {
347     HWND hAnimation = GetDlgItem(this->hwnd, IDD_PROGRESS_DLG);
348     SetWindowLongW(hAnimation, GWL_STYLE,
349         GetWindowLongW(hAnimation, GWL_STYLE)|ACS_TRANSPARENT|ACS_CENTER|ACS_AUTOPLAY);
350 
351     if(!Animate_OpenEx(hAnimation,hInstance,MAKEINTRESOURCEW(uiResourceId)))
352         return S_FALSE;
353 
354     return S_OK;
355 }
356 
HasUserCancelled()357 BOOL WINAPI CProgressDialog::HasUserCancelled()
358 {
359     return this->isCancelled;
360 }
361 
SetProgress64(ULONGLONG ullCompleted,ULONGLONG ullTotal)362 HRESULT WINAPI CProgressDialog::SetProgress64(ULONGLONG ullCompleted, ULONGLONG ullTotal)
363 {
364     HWND hwnd;
365 
366     EnterCriticalSection(&this->cs);
367     this->ullTotal = ullTotal;
368     this->ullCompleted = ullCompleted;
369 
370     if (GetTickCount() - this->progressClock[(this->clockHand + 29) % 30].dwTime > 20) {
371         this->clockHand = (this->clockHand + 1) % 30;
372         this->progressClock[this->clockHand].ullMark = ullCompleted;
373         this->progressClock[this->clockHand].dwTime = GetTickCount();
374     }
375 
376     this->dwUpdate |= UPDATE_PROGRESS;
377     hwnd = this->hwnd;
378     LeaveCriticalSection(&this->cs);
379 
380     if (hwnd)
381         SendMessageW(hwnd, WM_DLG_UPDATE, 0, 0);
382 
383     return S_OK;  /* Windows sometimes returns S_FALSE */
384 }
385 
SetProgress(DWORD dwCompleted,DWORD dwTotal)386 HRESULT WINAPI CProgressDialog::SetProgress(DWORD dwCompleted, DWORD dwTotal)
387 {
388     return this->SetProgress64(dwCompleted, dwTotal);
389 }
390 
SetLine(DWORD dwLineNum,LPCWSTR pwzLine,BOOL bPath,LPCVOID reserved)391 HRESULT WINAPI CProgressDialog::SetLine(DWORD dwLineNum, LPCWSTR pwzLine, BOOL bPath, LPCVOID reserved)
392 {
393     HWND hwnd;
394 
395     if (reserved)
396         FIXME("reserved pointer not null (%p)\n", reserved);
397 
398     dwLineNum--;
399     if (dwLineNum >= 3)  /* Windows seems to do something like that */
400         dwLineNum = 0;
401 
402     EnterCriticalSection(&this->cs);
403     set_buffer(&this->lines[dwLineNum], pwzLine);
404     this->dwUpdate |= UPDATE_LINE1 << dwLineNum;
405     hwnd = (this->isCancelled ? NULL : this->hwnd); /* no sense to send the message if window cancelled */
406     LeaveCriticalSection(&this->cs);
407 
408     if (hwnd)
409         SendMessageW(hwnd, WM_DLG_UPDATE, 0, 0);
410 
411     return S_OK;
412 }
413 
SetCancelMsg(LPCWSTR pwzMsg,LPCVOID reserved)414 HRESULT WINAPI CProgressDialog::SetCancelMsg(LPCWSTR pwzMsg, LPCVOID reserved)
415 {
416     HWND hwnd;
417 
418     if (reserved)
419         FIXME("reserved pointer not null (%p)\n", reserved);
420 
421     EnterCriticalSection(&this->cs);
422     set_buffer(&this->cancelMsg, pwzMsg);
423     this->dwUpdate |= UPDATE_LINE1 << CANCEL_MSG_LINE;
424     hwnd = (this->isCancelled ? this->hwnd : NULL); /* no sense to send the message if window not cancelled */
425     LeaveCriticalSection(&this->cs);
426 
427     if (hwnd)
428         SendMessageW(hwnd, WM_DLG_UPDATE, 0, 0);
429 
430     return S_OK;
431 }
432 
Timer(DWORD dwTimerAction,LPCVOID reserved)433 HRESULT WINAPI CProgressDialog::Timer(DWORD dwTimerAction, LPCVOID reserved)
434 {
435      if (reserved)
436          FIXME("Reserved field not NULL but %p\n", reserved);
437 
438     return S_OK;
439 }
440 
GetWindow(HWND * phwnd)441 HRESULT WINAPI CProgressDialog::GetWindow(HWND* phwnd)
442 {
443     EnterCriticalSection(&this->cs);
444     *phwnd = this->hwnd;
445     LeaveCriticalSection(&this->cs);
446     return S_OK;
447 }
448 
ContextSensitiveHelp(BOOL fEnterMode)449 HRESULT WINAPI CProgressDialog::ContextSensitiveHelp(BOOL fEnterMode)
450 {
451     return E_NOTIMPL;
452 }
453