Module PyProgress
[hide private]
[frames] | no frames]

Source Code for Module PyProgress

  1  # --------------------------------------------------------------------------------- # 
  2  # PYPROGRESS wxPython IMPLEMENTATION 
  3  # 
  4  # Andrea Gavana, @ 03 Nov 2006 
  5  # Latest Revision: 03 Nov 2006, 22.30 CET 
  6  # 
  7  # 
  8  # TODO List 
  9  # 
 10  # 1. Do we support all the styles of wx.ProgressDialog in indeterminated mode? 
 11  # 
 12  # 2. Other ideas? 
 13  # 
 14  # 
 15  # For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please 
 16  # Write To Me At: 
 17  # 
 18  # gavana@kpo.kz 
 19  # andrea.gavana@gmail.com 
 20  # 
 21  # Or, Obviously, To The wxPython Mailing List!!! 
 22  # 
 23  # 
 24  # End Of Comments 
 25  # --------------------------------------------------------------------------------- # 
 26   
 27  """ 
 28  Description 
 29  =========== 
 30   
 31  PyProgress is similar to wx.ProgressDialog in indeterminated mode, but with a 
 32  different gauge appearance and a different spinning behavior. The moving gauge 
 33  can be drawn with a single solid colour or with a shading gradient foreground. 
 34  The gauge background colour is user customizable. 
 35  The bar does not move always from the beginning to the end as in wx.ProgressDialog 
 36  in indeterminated mode, but spins cyclically forward and backward. 
 37  Other options include: 
 38   
 39    - Possibility to change the proportion between the spinning bar and the 
 40      entire gauge, so that the bar can be longer or shorter (the default is 20%); 
 41    - Modifying the number of steps the spinning bar performs before a forward 
 42      (or backward) loop reverses. 
 43       
 44  PyProgress can optionally display a Cancel button, and a wx.StaticText which 
 45  outputs the elapsed time from the starting of the process. 
 46   
 47   
 48  Supported Platforms 
 49  =================== 
 50   
 51  PyProgress has been tested on the following platforms: 
 52    * Windows (Windows XP); 
 53    * Linux Ubuntu (Dapper 6.06) 
 54   
 55   
 56  License And Version: 
 57  =================== 
 58   
 59  PyProgress is freeware and distributed under the wxPython license.  
 60   
 61   
 62  Latest Revision: Andrea Gavana @ 03 Nov 2006, 22.30 CET 
 63  Version 0.1 
 64   
 65  """ 
 66   
 67  __docformat__ = "epytext" 
 68   
 69   
 70  import wx 
 71   
 72  # Some constants, taken straight from wx.ProgressDialog 
 73  Uncancelable = -1 
 74  Canceled = 0 
 75  Continue = 1 
 76  Finished = 2 
 77   
 78  # Margins between gauge and text/button 
 79  LAYOUT_MARGIN = 8 
 80   
 81   
 82  # ---------------------------------------------------------------------------- # 
 83  # Class ProgressGauge 
 84  # ---------------------------------------------------------------------------- # 
 85   
86 -class ProgressGauge(wx.PyWindow):
87 """ This class provides a visual alternative for wx.Gauge.""" 88
89 - def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, 90 size=(-1,30)):
91 """ Default class constructor. """ 92 93 wx.PyWindow.__init__(self, parent, id, pos, size, style=wx.SUNKEN_BORDER) 94 95 self._value = 0 96 self._steps = 50 97 self._pos = 0 98 self._current = 0 99 self._gaugeproportion = 0.2 100 self._firstGradient = wx.WHITE 101 self._secondGradient = wx.BLUE 102 self._background = wx.Brush(wx.WHITE, wx.SOLID) 103 104 self.Bind(wx.EVT_PAINT, self.OnPaint) 105 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
106 107
108 - def GetFirstGradientColour(self):
109 """ Returns the first gradient colour. """ 110 111 return self._firstGradient
112 113
114 - def SetFirstGradientColour(self, colour):
115 """ Sets the first gradient colour. """ 116 117 self._firstGradient = colour 118 self.Refresh()
119 120
121 - def GetSecondGradientColour(self):
122 """ Returns the second gradient colour. """ 123 124 return self._secondGradient
125 126
127 - def SetSecondGradientColour(self, colour):
128 """ Sets the second gradient colour. """ 129 130 self._secondGradient = colour 131 self.Refresh()
132 133
134 - def GetGaugeBackground(self):
135 """ Returns the gauge background colour. """ 136 137 return self._background
138 139
140 - def SetGaugeBackground(self, colour):
141 """ Sets the gauge background colour. """ 142 143 self._background = wx.Brush(colour, wx.SOLID)
144 145
146 - def SetGaugeSteps(self, steps):
147 """ 148 Sets the number of steps the gauge performs before switching from 149 forward to backward (or vice-versa) movement. 150 """ 151 152 if steps <= 0: 153 raise "ERROR:\n Gauge steps must be greater than zero. " 154 return 155 156 if steps != self._steps: 157 self._steps = steps
158 159
160 - def GetGaugeSteps(self):
161 """ 162 Returns the number of steps the gauge performs before switching from 163 forward to backward (or vice-versa) movement. 164 """ 165 166 return self._steps
167 168
169 - def GetGaugeProportion(self):
170 """ 171 Returns the relative proportion between the sliding bar and the 172 whole gauge. 173 """ 174 175 return self._gaugeproportion
176 177
178 - def SetGaugeProportion(self, proportion):
179 """ 180 Sets the relative proportion between the sliding bar and the 181 whole gauge. 182 """ 183 184 if proportion <= 0 or proportion >= 1: 185 raise "ERROR:\n Gauge proportion must be between 0 and 1. " 186 return 187 188 if proportion != self._gaugeproportion: 189 self._gaugeproportion = proportion
190 191
192 - def OnEraseBackground(self, event):
193 """ Handles the wx.EVT_ERASE_BACKGROUND event for ProgressGauge. """ 194 195 pass
196 197
198 - def OnPaint(self, event):
199 """ Handles the wx.EVT_PAINT event for ProgressGauge. """ 200 201 dc = wx.BufferedPaintDC(self) 202 dc.SetBackground(self._background) 203 204 dc.Clear() 205 206 xsize, ysize = self.GetClientSize() 207 interval = xsize/float(self._steps) 208 209 self._pos = interval*self._value 210 211 status = self._current/(self._steps - int(self._gaugeproportion*xsize)/int(interval)) 212 213 if status%2 == 0: 214 increment = 1 215 else: 216 increment = -1 217 218 self._value = self._value + increment 219 self._current = self._current + 1 220 self.DrawProgress(dc, xsize, ysize, increment)
221 222
223 - def DrawProgress(self, dc, xsize, ysize, increment):
224 """ Actually draws the sliding bar. """ 225 226 if increment > 0: 227 col1 = self.GetFirstGradientColour() 228 col2 = self.GetSecondGradientColour() 229 else: 230 col1 = self.GetSecondGradientColour() 231 col2 = self.GetFirstGradientColour() 232 233 interval = self._gaugeproportion*xsize 234 235 r1, g1, b1 = int(col1.Red()), int(col1.Green()), int(col1.Blue()) 236 r2, g2, b2 = int(col2.Red()), int(col2.Green()), int(col2.Blue()) 237 238 rstep = float((r2 - r1)) / interval 239 gstep = float((g2 - g1)) / interval 240 bstep = float((b2 - b1)) / interval 241 242 rf, gf, bf = 0, 0, 0 243 dc.SetBrush(wx.TRANSPARENT_BRUSH) 244 245 for ii in xrange(int(self._pos), int(self._pos+interval)): 246 currCol = (r1 + rf, g1 + gf, b1 + bf) 247 dc.SetPen(wx.Pen(currCol, 2)) 248 dc.DrawLine(ii, 1, ii, ysize-2) 249 rf = rf + rstep 250 gf = gf + gstep 251 bf = bf + bstep
252 253
254 - def Update(self):
255 """ Updates the gauge with a new value. """ 256 257 self.Refresh()
258 259 260 # ---------------------------------------------------------------------------- # 261 # Class PyProgress 262 # ---------------------------------------------------------------------------- # 263
264 -class PyProgress(wx.Dialog):
265 """ 266 PyProgress is similar to wx.ProgressDialog in indeterminated mode, but with a 267 different gauge appearance and a different spinning behavior. The moving gauge 268 can be drawn with a single solid colour or with a shading gradient foreground. 269 The gauge background colour is user customizable. 270 The bar does not move always from the beginning to the end as in wx.ProgressDialog 271 in indeterminated mode, but spins cyclically forward and backward. 272 """ 273
274 - def __init__(self, parent=None, id=-1, title="", message="", 275 style=wx.PD_APP_MODAL|wx.PD_AUTO_HIDE):
276 """ Default class constructor. """ 277 278 wx.Dialog.__init__(self, parent, id, title) 279 280 self._delay = 3 281 self._hasAbortButton = False 282 283 # we may disappear at any moment, let the others know about it 284 self.SetExtraStyle(self.GetExtraStyle()|wx.WS_EX_TRANSIENT) 285 286 self._hasAbortButton = (style & wx.PD_CAN_ABORT) 287 288 if wx.Platform == "__WXMSW__": 289 # we have to remove the "Close" button from the title bar then as it is 290 # confusing to have it - it doesn't work anyhow 291 # FIXME: should probably have a (extended?) window style for this 292 if not self._hasAbortButton: 293 self.EnableClose(False) 294 295 self._state = (self._hasAbortButton and [Continue] or [Uncancelable])[0] 296 self._parentTop = wx.GetTopLevelParent(parent) 297 298 dc = wx.ClientDC(self) 299 dc.SetFont(wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)) 300 widthText, dummy = dc.GetTextExtent(message) 301 302 sizer = wx.BoxSizer(wx.VERTICAL) 303 304 self._msg = wx.StaticText(self, wx.ID_ANY, message) 305 sizer.Add(self._msg, 0, wx.LEFT|wx.TOP, 2*LAYOUT_MARGIN) 306 307 sizeDlg = wx.Size() 308 sizeLabel = self._msg.GetSize() 309 sizeDlg.y = 2*LAYOUT_MARGIN + sizeLabel.y 310 311 self._gauge = ProgressGauge(self, -1) 312 313 sizer.Add(self._gauge, 0, wx.LEFT|wx.RIGHT|wx.TOP|wx.EXPAND, 2*LAYOUT_MARGIN) 314 315 sizeGauge = self._gauge.GetSize() 316 sizeDlg.y += 2*LAYOUT_MARGIN + sizeGauge.y 317 318 # create the estimated/remaining/total time zones if requested 319 self._elapsed = None 320 self._display_estimated = self._last_timeupdate = self._break = 0 321 self._ctdelay = 0 322 323 label = None 324 325 nTimeLabels = 0 326 327 if style & wx.PD_ELAPSED_TIME: 328 329 nTimeLabels += 1 330 self._elapsed = self.CreateLabel("Elapsed time : ", sizer) 331 332 if nTimeLabels > 0: 333 334 label = wx.StaticText(self, -1, "") 335 # set it to the current time 336 self._timeStart = wx.GetCurrentTime() 337 sizeDlg.y += nTimeLabels*(label.GetSize().y + LAYOUT_MARGIN) 338 label.Destroy() 339 340 sizeDlgModified = False 341 342 if wx.Platform == "__WXMSW__": 343 sizerFlags = wx.ALIGN_RIGHT|wx.ALL 344 else: 345 sizerFlags = wx.ALIGN_CENTER_HORIZONTAL|wx.BOTTOM|wx.TOP 346 347 if self._hasAbortButton: 348 buttonSizer = wx.BoxSizer(wx.HORIZONTAL) 349 350 self._btnAbort = wx.Button(self, -1, "Cancel") 351 self._btnAbort.Bind(wx.EVT_BUTTON, self.OnCancel) 352 353 # Windows dialogs usually have buttons in the lower right corner 354 buttonSizer.Add(self._btnAbort, 0, sizerFlags, LAYOUT_MARGIN) 355 356 if not sizeDlgModified: 357 sizeDlg.y += 2*LAYOUT_MARGIN + wx.Button.GetDefaultSize().y 358 359 if self._hasAbortButton: 360 sizer.Add(buttonSizer, 0, sizerFlags, LAYOUT_MARGIN ) 361 362 self.Bind(wx.EVT_CLOSE, self.OnClose) 363 self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) 364 365 self._windowStyle = style 366 367 self.SetSizerAndFit(sizer) 368 369 sizeDlg.y += 2*LAYOUT_MARGIN 370 371 # try to make the dialog not square but rectangular of reasonable width 372 sizeDlg.x = max(widthText, 4*sizeDlg.y/3) 373 sizeDlg.x *= 3 374 sizeDlg.x /= 2 375 self.SetClientSize(sizeDlg) 376 377 self.Centre(wx.CENTER_FRAME|wx.BOTH) 378 379 if style & wx.PD_APP_MODAL: 380 self._winDisabler = wx.WindowDisabler(self) 381 else: 382 if self._parentTop: 383 self._parentTop.Disable() 384 self._winDisabler = None 385 386 self.ShowDialog() 387 self.Enable() 388 389 # this one can be initialized even if the others are unknown for now 390 # NB: do it after calling Layout() to keep the labels correctly aligned 391 if self._elapsed: 392 self.SetTimeLabel(0, self._elapsed) 393 394 if not wx.EventLoop().GetActive(): 395 self.evtloop = wx.EventLoop() 396 wx.EventLoop.SetActive(self.evtloop) 397 398 self.Update()
399 400
401 - def CreateLabel(self, text, sizer):
402 """ Creates the wx.StaticText that holds the elapsed time label. """ 403 404 locsizer = wx.BoxSizer(wx.HORIZONTAL) 405 dummy = wx.StaticText(self, wx.ID_ANY, text) 406 label = wx.StaticText(self, wx.ID_ANY, "unknown") 407 408 if wx.Platform in ["__WXMSW__", "__WXMAC__"]: 409 # label and time centered in one row 410 locsizer.Add(dummy, 1, wx.ALIGN_LEFT) 411 locsizer.Add(label, 1, wx.ALIGN_LEFT|wx.LEFT, LAYOUT_MARGIN) 412 sizer.Add(locsizer, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.TOP, LAYOUT_MARGIN) 413 else: 414 # label and time to the right in one row 415 sizer.Add(locsizer, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.TOP, LAYOUT_MARGIN) 416 locsizer.Add(dummy) 417 locsizer.Add(label, 0, wx.LEFT, LAYOUT_MARGIN) 418 419 return label
420 421 422 # ---------------------------------------------------------------------------- 423 # wxProgressDialog operations 424 # ---------------------------------------------------------------------------- 425
426 - def UpdatePulse(self, newmsg=""):
427 """ Update the progress dialog with a (optionally) new message. """ 428 429 self._gauge.Update() 430 431 if newmsg and newmsg != self._msg.GetLabel(): 432 self._msg.SetLabel(newmsg) 433 wx.YieldIfNeeded() 434 435 if self._elapsed: 436 elapsed = wx.GetCurrentTime() - self._timeStart 437 if self._last_timeupdate < elapsed: 438 self._last_timeupdate = elapsed 439 440 self.SetTimeLabel(elapsed, self._elapsed) 441 442 if self._state == Finished: 443 444 if not self._windowStyle & wx.PD_AUTO_HIDE: 445 446 self.EnableClose() 447 448 if newmsg == "": 449 # also provide the finishing message if the application didn't 450 self._msg.SetLabel("Done.") 451 452 wx.YieldIfNeeded() 453 self.ShowModal() 454 return False 455 456 else: 457 # reenable other windows before hiding this one because otherwise 458 # Windows wouldn't give the focus back to the window which had 459 # been previously focused because it would still be disabled 460 self.ReenableOtherWindows() 461 self.Hide() 462 463 # we have to yield because not only we want to update the display but 464 # also to process the clicks on the cancel and skip buttons 465 wx.YieldIfNeeded() 466 467 return self._state != Canceled
468 469
470 - def GetFirstGradientColour(self):
471 """ Returns the gauge first gradient colour. """ 472 473 return self._gauge.GetFirstGradientColour()
474 475
476 - def SetFirstGradientColour(self, colour):
477 """ Sets the gauge first gradient colour. """ 478 479 self._gauge.SetFirstGradientColour(colour)
480 481
482 - def GetSecondGradientColour(self):
483 """ Returns the gauge second gradient colour. """ 484 485 return self._gauge.GetSecondGradientColour()
486 487
488 - def SetSecondGradientColour(self, colour):
489 """ Sets the gauge second gradient colour. """ 490 491 self._gauge.SetSecondGradientColour(colour)
492 493
494 - def GetGaugeBackground(self):
495 """ Returns the gauge background colour. """ 496 497 return self._gauge.GetGaugeBackground()
498 499
500 - def SetGaugeBackground(self, colour):
501 """ Sets the gauge background colour. """ 502 503 self._gauge.SetGaugeBackground(colour)
504 505
506 - def SetGaugeSteps(self, steps):
507 """ 508 Sets the number of steps the gauge performs before switching from 509 forward to backward (or vice-versa) movement. 510 """ 511 512 self._gauge.SetGaugeSteps(steps)
513 514
515 - def GetGaugeSteps(self):
516 """ 517 Returns the number of steps the gauge performs before switching from 518 forward to backward (or vice-versa) movement. 519 """ 520 521 return self._gauge.GetGaugeSteps()
522 523
524 - def GetGaugeProportion(self):
525 """ 526 Returns the relative proportion between the sliding bar and the 527 whole gauge. 528 """ 529 530 return self._gauge.GetGaugeProportion()
531 532
533 - def SetGaugeProportion(self, proportion):
534 """ 535 Sets the relative proportion between the sliding bar and the 536 whole gauge. 537 """ 538 539 self._gauge.SetGaugeProportion(proportion)
540 541
542 - def ShowDialog(self, show=True):
543 """ Show the dialog. """ 544 545 # reenable other windows before hiding this one because otherwise 546 # Windows wouldn't give the focus back to the window which had 547 # been previously focused because it would still be disabled 548 if not show: 549 self.ReenableOtherWindows() 550 551 return self.Show()
552 553 554 # ---------------------------------------------------------------------------- 555 # event handlers 556 # ---------------------------------------------------------------------------- 557
558 - def OnCancel(self, event):
559 """ Handles the wx.EVT_BUTTON event for the Cancel button. """ 560 561 if self._state == Finished: 562 563 # this means that the count down is already finished and we're being 564 # shown as a modal dialog - so just let the default handler do the job 565 event.Skip() 566 567 else: 568 569 # request to cancel was received, the next time Update() is called we 570 # will handle it 571 self._state = Canceled 572 573 # update the buttons state immediately so that the user knows that the 574 # request has been noticed 575 self.DisableAbort() 576 577 # save the time when the dialog was stopped 578 self._timeStop = wx.GetCurrentTime() 579 580 self.ReenableOtherWindows()
581 582
583 - def OnDestroy(self, event):
584 """ Handles the wx.EVT_WINDOW_DESTROY event for PyProgress. """ 585 586 self.ReenableOtherWindows() 587 event.Skip()
588 589
590 - def OnClose(self, event):
591 """ Handles the wx.EVT_CLOSE event for PyProgress. """ 592 593 if self._state == Uncancelable: 594 595 # can't close this dialog 596 event.Veto() 597 598 elif self._state == Finished: 599 600 # let the default handler close the window as we already terminated 601 self.Hide() 602 event.Skip() 603 604 else: 605 606 # next Update() will notice it 607 self._state = Canceled 608 self.DisableAbort() 609 610 self._timeStop = wx.GetCurrentTime()
611 612
613 - def ReenableOtherWindows(self):
614 """ Re-enables the other windows if using wx.WindowDisabler. """ 615 616 if self._windowStyle & wx.PD_APP_MODAL: 617 if hasattr(self, "_winDisabler"): 618 del self._winDisabler 619 620 else: 621 622 if self._parentTop: 623 self._parentTop.Enable()
624 625
626 - def SetTimeLabel(self, val, label=None):
627 """ Sets the elapsed time label. """ 628 629 if label: 630 631 hours = val/3600 632 minutes = (val%3600)/60 633 seconds = val%60 634 strs = ("%lu:%02lu:%02lu")%(hours, minutes, seconds) 635 636 if strs != label.GetLabel(): 637 label.SetLabel(strs)
638 639
640 - def EnableAbort(self, enable=True):
641 """ Enables or disables the Cancel button. """ 642 643 if self._hasAbortButton: 644 if self._btnAbort: 645 self._btnAbort.Enable(enable)
646 647
648 - def EnableClose(self, enable=True):
649 """ Enables or disables the Close button. """ 650 651 if self._hasAbortButton: 652 if self._btnAbort: 653 self._btnAbort.Enable(enable) 654 self._btnAbort.SetLabel("Close") 655 self._btnAbort.Bind(wx.EVT_BUTTON, self.OnClose)
656 657
658 - def DisableAbort(self):
659 """ Disables the Cancel button. """ 660 661 self.EnableAbort(False)
662