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