1import os 2import string 3import Tkinter 4import Pmw 5 6class 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)) 366