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