| 1 | """ |
| 2 | A throbber displays an animated image that can be |
| 3 | started, stopped, reversed, etc. Useful for showing |
| 4 | an ongoing process (like most web browsers use) or |
| 5 | simply for adding eye-candy to an application. |
| 6 | |
| 7 | Throbbers utilize a wxTimer so that normal processing |
| 8 | can continue unencumbered. |
| 9 | """ |
| 10 | |
| 11 | # |
| 12 | # throbber.py - Cliff Wells <clifford.wells@comcast.net> |
| 13 | # |
| 14 | # Thanks to Harald Massa <harald.massa@suedvers.de> for |
| 15 | # suggestions and sample code. |
| 16 | # |
| 17 | # $Id$ |
| 18 | # |
| 19 | # 12/12/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
| 20 | # |
| 21 | # o 2.5 compatability update. |
| 22 | # |
| 23 | |
| 24 | |
| 25 | import os |
| 26 | import wx |
| 27 | |
| 28 | # ------------------------------------------------------------------------------ |
| 29 | |
| 30 | THROBBER_EVENT = wx.NewEventType() |
| 31 | EVT_UPDATE_THROBBER = wx.PyEventBinder(THROBBER_EVENT, 0) |
| 32 | |
| 33 | class UpdateThrobberEvent(wx.PyEvent): |
| 34 | def __init__(self): |
| 35 | wx.PyEvent.__init__(self) |
| 36 | self.SetEventType(THROBBER_EVENT) |
| 37 | |
| 38 | # ------------------------------------------------------------------------------ |
| 39 | |
| 40 | class Throbber(wx.PyPanel): |
| 41 | """ |
| 42 | The first argument is either the name of a file that will be split into frames |
| 43 | (a composite image) or a list of strings of image names that will be treated |
| 44 | as individual frames. If a single (composite) image is given, then additional |
| 45 | information must be provided: the number of frames in the image and the width |
| 46 | of each frame. The first frame is treated as the "at rest" frame (it is not |
| 47 | shown during animation, but only when Throbber.Rest() is called. |
| 48 | A second, single image may be optionally specified to overlay on top of the |
| 49 | animation. A label may also be specified to show on top of the animation. |
| 50 | """ |
| 51 | def __init__(self, parent, id, |
| 52 | bitmap, # single (composite) bitmap or list of bitmaps |
| 53 | pos = wx.DefaultPosition, |
| 54 | size = wx.DefaultSize, |
| 55 | frameDelay = 0.1,# time between frames |
| 56 | frames = 0, # number of frames (only necessary for composite image) |
| 57 | frameWidth = 0, # width of each frame (only necessary for composite image) |
| 58 | label = None, # optional text to be displayed |
| 59 | overlay = None, # optional image to overlay on animation |
| 60 | reverse = 0, # reverse direction at end of animation |
| 61 | style = 0, # window style |
| 62 | name = "throbber"): |
| 63 | wx.PyPanel.__init__(self, parent, id, pos, size, style, name) |
| 64 | self.name = name |
| 65 | self.label = label |
| 66 | self.running = (1 != 1) |
| 67 | _seqTypes = (type([]), type(())) |
| 68 | |
| 69 | # set size, guessing if necessary |
| 70 | width, height = size |
| 71 | if width == -1: |
| 72 | if type(bitmap) in _seqTypes: |
| 73 | width = bitmap[0].GetWidth() |
| 74 | else: |
| 75 | if frameWidth: |
| 76 | width = frameWidth |
| 77 | if height == -1: |
| 78 | if type(bitmap) in _seqTypes: |
| 79 | height = bitmap[0].GetHeight() |
| 80 | else: |
| 81 | height = bitmap.GetHeight() |
| 82 | self.width, self.height = width, height |
| 83 | |
| 84 | # double check it |
| 85 | assert width != -1 and height != -1, "Unable to guess size" |
| 86 | |
| 87 | if label: |
| 88 | extentX, extentY = self.GetTextExtent(label) |
| 89 | self.labelX = (width - extentX)/2 |
| 90 | self.labelY = (height - extentY)/2 |
| 91 | self.frameDelay = frameDelay |
| 92 | self.current = 0 |
| 93 | self.direction = 1 |
| 94 | self.autoReverse = reverse |
| 95 | self.overlay = overlay |
| 96 | if overlay is not None: |
| 97 | self.overlay = overlay |
| 98 | self.overlayX = (width - self.overlay.GetWidth()) / 2 |
| 99 | self.overlayY = (height - self.overlay.GetHeight()) / 2 |
| 100 | self.showOverlay = overlay is not None |
| 101 | self.showLabel = label is not None |
| 102 | |
| 103 | # do we have a sequence of images? |
| 104 | if type(bitmap) in _seqTypes: |
| 105 | self.submaps = bitmap |
| 106 | self.frames = len(self.submaps) |
| 107 | # or a composite image that needs to be split? |
| 108 | else: |
| 109 | self.frames = frames |
| 110 | self.submaps = [] |
| 111 | for chunk in range(frames): |
| 112 | rect = (chunk * frameWidth, 0, width, height) |
| 113 | self.submaps.append(bitmap.GetSubBitmap(rect)) |
| 114 | |
| 115 | # self.sequence can be changed, but it's not recommended doing it |
| 116 | # while the throbber is running. self.sequence[0] should always |
| 117 | # refer to whatever frame is to be shown when 'resting' and be sure |
| 118 | # that no item in self.sequence >= self.frames or < 0!!! |
| 119 | self.sequence = range(self.frames) |
| 120 | |
| 121 | self.SetClientSize((width, height)) |
| 122 | |
| 123 | timerID = wx.NewId() |
| 124 | self.timer = wx.Timer(self, timerID) |
| 125 | |
| 126 | self.Bind(EVT_UPDATE_THROBBER, self.Rotate) |
| 127 | self.Bind(wx.EVT_PAINT, self.OnPaint) |
| 128 | self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer) |
| 129 | self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroyWindow) |
| 130 | |
| 131 | |
| 132 | def DoGetBestSize(self): |
| 133 | return (self.width, self.height) |
| 134 | |
| 135 | |
| 136 | def OnTimer(self, event): |
| 137 | wx.PostEvent(self, UpdateThrobberEvent()) |
| 138 | |
| 139 | |
| 140 | def OnDestroyWindow(self, event): |
| 141 | self.Stop() |
| 142 | event.Skip() |
| 143 | |
| 144 | |
| 145 | def Draw(self, dc): |
| 146 | dc.DrawBitmap(self.submaps[self.sequence[self.current]], 0, 0, True) |
| 147 | if self.overlay and self.showOverlay: |
| 148 | dc.DrawBitmap(self.overlay, self.overlayX, self.overlayY, True) |
| 149 | if self.label and self.showLabel: |
| 150 | dc.DrawText(self.label, self.labelX, self.labelY) |
| 151 | dc.SetTextForeground(wx.WHITE) |
| 152 | dc.DrawText(self.label, self.labelX-1, self.labelY-1) |
| 153 | |
| 154 | |
| 155 | def OnPaint(self, event): |
| 156 | self.Draw(wx.PaintDC(self)) |
| 157 | event.Skip() |
| 158 | |
| 159 | |
| 160 | def Rotate(self, event): |
| 161 | self.current += self.direction |
| 162 | if self.current >= len(self.sequence): |
| 163 | if self.autoReverse: |
| 164 | self.Reverse() |
| 165 | self.current = len(self.sequence) - 1 |
| 166 | else: |
| 167 | self.current = 1 |
| 168 | if self.current < 1: |
| 169 | if self.autoReverse: |
| 170 | self.Reverse() |
| 171 | self.current = 1 |
| 172 | else: |
| 173 | self.current = len(self.sequence) - 1 |
| 174 | self.Draw(wx.ClientDC(self)) |
| 175 | |
| 176 | |
| 177 | # --------- public methods --------- |
| 178 | def SetFont(self, font): |
| 179 | """Set the font for the label""" |
| 180 | wx.Panel.SetFont(self, font) |
| 181 | self.SetLabel(self.label) |
| 182 | self.Draw(wx.ClientDC(self)) |
| 183 | |
| 184 | |
| 185 | def Rest(self): |
| 186 | """Stop the animation and return to frame 0""" |
| 187 | self.Stop() |
| 188 | self.current = 0 |
| 189 | self.Draw(wx.ClientDC(self)) |
| 190 | |
| 191 | |
| 192 | def Reverse(self): |
| 193 | """Change the direction of the animation""" |
| 194 | self.direction = -self.direction |
| 195 | |
| 196 | |
| 197 | def Running(self): |
| 198 | """Returns True if the animation is running""" |
| 199 | return self.running |
| 200 | |
| 201 | |
| 202 | def Start(self): |
| 203 | """Start the animation""" |
| 204 | if not self.running: |
| 205 | self.running = not self.running |
| 206 | self.timer.Start(int(self.frameDelay * 1000)) |
| 207 | |
| 208 | |
| 209 | def Stop(self): |
| 210 | """Stop the animation""" |
| 211 | if self.running: |
| 212 | self.timer.Stop() |
| 213 | self.running = not self.running |
| 214 | |
| 215 | |
| 216 | def SetFrameDelay(self, frameDelay = 0.05): |
| 217 | """Delay between each frame""" |
| 218 | self.frameDelay = frameDelay |
| 219 | if self.running: |
| 220 | self.Stop() |
| 221 | self.Start() |
| 222 | |
| 223 | |
| 224 | def ToggleOverlay(self, state = None): |
| 225 | """Toggle the overlay image""" |
| 226 | if state is None: |
| 227 | self.showOverlay = not self.showOverlay |
| 228 | else: |
| 229 | self.showOverlay = state |
| 230 | self.Draw(wx.ClientDC(self)) |
| 231 | |
| 232 | |
| 233 | def ToggleLabel(self, state = None): |
| 234 | """Toggle the label""" |
| 235 | if state is None: |
| 236 | self.showLabel = not self.showLabel |
| 237 | else: |
| 238 | self.showLabel = state |
| 239 | self.Draw(wx.ClientDC(self)) |
| 240 | |
| 241 | |
| 242 | def SetLabel(self, label): |
| 243 | """Change the text of the label""" |
| 244 | self.label = label |
| 245 | if label: |
| 246 | extentX, extentY = self.GetTextExtent(label) |
| 247 | self.labelX = (self.width - extentX)/2 |
| 248 | self.labelY = (self.height - extentY)/2 |
| 249 | self.Draw(wx.ClientDC(self)) |
| 250 | |
| 251 | |
| 252 | |
| 253 | # ------------------------------------------------------------------------------ |
| 254 | |