Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | import os |
2 | import string | |
3 | import Tkinter | |
4 | import Pmw | |
5 | ||
6 | class Balloon(Pmw.MegaToplevel): | |
7 | def __init__(self, parent = None, **kw): | |
8 | ||
9 | # Define the megawidget options. | |
10 | optiondefs = ( | |
11 | ('initwait', 500, None), # milliseconds | |
12 | ('label_background', 'lightyellow', None), | |
13 | ('label_foreground', 'black', None), | |
14 | ('label_justify', 'left', None), | |
15 | ('master', 'parent', None), | |
16 | ('relmouse', 'none', self._relmouse), | |
17 | ('state', 'both', self._state), | |
18 | ('statuscommand', None, None), | |
19 | ('xoffset', 20, None), # pixels | |
20 | ('yoffset', 1, None), # pixels | |
21 | ('hull_highlightthickness', 1, None), | |
22 | ('hull_highlightbackground', 'black', None), | |
23 | ) | |
24 | self.defineoptions(kw, optiondefs) | |
25 | ||
26 | # Initialise the base class (after defining the options). | |
27 | Pmw.MegaToplevel.__init__(self, parent) | |
28 | ||
29 | self.withdraw() | |
30 | self.overrideredirect(1) | |
31 | ||
32 | # Create the components. | |
33 | interior = self.interior() | |
34 | self._label = self.createcomponent('label', | |
35 | (), None, | |
36 | Tkinter.Label, (interior,)) | |
37 | self._label.pack() | |
38 | ||
39 | # The default hull configuration options give a black border | |
40 | # around the balloon, but avoids a black 'flash' when the | |
41 | # balloon is deiconified, before the text appears. | |
42 | if not kw.has_key('hull_background'): | |
43 | self.configure(hull_background = \ | |
44 | str(self._label.cget('background'))) | |
45 | ||
46 | # Initialise instance variables. | |
47 | self._timer = None | |
48 | ||
49 | # The widget or item that is currently triggering the balloon. | |
50 | # It is None if the balloon is not being displayed. It is a | |
51 | # one-tuple if the balloon is being displayed in response to a | |
52 | # widget binding (value is the widget). It is a two-tuple if | |
53 | # the balloon is being displayed in response to a canvas or | |
54 | # text item binding (value is the widget and the item). | |
55 | self._currentTrigger = None | |
56 | ||
57 | # Check keywords and initialise options. | |
58 | self.initialiseoptions() | |
59 | ||
60 | def destroy(self): | |
61 | if self._timer is not None: | |
62 | self.after_cancel(self._timer) | |
63 | self._timer = None | |
64 | Pmw.MegaToplevel.destroy(self) | |
65 | ||
66 | def bind(self, widget, balloonHelp, statusHelp = None): | |
67 | ||
68 | # If a previous bind for this widget exists, remove it. | |
69 | self.unbind(widget) | |
70 | ||
71 | if balloonHelp is None and statusHelp is None: | |
72 | return | |
73 | ||
74 | if statusHelp is None: | |
75 | statusHelp = balloonHelp | |
76 | enterId = widget.bind('<Enter>', | |
77 | lambda event, self = self, w = widget, | |
78 | sHelp = statusHelp, bHelp = balloonHelp: | |
79 | self._enter(event, w, sHelp, bHelp, 0)) | |
80 | ||
81 | # Set Motion binding so that if the pointer remains at rest | |
82 | # within the widget until the status line removes the help and | |
83 | # then the pointer moves again, then redisplay the help in the | |
84 | # status line. | |
85 | # Note: The Motion binding only works for basic widgets, and | |
86 | # the hull of megawidgets but not for other megawidget components. | |
87 | motionId = widget.bind('<Motion>', | |
88 | lambda event = None, self = self, statusHelp = statusHelp: | |
89 | self.showstatus(statusHelp)) | |
90 | ||
91 | leaveId = widget.bind('<Leave>', self._leave) | |
92 | buttonId = widget.bind('<ButtonPress>', self._buttonpress) | |
93 | ||
94 | # Set Destroy binding so that the balloon can be withdrawn and | |
95 | # the timer can be cancelled if the widget is destroyed. | |
96 | destroyId = widget.bind('<Destroy>', self._destroy) | |
97 | ||
98 | # Use the None item in the widget's private Pmw dictionary to | |
99 | # store the widget's bind callbacks, for later clean up. | |
100 | if not hasattr(widget, '_Pmw_BalloonBindIds'): | |
101 | widget._Pmw_BalloonBindIds = {} | |
102 | widget._Pmw_BalloonBindIds[None] = \ | |
103 | (enterId, motionId, leaveId, buttonId, destroyId) | |
104 | ||
105 | def unbind(self, widget): | |
106 | if hasattr(widget, '_Pmw_BalloonBindIds'): | |
107 | if widget._Pmw_BalloonBindIds.has_key(None): | |
108 | (enterId, motionId, leaveId, buttonId, destroyId) = \ | |
109 | widget._Pmw_BalloonBindIds[None] | |
110 | # Need to pass in old bindings, so that Tkinter can | |
111 | # delete the commands. Otherwise, memory is leaked. | |
112 | widget.unbind('<Enter>', enterId) | |
113 | widget.unbind('<Motion>', motionId) | |
114 | widget.unbind('<Leave>', leaveId) | |
115 | widget.unbind('<ButtonPress>', buttonId) | |
116 | widget.unbind('<Destroy>', destroyId) | |
117 | del widget._Pmw_BalloonBindIds[None] | |
118 | ||
119 | if self._currentTrigger is not None and len(self._currentTrigger) == 1: | |
120 | # The balloon is currently being displayed and the current | |
121 | # trigger is a widget. | |
122 | triggerWidget = self._currentTrigger[0] | |
123 | if triggerWidget == widget: | |
124 | if self._timer is not None: | |
125 | self.after_cancel(self._timer) | |
126 | self._timer = None | |
127 | self.withdraw() | |
128 | self.clearstatus() | |
129 | self._currentTrigger = None | |
130 | ||
131 | def tagbind(self, widget, tagOrItem, balloonHelp, statusHelp = None): | |
132 | ||
133 | # If a previous bind for this widget's tagOrItem exists, remove it. | |
134 | self.tagunbind(widget, tagOrItem) | |
135 | ||
136 | if balloonHelp is None and statusHelp is None: | |
137 | return | |
138 | ||
139 | if statusHelp is None: | |
140 | statusHelp = balloonHelp | |
141 | enterId = widget.tag_bind(tagOrItem, '<Enter>', | |
142 | lambda event, self = self, w = widget, | |
143 | sHelp = statusHelp, bHelp = balloonHelp: | |
144 | self._enter(event, w, sHelp, bHelp, 1)) | |
145 | motionId = widget.tag_bind(tagOrItem, '<Motion>', | |
146 | lambda event = None, self = self, statusHelp = statusHelp: | |
147 | self.showstatus(statusHelp)) | |
148 | leaveId = widget.tag_bind(tagOrItem, '<Leave>', self._leave) | |
149 | buttonId = widget.tag_bind(tagOrItem, '<ButtonPress>', self._buttonpress) | |
150 | ||
151 | # Use the tagOrItem item in the widget's private Pmw dictionary to | |
152 | # store the tagOrItem's bind callbacks, for later clean up. | |
153 | if not hasattr(widget, '_Pmw_BalloonBindIds'): | |
154 | widget._Pmw_BalloonBindIds = {} | |
155 | widget._Pmw_BalloonBindIds[tagOrItem] = \ | |
156 | (enterId, motionId, leaveId, buttonId) | |
157 | ||
158 | def tagunbind(self, widget, tagOrItem): | |
159 | if hasattr(widget, '_Pmw_BalloonBindIds'): | |
160 | if widget._Pmw_BalloonBindIds.has_key(tagOrItem): | |
161 | (enterId, motionId, leaveId, buttonId) = \ | |
162 | widget._Pmw_BalloonBindIds[tagOrItem] | |
163 | widget.tag_unbind(tagOrItem, '<Enter>', enterId) | |
164 | widget.tag_unbind(tagOrItem, '<Motion>', motionId) | |
165 | widget.tag_unbind(tagOrItem, '<Leave>', leaveId) | |
166 | widget.tag_unbind(tagOrItem, '<ButtonPress>', buttonId) | |
167 | del widget._Pmw_BalloonBindIds[tagOrItem] | |
168 | ||
169 | if self._currentTrigger is None: | |
170 | # The balloon is not currently being displayed. | |
171 | return | |
172 | ||
173 | if len(self._currentTrigger) == 1: | |
174 | # The current trigger is a widget. | |
175 | return | |
176 | ||
177 | if len(self._currentTrigger) == 2: | |
178 | # The current trigger is a canvas item. | |
179 | (triggerWidget, triggerItem) = self._currentTrigger | |
180 | if triggerWidget == widget and triggerItem == tagOrItem: | |
181 | if self._timer is not None: | |
182 | self.after_cancel(self._timer) | |
183 | self._timer = None | |
184 | self.withdraw() | |
185 | self.clearstatus() | |
186 | self._currentTrigger = None | |
187 | else: # The current trigger is a text item. | |
188 | (triggerWidget, x, y) = self._currentTrigger | |
189 | if triggerWidget == widget: | |
190 | currentPos = widget.index('@%d,%d' % (x, y)) | |
191 | currentTags = widget.tag_names(currentPos) | |
192 | if tagOrItem in currentTags: | |
193 | if self._timer is not None: | |
194 | self.after_cancel(self._timer) | |
195 | self._timer = None | |
196 | self.withdraw() | |
197 | self.clearstatus() | |
198 | self._currentTrigger = None | |
199 | ||
200 | def showstatus(self, statusHelp): | |
201 | if self['state'] in ('status', 'both'): | |
202 | cmd = self['statuscommand'] | |
203 | if callable(cmd): | |
204 | cmd(statusHelp) | |
205 | ||
206 | def clearstatus(self): | |
207 | self.showstatus(None) | |
208 | ||
209 | def _state(self): | |
210 | if self['state'] not in ('both', 'balloon', 'status', 'none'): | |
211 | raise ValueError, 'bad state option ' + repr(self['state']) + \ | |
212 | ': should be one of \'both\', \'balloon\', ' + \ | |
213 | '\'status\' or \'none\'' | |
214 | ||
215 | def _relmouse(self): | |
216 | if self['relmouse'] not in ('both', 'x', 'y', 'none'): | |
217 | raise ValueError, 'bad relmouse option ' + repr(self['relmouse'])+ \ | |
218 | ': should be one of \'both\', \'x\', ' + '\'y\' or \'none\'' | |
219 | ||
220 | def _enter(self, event, widget, statusHelp, balloonHelp, isItem): | |
221 | ||
222 | # Do not display balloon if mouse button is pressed. This | |
223 | # will only occur if the button was pressed inside a widget, | |
224 | # then the mouse moved out of and then back into the widget, | |
225 | # with the button still held down. The number 0x1f00 is the | |
226 | # button mask for the 5 possible buttons in X. | |
227 | buttonPressed = (event.state & 0x1f00) != 0 | |
228 | ||
229 | if not buttonPressed and balloonHelp is not None and \ | |
230 | self['state'] in ('balloon', 'both'): | |
231 | if self._timer is not None: | |
232 | self.after_cancel(self._timer) | |
233 | self._timer = None | |
234 | ||
235 | self._timer = self.after(self['initwait'], | |
236 | lambda self = self, widget = widget, help = balloonHelp, | |
237 | isItem = isItem: | |
238 | self._showBalloon(widget, help, isItem)) | |
239 | ||
240 | if isItem: | |
241 | if hasattr(widget, 'canvasx'): | |
242 | # The widget is a canvas. | |
243 | item = widget.find_withtag('current') | |
244 | if len(item) > 0: | |
245 | item = item[0] | |
246 | else: | |
247 | item = None | |
248 | self._currentTrigger = (widget, item) | |
249 | else: | |
250 | # The widget is a text widget. | |
251 | self._currentTrigger = (widget, event.x, event.y) | |
252 | else: | |
253 | self._currentTrigger = (widget,) | |
254 | ||
255 | self.showstatus(statusHelp) | |
256 | ||
257 | def _leave(self, event): | |
258 | if self._timer is not None: | |
259 | self.after_cancel(self._timer) | |
260 | self._timer = None | |
261 | self.withdraw() | |
262 | self.clearstatus() | |
263 | self._currentTrigger = None | |
264 | ||
265 | def _destroy(self, event): | |
266 | ||
267 | # Only withdraw the balloon and cancel the timer if the widget | |
268 | # being destroyed is the widget that triggered the balloon. | |
269 | # Note that in a Tkinter Destroy event, the widget field is a | |
270 | # string and not a widget as usual. | |
271 | ||
272 | if self._currentTrigger is None: | |
273 | # The balloon is not currently being displayed | |
274 | return | |
275 | ||
276 | if len(self._currentTrigger) == 1: | |
277 | # The current trigger is a widget (not an item) | |
278 | triggerWidget = self._currentTrigger[0] | |
279 | if str(triggerWidget) == event.widget: | |
280 | if self._timer is not None: | |
281 | self.after_cancel(self._timer) | |
282 | self._timer = None | |
283 | self.withdraw() | |
284 | self.clearstatus() | |
285 | self._currentTrigger = None | |
286 | ||
287 | def _buttonpress(self, event): | |
288 | if self._timer is not None: | |
289 | self.after_cancel(self._timer) | |
290 | self._timer = None | |
291 | self.withdraw() | |
292 | self._currentTrigger = None | |
293 | ||
294 | def _showBalloon(self, widget, balloonHelp, isItem): | |
295 | ||
296 | self._label.configure(text = balloonHelp) | |
297 | ||
298 | # First, display the balloon offscreen to get dimensions. | |
299 | screenWidth = self.winfo_screenwidth() | |
300 | screenHeight = self.winfo_screenheight() | |
301 | self.geometry('+%d+0' % (screenWidth + 1)) | |
302 | self.update_idletasks() | |
303 | ||
304 | if isItem: | |
305 | # Get the bounding box of the current item. | |
306 | bbox = widget.bbox('current') | |
307 | if bbox is None: | |
308 | # The item that triggered the balloon has disappeared, | |
309 | # perhaps by a user's timer event that occured between | |
310 | # the <Enter> event and the 'initwait' timer calling | |
311 | # this method. | |
312 | return | |
313 | ||
314 | # The widget is either a text or canvas. The meaning of | |
315 | # the values returned by the bbox method is different for | |
316 | # each, so use the existence of the 'canvasx' method to | |
317 | # distinguish between them. | |
318 | if hasattr(widget, 'canvasx'): | |
319 | # The widget is a canvas. Place balloon under canvas | |
320 | # item. The positions returned by bbox are relative | |
321 | # to the entire canvas, not just the visible part, so | |
322 | # need to convert to window coordinates. | |
323 | leftrel = bbox[0] - widget.canvasx(0) | |
324 | toprel = bbox[1] - widget.canvasy(0) | |
325 | bottomrel = bbox[3] - widget.canvasy(0) | |
326 | else: | |
327 | # The widget is a text widget. Place balloon under | |
328 | # the character closest to the mouse. The positions | |
329 | # returned by bbox are relative to the text widget | |
330 | # window (ie the visible part of the text only). | |
331 | leftrel = bbox[0] | |
332 | toprel = bbox[1] | |
333 | bottomrel = bbox[1] + bbox[3] | |
334 | else: | |
335 | leftrel = 0 | |
336 | toprel = 0 | |
337 | bottomrel = widget.winfo_height() | |
338 | ||
339 | xpointer, ypointer = widget.winfo_pointerxy() # -1 if off screen | |
340 | ||
341 | if xpointer >= 0 and self['relmouse'] in ('both', 'x'): | |
342 | x = xpointer | |
343 | else: | |
344 | x = leftrel + widget.winfo_rootx() | |
345 | x = x + self['xoffset'] | |
346 | ||
347 | if ypointer >= 0 and self['relmouse'] in ('both', 'y'): | |
348 | y = ypointer | |
349 | else: | |
350 | y = bottomrel + widget.winfo_rooty() | |
351 | y = y + self['yoffset'] | |
352 | ||
353 | edges = (string.atoi(str(self.cget('hull_highlightthickness'))) + | |
354 | string.atoi(str(self.cget('hull_borderwidth')))) * 2 | |
355 | if x + self._label.winfo_reqwidth() + edges > screenWidth: | |
356 | x = screenWidth - self._label.winfo_reqwidth() - edges | |
357 | ||
358 | if y + self._label.winfo_reqheight() + edges > screenHeight: | |
359 | if ypointer >= 0 and self['relmouse'] in ('both', 'y'): | |
360 | y = ypointer | |
361 | else: | |
362 | y = toprel + widget.winfo_rooty() | |
363 | y = y - self._label.winfo_reqheight() - self['yoffset'] - edges | |
364 | ||
365 | Pmw.setgeometryanddeiconify(self, '+%d+%d' % (x, y)) |