smoother time estimation updates in wxProgressDialog (patch 992813)
[wxWidgets.git] / src / generic / progdlgg.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: progdlgg.h
3 // Purpose: wxProgressDialog class
4 // Author: Karsten Ballüder
5 // Modified by:
6 // Created: 09.05.1999
7 // RCS-ID: $Id$
8 // Copyright: (c) Karsten Ballüder
9 // Licence: wxWindows licence
10 /////////////////////////////////////////////////////////////////////////////
11
12 // ============================================================================
13 // declarations
14 // ============================================================================
15
16 // ----------------------------------------------------------------------------
17 // headers
18 // ----------------------------------------------------------------------------
19
20 #if defined(__GNUG__) && !defined(NO_GCC_PRAGMA)
21 #pragma implementation "progdlgg.h"
22 #endif
23
24 // For compilers that support precompilation, includes "wx.h".
25 #include "wx/wxprec.h"
26
27 #ifdef __BORLANDC__
28 #pragma hdrstop
29 #endif
30
31 #if wxUSE_PROGRESSDLG
32
33 #ifndef WX_PRECOMP
34 #include "wx/utils.h"
35 #include "wx/frame.h"
36 #include "wx/button.h"
37 #include "wx/stattext.h"
38 #include "wx/sizer.h"
39 #include "wx/event.h"
40 #include "wx/gauge.h"
41 #include "wx/intl.h"
42 #include "wx/settings.h"
43 #include "wx/dcclient.h"
44 #include "wx/timer.h"
45 #endif
46
47 #include "wx/generic/progdlgg.h"
48
49 // ----------------------------------------------------------------------------
50 // constants
51 // ----------------------------------------------------------------------------
52
53 #define LAYOUT_MARGIN 8
54
55 // ----------------------------------------------------------------------------
56 // private functions
57 // ----------------------------------------------------------------------------
58
59 // update the label to show the given time (in seconds)
60 static void SetTimeLabel(unsigned long val, wxStaticText *label);
61
62 // ----------------------------------------------------------------------------
63 // event tables
64 // ----------------------------------------------------------------------------
65
66 BEGIN_EVENT_TABLE(wxProgressDialog, wxDialog)
67 EVT_BUTTON(wxID_CANCEL, wxProgressDialog::OnCancel)
68
69 EVT_CLOSE(wxProgressDialog::OnClose)
70 END_EVENT_TABLE()
71
72 IMPLEMENT_CLASS(wxProgressDialog, wxDialog)
73
74 // ============================================================================
75 // wxProgressDialog implementation
76 // ============================================================================
77
78 // ----------------------------------------------------------------------------
79 // wxProgressDialog creation
80 // ----------------------------------------------------------------------------
81
82 wxProgressDialog::wxProgressDialog(wxString const &title,
83 wxString const &message,
84 int maximum,
85 wxWindow *parent,
86 int style)
87 : wxDialog(parent, wxID_ANY, title),
88 m_delay(3)
89 {
90 // we may disappear at any moment, let the others know about it
91 SetExtraStyle(GetExtraStyle() | wxWS_EX_TRANSIENT);
92
93 m_windowStyle |= style;
94
95 bool hasAbortButton = (style & wxPD_CAN_ABORT) != 0;
96
97 #if defined(__WXMSW__) && !defined(__WXUNIVERSAL__)
98 // we have to remove the "Close" button from the title bar then as it is
99 // confusing to have it - it doesn't work anyhow
100 //
101 // FIXME: should probably have a (extended?) window style for this
102 if ( !hasAbortButton )
103 {
104 EnableCloseButton(false);
105 }
106 #endif // wxMSW
107
108 m_state = hasAbortButton ? Continue : Uncancelable;
109 m_maximum = maximum;
110
111 #if defined(__WXMSW__) || defined(__WXPM__)
112 // we can't have values > 65,536 in the progress control under Windows, so
113 // scale everything down
114 m_factor = m_maximum / 65536 + 1;
115 m_maximum /= m_factor;
116 #endif // __WXMSW__
117
118 m_parentTop = wxGetTopLevelParent(parent);
119
120 wxClientDC dc(this);
121 dc.SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT));
122 long widthText;
123 dc.GetTextExtent(message, &widthText, NULL, NULL, NULL, NULL);
124
125 wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
126
127 m_msg = new wxStaticText(this, wxID_ANY, message);
128 sizer->Add(m_msg, 0, wxLEFT | wxTOP, 2*LAYOUT_MARGIN);
129
130 wxSize sizeDlg,
131 sizeLabel = m_msg->GetSize();
132 sizeDlg.y = 2*LAYOUT_MARGIN + sizeLabel.y;
133
134 if ( maximum > 0 )
135 {
136 // note that we can't use wxGA_SMOOTH because it happens to
137 // cause the dialog to be modal. Have an extra
138 // style argument to wxProgressDialog, perhaps.
139 m_gauge = new wxGauge(this, wxID_ANY, m_maximum,
140 wxDefaultPosition, wxDefaultSize,
141 wxGA_HORIZONTAL);
142
143 sizer->Add(m_gauge, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, 2*LAYOUT_MARGIN);
144 m_gauge->SetValue(0);
145
146 wxSize sizeGauge = m_gauge->GetSize();
147 sizeDlg.y += 2*LAYOUT_MARGIN + sizeGauge.y;
148 }
149 else
150 m_gauge = (wxGauge *)NULL;
151
152 // create the estimated/remaining/total time zones if requested
153 m_elapsed = m_estimated = m_remaining = (wxStaticText*)NULL;
154 m_display_estimated = m_last_timeupdate = m_break = 0;
155 m_ctdelay = 0;
156
157 // if we are going to have at least one label, remmeber it in this var
158 wxStaticText *label = NULL;
159
160 // also count how many labels we really have
161 size_t nTimeLabels = 0;
162
163 if ( style & wxPD_ELAPSED_TIME )
164 {
165 nTimeLabels++;
166
167 label =
168 m_elapsed = CreateLabel(_("Elapsed time : "), sizer);
169 }
170
171 if ( style & wxPD_ESTIMATED_TIME )
172 {
173 nTimeLabels++;
174
175 label =
176 m_estimated = CreateLabel(_("Estimated time : "), sizer);
177 }
178
179 if ( style & wxPD_REMAINING_TIME )
180 {
181 nTimeLabels++;
182
183 label =
184 m_remaining = CreateLabel(_("Remaining time : "), sizer);
185 }
186
187 if ( nTimeLabels > 0 )
188 {
189 // set it to the current time
190 m_timeStart = wxGetCurrentTime();
191 sizeDlg.y += nTimeLabels * (label->GetSize().y + LAYOUT_MARGIN);
192 }
193
194 if ( hasAbortButton )
195 {
196 m_btnAbort = new wxButton(this, wxID_CANCEL);
197
198 // Windows dialogs usually have buttons in the lower right corner
199 #if defined(__WXMSW__) || defined(__WXPM__)
200 sizer->Add(m_btnAbort, 0, wxALIGN_RIGHT | wxALL, 2*LAYOUT_MARGIN);
201 #else // !MSW
202 sizer->Add(m_btnAbort, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM | wxTOP, 2*LAYOUT_MARGIN);
203 #endif // MSW/!MSW
204 sizeDlg.y += 2*LAYOUT_MARGIN + wxButton::GetDefaultSize().y;
205 }
206 else // no "Cancel" button
207 {
208 m_btnAbort = (wxButton *)NULL;
209 }
210
211 SetSizerAndFit(sizer);
212
213 sizeDlg.y += 2*LAYOUT_MARGIN;
214
215 // try to make the dialog not square but rectangular of reasonabel width
216 sizeDlg.x = (wxCoord)wxMax(widthText, 4*sizeDlg.y/3);
217 sizeDlg.x *= 3;
218 sizeDlg.x /= 2;
219 SetClientSize(sizeDlg);
220
221 Centre(wxCENTER_FRAME | wxBOTH);
222
223 if ( style & wxPD_APP_MODAL )
224 {
225 m_winDisabler = new wxWindowDisabler(this);
226 }
227 else
228 {
229 if ( m_parentTop )
230 m_parentTop->Disable();
231 m_winDisabler = NULL;
232 }
233
234 Show();
235 Enable();
236
237 // this one can be initialized even if the others are unknown for now
238 //
239 // NB: do it after calling Layout() to keep the labels correctly aligned
240 if ( m_elapsed )
241 {
242 SetTimeLabel(0, m_elapsed);
243 }
244
245 Update();
246 }
247
248 wxStaticText *wxProgressDialog::CreateLabel(const wxString& text,
249 wxSizer *sizer)
250 {
251 wxBoxSizer *locsizer = new wxBoxSizer(wxHORIZONTAL);
252
253 wxStaticText *dummy = new wxStaticText(this, -1, text);
254 wxStaticText *label = new wxStaticText(this, wxID_ANY, _("unknown"));
255
256 // VZ: I like the labels be centered - if the others don't mind, you may
257 // remove "#ifdef __WXMSW__" and use it for all ports
258 #if defined(__WXMSW__) || defined(__WXPM__) || defined(__WXMAC__)
259 locsizer->Add(dummy, 1, wxALIGN_RIGHT);
260 locsizer->Add(label, 1, wxALIGN_LEFT | wxLEFT, LAYOUT_MARGIN);
261 sizer->Add(locsizer, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, LAYOUT_MARGIN);
262 #else // !MSW
263 sizer->Add(locsizer, 0, wxALIGN_RIGHT | wxRIGHT | wxTOP, LAYOUT_MARGIN);
264 locsizer->Add(dummy);
265 locsizer->Add(label, 0, wxLEFT, LAYOUT_MARGIN);
266 #endif // MSW/!MSW
267
268 return label;
269 }
270
271 // ----------------------------------------------------------------------------
272 // wxProgressDialog operations
273 // ----------------------------------------------------------------------------
274
275 bool
276 wxProgressDialog::Update(int value, const wxString& newmsg)
277 {
278 wxASSERT_MSG( value == -1 || m_gauge, wxT("cannot update non existent dialog") );
279
280 #ifdef __WXMSW__
281 value /= m_factor;
282 #endif // __WXMSW__
283
284 wxASSERT_MSG( value <= m_maximum, wxT("invalid progress value") );
285
286 // fill up the gauge if value == maximum because this means that the dialog
287 // is going to close and the gauge shouldn't be partly empty in this case
288 if ( m_gauge && value <= m_maximum )
289 {
290 m_gauge->SetValue(value == m_maximum ? value : value + 1);
291 }
292
293 if ( !newmsg.IsEmpty() )
294 {
295 m_msg->SetLabel(newmsg);
296
297 wxYieldIfNeeded() ;
298 }
299
300 if ( (m_elapsed || m_remaining || m_estimated) && (value != 0) )
301 {
302 unsigned long elapsed = wxGetCurrentTime() - m_timeStart;
303 if ( m_last_timeupdate < elapsed
304 || value == m_maximum
305 )
306 {
307 m_last_timeupdate = elapsed;
308 unsigned long estimated = m_break +
309 (unsigned long)(( (double) (elapsed-m_break) * m_maximum ) / ((double)value)) ;
310 if ( estimated > m_display_estimated
311 && m_ctdelay >= 0
312 )
313 {
314 ++m_ctdelay;
315 }
316 else if ( estimated < m_display_estimated
317 && m_ctdelay <= 0
318 )
319 {
320 --m_ctdelay;
321 }
322 else
323 {
324 m_ctdelay = 0;
325 }
326 if ( m_ctdelay >= m_delay // enough confirmations for a higher value
327 || m_ctdelay <= (m_delay*-1) // enough confirmations for a lower value
328 || value == m_maximum // to stay consistent
329 || elapsed > m_display_estimated // to stay consistent
330 || ( elapsed > 0 && elapsed < 4 ) // additional updates in the beginning
331 )
332 {
333 m_display_estimated = estimated;
334 m_ctdelay = 0;
335 }
336 }
337
338 unsigned long display_remaining = m_display_estimated - elapsed;
339 if ( display_remaining < 0 )
340 {
341 display_remaining = 0;
342 }
343
344 SetTimeLabel(elapsed, m_elapsed);
345 SetTimeLabel(m_display_estimated, m_estimated);
346 SetTimeLabel(display_remaining, m_remaining);
347 }
348
349 if ( value == m_maximum )
350 {
351 // so that we return true below and that out [Cancel] handler knew what
352 // to do
353 m_state = Finished;
354 if( !(GetWindowStyle() & wxPD_AUTO_HIDE) )
355 {
356 if ( m_btnAbort )
357 {
358 // tell the user what he should do...
359 m_btnAbort->SetLabel(_("Close"));
360 }
361 #if defined(__WXMSW__) && !defined(__WXUNIVERSAL__)
362 else // enable the button to give the user a way to close the dlg
363 {
364 EnableCloseButton();
365 }
366 #endif // __WXMSW__
367
368 if ( !newmsg )
369 {
370 // also provide the finishing message if the application didn't
371 m_msg->SetLabel(_("Done."));
372 }
373
374 wxYieldIfNeeded() ;
375
376 (void)ShowModal();
377 }
378 else // auto hide
379 {
380 // reenable other windows before hiding this one because otherwise
381 // Windows wouldn't give the focus back to the window which had
382 // been previously focused because it would still be disabled
383 ReenableOtherWindows();
384
385 Hide();
386 }
387 }
388 else
389 {
390 // we have to yield because not only we want to update the display but
391 // also to process the clicks on the cancel button
392 wxYieldIfNeeded() ;
393 }
394
395 // update the display in case yielding above didn't do it
396 Update();
397
398 return m_state != Canceled;
399 }
400
401 void wxProgressDialog::Resume()
402 {
403 m_state = Continue;
404 m_ctdelay = m_delay; // force an update of the elapsed/estimated/remaining time
405 m_break += wxGetCurrentTime()-m_timeStop;
406
407 // it may have been disabled by OnCancel(), so enable it back to let the
408 // user interrupt us again if needed
409 m_btnAbort->Enable();
410 }
411
412 bool wxProgressDialog::Show( bool show )
413 {
414 // reenable other windows before hiding this one because otherwise
415 // Windows wouldn't give the focus back to the window which had
416 // been previously focused because it would still be disabled
417 if(!show)
418 ReenableOtherWindows();
419
420 return wxDialog::Show(show);
421 }
422
423 // ----------------------------------------------------------------------------
424 // event handlers
425 // ----------------------------------------------------------------------------
426
427 void wxProgressDialog::OnCancel(wxCommandEvent& event)
428 {
429 if ( m_state == Finished )
430 {
431 // this means that the count down is already finished and we're being
432 // shown as a modal dialog - so just let the default handler do the job
433 event.Skip();
434 }
435 else
436 {
437 // request to cancel was received, the next time Update() is called we
438 // will handle it
439 m_state = Canceled;
440
441 // update the button state immediately so that the user knows that the
442 // request has been noticed
443 m_btnAbort->Disable();
444
445 // save the time when the dialog was stopped
446 m_timeStop = wxGetCurrentTime();
447 }
448 }
449
450 void wxProgressDialog::OnClose(wxCloseEvent& event)
451 {
452 if ( m_state == Uncancelable )
453 {
454 // can't close this dialog
455 event.Veto();
456 }
457 else if ( m_state == Finished )
458 {
459 // let the default handler close the window as we already terminated
460 event.Skip();
461 }
462 else
463 {
464 // next Update() will notice it
465 m_state = Canceled;
466 m_btnAbort->Disable();
467 m_timeStop = wxGetCurrentTime();
468 }
469 }
470
471 // ----------------------------------------------------------------------------
472 // destruction
473 // ----------------------------------------------------------------------------
474
475 wxProgressDialog::~wxProgressDialog()
476 {
477 // normally this should have been already done, but just in case
478 ReenableOtherWindows();
479 }
480
481 void wxProgressDialog::ReenableOtherWindows()
482 {
483 if ( GetWindowStyle() & wxPD_APP_MODAL )
484 {
485 delete m_winDisabler;
486 m_winDisabler = (wxWindowDisabler *)NULL;
487 }
488 else
489 {
490 if ( m_parentTop )
491 m_parentTop->Enable();
492 }
493 }
494
495 // ----------------------------------------------------------------------------
496 // private functions
497 // ----------------------------------------------------------------------------
498
499 static void SetTimeLabel(unsigned long val, wxStaticText *label)
500 {
501 if ( label )
502 {
503 wxString s;
504 unsigned long hours = val / 3600;
505 unsigned long minutes = (val % 3600) / 60;
506 unsigned long seconds = val % 60;
507 s.Printf(wxT("%lu:%02lu:%02lu"), hours, minutes, seconds);
508
509 if ( s != label->GetLabel() )
510 label->SetLabel(s);
511 }
512 }
513
514 #endif // wxUSE_PROGRESSDLG