]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/throbber.py
3211ce699e7e0798e33ebc2521d8fa33be6efb0d
[wxWidgets.git] / wxPython / wxPython / lib / throbber.py
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 run in a separate thread so normal application
8 processing can continue unencumbered.
9 """
10
11 #
12 # throbber.py - Cliff Wells <clifford.wells@attbi.com>
13 #
14 # Thanks to Harald Massa <harald.massa@suedvers.de> for
15 # suggestions and sample code.
16 #
17 # $Id$
18 #
19
20 import threading, os
21 from wxPython.wx import *
22
23 # ------------------------------------------------------------------------------
24
25 wxEVT_UPDATE_THROBBER = wxNewEventType()
26 def EVT_UPDATE_THROBBER(win, func):
27 win.Connect(-1, -1, wxEVT_UPDATE_THROBBER, func)
28
29 class UpdateThrobberEvent(wxPyEvent):
30 def __init__(self):
31 wxPyEvent.__init__(self)
32 self.SetEventType(wxEVT_UPDATE_THROBBER)
33
34 # ------------------------------------------------------------------------------
35
36 class Throbber(wxPanel):
37 """
38 The first argument is either the name of a file that will be split into frames
39 (a composite image) or a list of strings of image names that will be treated
40 as individual frames. If a single (composite) image is given, then additional
41 information must be provided: the number of frames in the image and the width
42 of each frame. The first frame is treated as the "at rest" frame (it is not
43 shown during animation, but only when Throbber.Rest() is called.
44 A second, single image may be optionally specified to overlay on top of the
45 animation. A label may also be specified to show on top of the animation.
46 """
47 def __init__(self, parent, id,
48 bitmap, # single (composite) bitmap or list of bitmaps
49 pos = wxDefaultPosition,
50 size = wxDefaultSize,
51 frameDelay = 0.1,# time between frames
52 frames = 0, # number of frames (only necessary for composite image)
53 frameWidth = 0, # width of each frame (only necessary for composite image)
54 label = None, # optional text to be displayed
55 overlay = None, # optional image to overlay on animation
56 reverse = 0, # reverse direction at end of animation
57 style = 0, # window style
58 name = "throbber"):
59 wxPanel.__init__(self, parent, id, pos, size, style, name)
60 self.name = name
61 self.label = label
62 _seqTypes = (type([]), type(()))
63
64 # set size, guessing if necessary
65 width, height = size
66 if width == -1:
67 if type(bitmap) in _seqTypes:
68 width = bitmap[0].GetWidth()
69 else:
70 if frameWidth:
71 width = frameWidth
72 if height == -1:
73 if type(bitmap) in _seqTypes:
74 height = bitmap[0].GetHeight()
75 else:
76 height = bitmap.GetHeight()
77 self.width, self.height = width, height
78
79 # double check it
80 assert width != -1 and height != -1, "Unable to guess size"
81
82 if label:
83 extentX, extentY = self.GetTextExtent(label)
84 self.labelX = (width - extentX)/2
85 self.labelY = (height - extentY)/2
86 self.frameDelay = frameDelay
87 self.current = 0
88 self.direction = 1
89 self.autoReverse = reverse
90 self.overlay = overlay
91 if overlay is not None:
92 self.overlay = overlay
93 self.overlayX = (width - self.overlay.GetWidth()) / 2
94 self.overlayY = (height - self.overlay.GetHeight()) / 2
95 self.showOverlay = overlay is not None
96 self.showLabel = label is not None
97
98 # do we have a sequence of images?
99 if type(bitmap) in _seqTypes:
100 self.submaps = bitmap
101 self.frames = len(self.submaps)
102 # or a composite image that needs to be split?
103 else:
104 self.frames = frames
105 self.submaps = []
106 for chunk in range(frames):
107 rect = (chunk * frameWidth, 0, width, height)
108 self.submaps.append(bitmap.GetSubBitmap(rect))
109
110 # self.sequence can be changed, but it's not recommended doing it
111 # while the throbber is running. self.sequence[0] should always
112 # refer to whatever frame is to be shown when 'resting' and be sure
113 # that no item in self.sequence >= self.frames or < 0!!!
114 self.sequence = range(self.frames)
115
116 self.SetClientSize((width, height))
117
118 EVT_PAINT(self, self.OnPaint)
119 EVT_UPDATE_THROBBER(self, self.Rotate)
120 EVT_WINDOW_DESTROY(self, self.OnDestroyWindow)
121
122 self.event = threading.Event()
123 self.event.set() # we start out in the "resting" state
124
125
126 def OnDestroyWindow(self, event):
127 # this is currently broken due to a bug in wxWindows... hopefully
128 # it'll be fixed soon. Meanwhile be sure to explicitly call Stop()
129 # before the throbber is destroyed.
130 self.Stop()
131 event.Skip()
132
133
134 def Draw(self, dc):
135 dc.DrawBitmap(self.submaps[self.sequence[self.current]], 0, 0, True)
136 if self.overlay and self.showOverlay:
137 dc.DrawBitmap(self.overlay, self.overlayX, self.overlayY, True)
138 if self.label and self.showLabel:
139 dc.DrawText(self.label, self.labelX, self.labelY)
140 dc.SetTextForeground(wxWHITE)
141 dc.DrawText(self.label, self.labelX-1, self.labelY-1)
142
143
144 def OnPaint(self, event):
145 self.Draw(wxPaintDC(self))
146 event.Skip()
147
148
149 def UpdateThread(self):
150 try:
151 while hasattr(self, 'event') and not self.event.isSet():
152 wxPostEvent(self, UpdateThrobberEvent())
153 self.event.wait(self.frameDelay)
154 except wxPyDeadObjectError: # BUG: we were destroyed
155 return
156
157
158 def Rotate(self, event):
159 if self.event.isSet():
160 return
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(wxClientDC(self))
175
176
177 # --------- public methods ---------
178 def SetFont(self, font):
179 """Set the font for the label"""
180 wxPanel.SetFont(self, font)
181 self.SetLabel(self.label)
182 self.Draw(wxClientDC(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(wxClientDC(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 not self.event.isSet()
200
201
202 def Start(self):
203 """Start the animation"""
204 if not self.Running():
205 self.event.clear()
206 thread = threading.Thread(target = self.UpdateThread,
207 name = "%s-thread" % self.name)
208 thread.start()
209
210
211 def Stop(self):
212 """Stop the animation"""
213 if self.event.isSet():
214 return
215 self.event.set()
216
217
218 def SetFrameDelay(self, frameDelay = 0.05):
219 """Delay between each frame"""
220 self.frameDelay = frameDelay
221
222
223 def ToggleOverlay(self, state = None):
224 """Toggle the overlay image"""
225 if state is None:
226 self.showOverlay = not self.showOverlay
227 else:
228 self.showOverlay = state
229 self.Draw(wxClientDC(self))
230
231
232 def ToggleLabel(self, state = None):
233 """Toggle the label"""
234 if state is None:
235 self.showLabel = not self.showLabel
236 else:
237 self.showLabel = state
238 self.Draw(wxClientDC(self))
239
240
241 def SetLabel(self, label):
242 """Change the text of the label"""
243 self.label = label
244 if label:
245 extentX, extentY = self.GetTextExtent(label)
246 self.labelX = (self.width - extentX)/2
247 self.labelY = (self.height - extentY)/2
248 self.Draw(wxClientDC(self))
249
250
251
252 # ------------------------------------------------------------------------------
253