| 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 | rest = 0, |
| 64 | current = 0, |
| 65 | direction = 1, |
| 66 | sequence = None |
| 67 | ): |
| 68 | wx.PyPanel.__init__(self, parent, id, pos, size, style, name) |
| 69 | self.name = name |
| 70 | self.label = label |
| 71 | self.running = (1 != 1) |
| 72 | _seqTypes = (type([]), type(())) |
| 73 | |
| 74 | # set size, guessing if necessary |
| 75 | width, height = size |
| 76 | if width == -1: |
| 77 | if type(bitmap) in _seqTypes: |
| 78 | width = bitmap[0].GetWidth() |
| 79 | else: |
| 80 | if frameWidth: |
| 81 | width = frameWidth |
| 82 | if height == -1: |
| 83 | if type(bitmap) in _seqTypes: |
| 84 | height = bitmap[0].GetHeight() |
| 85 | else: |
| 86 | height = bitmap.GetHeight() |
| 87 | self.width, self.height = width, height |
| 88 | |
| 89 | # double check it |
| 90 | assert width != -1 and height != -1, "Unable to guess size" |
| 91 | |
| 92 | if label: |
| 93 | extentX, extentY = self.GetTextExtent(label) |
| 94 | self.labelX = (width - extentX)/2 |
| 95 | self.labelY = (height - extentY)/2 |
| 96 | self.frameDelay = frameDelay |
| 97 | self.rest = rest |
| 98 | self.current = current |
| 99 | self.direction = direction |
| 100 | self.autoReverse = reverse |
| 101 | self.overlay = overlay |
| 102 | if overlay is not None: |
| 103 | self.overlay = overlay |
| 104 | self.overlayX = (width - self.overlay.GetWidth()) / 2 |
| 105 | self.overlayY = (height - self.overlay.GetHeight()) / 2 |
| 106 | self.showOverlay = overlay is not None |
| 107 | self.showLabel = label is not None |
| 108 | |
| 109 | # do we have a sequence of images? |
| 110 | if type(bitmap) in _seqTypes: |
| 111 | self.submaps = bitmap |
| 112 | self.frames = len(self.submaps) |
| 113 | # or a composite image that needs to be split? |
| 114 | else: |
| 115 | self.frames = frames |
| 116 | self.submaps = [] |
| 117 | for chunk in range(frames): |
| 118 | rect = (chunk * frameWidth, 0, width, height) |
| 119 | self.submaps.append(bitmap.GetSubBitmap(rect)) |
| 120 | |
| 121 | # self.sequence can be changed, but it's not recommended doing it |
| 122 | # while the throbber is running. self.sequence[0] should always |
| 123 | # refer to whatever frame is to be shown when 'resting' and be sure |
| 124 | # that no item in self.sequence >= self.frames or < 0!!! |
| 125 | self.SetSequence(sequence) |
| 126 | |
| 127 | self.SetClientSize((width, height)) |
| 128 | |
| 129 | timerID = wx.NewId() |
| 130 | self.timer = wx.Timer(self, timerID) |
| 131 | |
| 132 | self.Bind(EVT_UPDATE_THROBBER, self.Update) |
| 133 | self.Bind(wx.EVT_PAINT, self.OnPaint) |
| 134 | self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer) |
| 135 | self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroyWindow) |
| 136 | |
| 137 | |
| 138 | def DoGetBestSize(self): |
| 139 | return (self.width, self.height) |
| 140 | |
| 141 | |
| 142 | def OnTimer(self, event): |
| 143 | wx.PostEvent(self, UpdateThrobberEvent()) |
| 144 | |
| 145 | |
| 146 | def OnDestroyWindow(self, event): |
| 147 | self.Stop() |
| 148 | event.Skip() |
| 149 | |
| 150 | |
| 151 | def Draw(self, dc): |
| 152 | dc.DrawBitmap(self.submaps[self.sequence[self.current]], 0, 0, True) |
| 153 | if self.overlay and self.showOverlay: |
| 154 | dc.DrawBitmap(self.overlay, self.overlayX, self.overlayY, True) |
| 155 | if self.label and self.showLabel: |
| 156 | dc.DrawText(self.label, self.labelX, self.labelY) |
| 157 | dc.SetTextForeground(wx.WHITE) |
| 158 | dc.DrawText(self.label, self.labelX-1, self.labelY-1) |
| 159 | |
| 160 | |
| 161 | def OnPaint(self, event): |
| 162 | self.Draw(wx.PaintDC(self)) |
| 163 | event.Skip() |
| 164 | |
| 165 | |
| 166 | def Update(self, event): |
| 167 | self.Next() |
| 168 | |
| 169 | |
| 170 | def Wrap(self): |
| 171 | if self.current >= len(self.sequence): |
| 172 | if self.autoReverse: |
| 173 | self.Reverse() |
| 174 | self.current = len(self.sequence) - 1 |
| 175 | else: |
| 176 | self.current = 0 |
| 177 | if self.current < 0: |
| 178 | if self.autoReverse: |
| 179 | self.Reverse() |
| 180 | self.current = 0 |
| 181 | else: |
| 182 | self.current = len(self.sequence) - 1 |
| 183 | self.Draw(wx.ClientDC(self)) |
| 184 | |
| 185 | |
| 186 | # --------- public methods --------- |
| 187 | def SetFont(self, font): |
| 188 | """Set the font for the label""" |
| 189 | wx.Panel.SetFont(self, font) |
| 190 | self.SetLabel(self.label) |
| 191 | self.Draw(wx.ClientDC(self)) |
| 192 | |
| 193 | |
| 194 | def Rest(self): |
| 195 | """Stop the animation and return to frame 0""" |
| 196 | self.Stop() |
| 197 | self.current = self.rest |
| 198 | self.Draw(wx.ClientDC(self)) |
| 199 | |
| 200 | |
| 201 | def Reverse(self): |
| 202 | """Change the direction of the animation""" |
| 203 | self.direction = -self.direction |
| 204 | |
| 205 | |
| 206 | def Running(self): |
| 207 | """Returns True if the animation is running""" |
| 208 | return self.running |
| 209 | |
| 210 | |
| 211 | def Start(self): |
| 212 | """Start the animation""" |
| 213 | if not self.running: |
| 214 | self.running = not self.running |
| 215 | self.timer.Start(int(self.frameDelay * 1000)) |
| 216 | |
| 217 | |
| 218 | def Stop(self): |
| 219 | """Stop the animation""" |
| 220 | if self.running: |
| 221 | self.timer.Stop() |
| 222 | self.running = not self.running |
| 223 | |
| 224 | |
| 225 | def SetCurrent(self, current): |
| 226 | """Set current image""" |
| 227 | running = self.Running() |
| 228 | if not running: |
| 229 | #FIXME: need to make sure value is within range!!! |
| 230 | self.current = current |
| 231 | self.Draw(wx.ClientDC(self)) |
| 232 | |
| 233 | |
| 234 | def SetRest(self, rest): |
| 235 | """Set rest image""" |
| 236 | self.rest = rest |
| 237 | |
| 238 | |
| 239 | def SetSequence(self, sequence = None): |
| 240 | """Order to display images""" |
| 241 | |
| 242 | # self.sequence can be changed, but it's not recommended doing it |
| 243 | # while the throbber is running. self.sequence[0] should always |
| 244 | # refer to whatever frame is to be shown when 'resting' and be sure |
| 245 | # that no item in self.sequence >= self.frames or < 0!!! |
| 246 | |
| 247 | running = self.Running() |
| 248 | self.Stop() |
| 249 | |
| 250 | if sequence is not None: |
| 251 | #FIXME: need to make sure values are within range!!! |
| 252 | self.sequence = sequence |
| 253 | else: |
| 254 | self.sequence = range(self.frames) |
| 255 | |
| 256 | if running: |
| 257 | self.Start() |
| 258 | |
| 259 | |
| 260 | def Increment(self): |
| 261 | """Display next image in sequence""" |
| 262 | self.current += 1 |
| 263 | self.Wrap() |
| 264 | |
| 265 | |
| 266 | def Decrement(self): |
| 267 | """Display previous image in sequence""" |
| 268 | self.current -= 1 |
| 269 | self.Wrap() |
| 270 | |
| 271 | |
| 272 | def Next(self): |
| 273 | """Display next image in sequence according to direction""" |
| 274 | self.current += self.direction |
| 275 | self.Wrap() |
| 276 | |
| 277 | |
| 278 | def Previous(self): |
| 279 | """Display previous image in sequence according to direction""" |
| 280 | self.current -= self.direction |
| 281 | self.Wrap() |
| 282 | |
| 283 | |
| 284 | def SetFrameDelay(self, frameDelay = 0.05): |
| 285 | """Delay between each frame""" |
| 286 | self.frameDelay = frameDelay |
| 287 | if self.running: |
| 288 | self.Stop() |
| 289 | self.Start() |
| 290 | |
| 291 | |
| 292 | def ToggleOverlay(self, state = None): |
| 293 | """Toggle the overlay image""" |
| 294 | if state is None: |
| 295 | self.showOverlay = not self.showOverlay |
| 296 | else: |
| 297 | self.showOverlay = state |
| 298 | self.Draw(wx.ClientDC(self)) |
| 299 | |
| 300 | |
| 301 | def ToggleLabel(self, state = None): |
| 302 | """Toggle the label""" |
| 303 | if state is None: |
| 304 | self.showLabel = not self.showLabel |
| 305 | else: |
| 306 | self.showLabel = state |
| 307 | self.Draw(wx.ClientDC(self)) |
| 308 | |
| 309 | |
| 310 | def SetLabel(self, label): |
| 311 | """Change the text of the label""" |
| 312 | self.label = label |
| 313 | if label: |
| 314 | extentX, extentY = self.GetTextExtent(label) |
| 315 | self.labelX = (self.width - extentX)/2 |
| 316 | self.labelY = (self.height - extentY)/2 |
| 317 | self.Draw(wx.ClientDC(self)) |
| 318 | |
| 319 | |
| 320 | |
| 321 | # ------------------------------------------------------------------------------ |
| 322 | |