1# Based on iwidgets2.2.0/scrolledlistbox.itk code. 2 3import types 4import tkinter 5import Pmw 6import collections 7 8class ScrolledListBox(Pmw.MegaWidget): 9 _classBindingsDefinedFor = 0 10 11 def __init__(self, parent = None, **kw): 12 13 # Define the megawidget options. 14 INITOPT = Pmw.INITOPT 15 optiondefs = ( 16 ('dblclickcommand', None, None), 17 ('hscrollmode', 'dynamic', self._hscrollMode), 18 ('items', (), INITOPT), 19 ('labelmargin', 0, INITOPT), 20 ('labelpos', None, INITOPT), 21 ('scrollmargin', 2, INITOPT), 22 ('selectioncommand', None, None), 23 ('usehullsize', 0, INITOPT), 24 ('vscrollmode', 'dynamic', self._vscrollMode), 25 ) 26 self.defineoptions(kw, optiondefs) 27 28 # Initialise the base class (after defining the options). 29 Pmw.MegaWidget.__init__(self, parent) 30 31 # Create the components. 32 interior = self.interior() 33 34 if self['usehullsize']: 35 interior.grid_propagate(0) 36 37 # Create the listbox widget. 38 self._listbox = self.createcomponent('listbox', 39 (), None, 40 tkinter.Listbox, (interior,)) 41 self._listbox.grid(row = 2, column = 2, sticky = 'news') 42 interior.grid_rowconfigure(2, weight = 1, minsize = 0) 43 interior.grid_columnconfigure(2, weight = 1, minsize = 0) 44 45 # Create the horizontal scrollbar 46 self._horizScrollbar = self.createcomponent('horizscrollbar', 47 (), 'Scrollbar', 48 tkinter.Scrollbar, (interior,), 49 orient='horizontal', 50 command=self._listbox.xview 51 ) 52 53 # Create the vertical scrollbar 54 self._vertScrollbar = self.createcomponent('vertscrollbar', 55 (), 'Scrollbar', 56 tkinter.Scrollbar, (interior,), 57 orient='vertical', 58 command=self._listbox.yview 59 ) 60 61 self.createlabel(interior, childCols = 3, childRows = 3) 62 63 # Add the items specified by the initialisation option. 64 items = self['items'] 65 if type(items) != tuple: 66 items = tuple(items) 67 if len(items) > 0: 68 self._listbox.insert(*('end',) + items) 69 70 _registerScrolledList(self._listbox, self) 71 72 # Establish the special class bindings if not already done. 73 # Also create bindings if the Tkinter default interpreter has 74 # changed. Use Tkinter._default_root to create class 75 # bindings, so that a reference to root is created by 76 # bind_class rather than a reference to self, which would 77 # prevent object cleanup. 78 theTag = 'ScrolledListBoxTag' 79 if ScrolledListBox._classBindingsDefinedFor != tkinter._default_root: 80 root = tkinter._default_root 81 82 def doubleEvent(event): 83 _handleEvent(event, 'double') 84 def keyEvent(event): 85 _handleEvent(event, 'key') 86 def releaseEvent(event): 87 _handleEvent(event, 'release') 88 89 # Bind space and return keys and button 1 to the selectioncommand. 90 root.bind_class(theTag, '<Key-space>', keyEvent) 91 root.bind_class(theTag, '<Key-Return>', keyEvent) 92 root.bind_class(theTag, '<ButtonRelease-1>', releaseEvent) 93 94 # Bind double button 1 click to the dblclickcommand. 95 root.bind_class(theTag, '<Double-ButtonRelease-1>', doubleEvent) 96 97 ScrolledListBox._classBindingsDefinedFor = root 98 99 bindtags = self._listbox.bindtags() 100 self._listbox.bindtags(bindtags + (theTag,)) 101 102 # Initialise instance variables. 103 self._horizScrollbarOn = 0 104 self._vertScrollbarOn = 0 105 self.scrollTimer = None 106 self._scrollRecurse = 0 107 self._horizScrollbarNeeded = 0 108 self._vertScrollbarNeeded = 0 109 110 # Check keywords and initialise options. 111 self.initialiseoptions() 112 113 def destroy(self): 114 if self.scrollTimer is not None: 115 self.after_cancel(self.scrollTimer) 116 self.scrollTimer = None 117 _deregisterScrolledList(self._listbox) 118 Pmw.MegaWidget.destroy(self) 119 120 # ====================================================================== 121 122 # Public methods. 123 124 def clear(self): 125 self.setlist(()) 126 127 def getcurselection(self): 128 rtn = [] 129 for sel in self.curselection(): 130 rtn.append(self._listbox.get(sel)) 131 return tuple(rtn) 132 133 def getvalue(self): 134 return self.getcurselection() 135 136 def setvalue(self, textOrList): 137 self._listbox.selection_clear(0, 'end') 138 listitems = list(self._listbox.get(0, 'end')) 139 if type(textOrList) is str: 140 if textOrList in listitems: 141 self._listbox.selection_set(listitems.index(textOrList)) 142 else: 143 raise ValueError('no such item "%s"' % textOrList) 144 else: 145 for item in textOrList: 146 if item in listitems: 147 self._listbox.selection_set(listitems.index(item)) 148 else: 149 raise ValueError('no such item "%s"' % item) 150 151 def setlist(self, items): 152 self._listbox.delete(0, 'end') 153 if len(items) > 0: 154 if type(items) != tuple: 155 items = tuple(items) 156 self._listbox.insert(*(0,) + items) 157 158 # Override Tkinter.Listbox get method, so that if it is called with 159 # no arguments, return all list elements (consistent with other widgets). 160 def get(self, first=None, last=None): 161 if first is None: 162 return self._listbox.get(0, 'end') 163 else: 164 return self._listbox.get(first, last) 165 166 # ====================================================================== 167 168 # Configuration methods. 169 170 def _hscrollMode(self): 171 # The horizontal scroll mode has been configured. 172 173 mode = self['hscrollmode'] 174 175 if mode == 'static': 176 if not self._horizScrollbarOn: 177 self._toggleHorizScrollbar() 178 elif mode == 'dynamic': 179 if self._horizScrollbarNeeded != self._horizScrollbarOn: 180 self._toggleHorizScrollbar() 181 elif mode == 'none': 182 if self._horizScrollbarOn: 183 self._toggleHorizScrollbar() 184 else: 185 message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode 186 raise ValueError(message) 187 188 self._configureScrollCommands() 189 190 def _vscrollMode(self): 191 # The vertical scroll mode has been configured. 192 193 mode = self['vscrollmode'] 194 195 if mode == 'static': 196 if not self._vertScrollbarOn: 197 self._toggleVertScrollbar() 198 elif mode == 'dynamic': 199 if self._vertScrollbarNeeded != self._vertScrollbarOn: 200 self._toggleVertScrollbar() 201 elif mode == 'none': 202 if self._vertScrollbarOn: 203 self._toggleVertScrollbar() 204 else: 205 message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode 206 raise ValueError(message) 207 208 self._configureScrollCommands() 209 210 # ====================================================================== 211 212 # Private methods. 213 214 def _configureScrollCommands(self): 215 # If both scrollmodes are not dynamic we can save a lot of 216 # time by not having to create an idle job to handle the 217 # scroll commands. 218 219 # Clean up previous scroll commands to prevent memory leak. 220 tclCommandName = str(self._listbox.cget('xscrollcommand')) 221 if tclCommandName != '': 222 self._listbox.deletecommand(tclCommandName) 223 tclCommandName = str(self._listbox.cget('yscrollcommand')) 224 if tclCommandName != '': 225 self._listbox.deletecommand(tclCommandName) 226 227 if self['hscrollmode'] == self['vscrollmode'] == 'dynamic': 228 self._listbox.configure( 229 xscrollcommand=self._scrollBothLater, 230 yscrollcommand=self._scrollBothLater 231 ) 232 else: 233 self._listbox.configure( 234 xscrollcommand=self._scrollXNow, 235 yscrollcommand=self._scrollYNow 236 ) 237 238 def _scrollXNow(self, first, last): 239 self._horizScrollbar.set(first, last) 240 self._horizScrollbarNeeded = ((first, last) != ('0', '1')) 241 242 if self['hscrollmode'] == 'dynamic': 243 if self._horizScrollbarNeeded != self._horizScrollbarOn: 244 self._toggleHorizScrollbar() 245 246 def _scrollYNow(self, first, last): 247 self._vertScrollbar.set(first, last) 248 self._vertScrollbarNeeded = ((first, last) != ('0', '1')) 249 250 if self['vscrollmode'] == 'dynamic': 251 if self._vertScrollbarNeeded != self._vertScrollbarOn: 252 self._toggleVertScrollbar() 253 254 def _scrollBothLater(self, first, last): 255 # Called by the listbox to set the horizontal or vertical 256 # scrollbar when it has scrolled or changed size or contents. 257 258 if self.scrollTimer is None: 259 self.scrollTimer = self.after_idle(self._scrollBothNow) 260 261 def _scrollBothNow(self): 262 # This performs the function of _scrollXNow and _scrollYNow. 263 # If one is changed, the other should be updated to match. 264 self.scrollTimer = None 265 266 # Call update_idletasks to make sure that the containing frame 267 # has been resized before we attempt to set the scrollbars. 268 # Otherwise the scrollbars may be mapped/unmapped continuously. 269 self._scrollRecurse = self._scrollRecurse + 1 270 self.update_idletasks() 271 self._scrollRecurse = self._scrollRecurse - 1 272 if self._scrollRecurse != 0: 273 return 274 275 xview = self._listbox.xview() 276 yview = self._listbox.yview() 277 self._horizScrollbar.set(xview[0], xview[1]) 278 self._vertScrollbar.set(yview[0], yview[1]) 279 280 self._horizScrollbarNeeded = (xview != (0.0, 1.0)) 281 self._vertScrollbarNeeded = (yview != (0.0, 1.0)) 282 283 # If both horizontal and vertical scrollmodes are dynamic and 284 # currently only one scrollbar is mapped and both should be 285 # toggled, then unmap the mapped scrollbar. This prevents a 286 # continuous mapping and unmapping of the scrollbars. 287 if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and 288 self._horizScrollbarNeeded != self._horizScrollbarOn and 289 self._vertScrollbarNeeded != self._vertScrollbarOn and 290 self._vertScrollbarOn != self._horizScrollbarOn): 291 if self._horizScrollbarOn: 292 self._toggleHorizScrollbar() 293 else: 294 self._toggleVertScrollbar() 295 return 296 297 if self['hscrollmode'] == 'dynamic': 298 if self._horizScrollbarNeeded != self._horizScrollbarOn: 299 self._toggleHorizScrollbar() 300 301 if self['vscrollmode'] == 'dynamic': 302 if self._vertScrollbarNeeded != self._vertScrollbarOn: 303 self._toggleVertScrollbar() 304 305 def _toggleHorizScrollbar(self): 306 307 self._horizScrollbarOn = not self._horizScrollbarOn 308 309 interior = self.interior() 310 if self._horizScrollbarOn: 311 self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') 312 interior.grid_rowconfigure(3, minsize = self['scrollmargin']) 313 else: 314 self._horizScrollbar.grid_forget() 315 interior.grid_rowconfigure(3, minsize = 0) 316 317 def _toggleVertScrollbar(self): 318 319 self._vertScrollbarOn = not self._vertScrollbarOn 320 321 interior = self.interior() 322 if self._vertScrollbarOn: 323 self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') 324 interior.grid_columnconfigure(3, minsize = self['scrollmargin']) 325 else: 326 self._vertScrollbar.grid_forget() 327 interior.grid_columnconfigure(3, minsize = 0) 328 329 def _handleEvent(self, event, eventType): 330 if eventType == 'double': 331 command = self['dblclickcommand'] 332 elif eventType == 'key': 333 command = self['selectioncommand'] 334 else: #eventType == 'release' 335 # Do not execute the command if the mouse was released 336 # outside the listbox. 337 if (event.x < 0 or self._listbox.winfo_width() <= event.x or 338 event.y < 0 or self._listbox.winfo_height() <= event.y): 339 return 340 341 command = self['selectioncommand'] 342 343 if isinstance(command, collections.Callable): 344 command() 345 346 # Need to explicitly forward this to override the stupid 347 # (grid_)size method inherited from Tkinter.Frame.Grid. 348 def size(self): 349 return self._listbox.size() 350 351 # Need to explicitly forward this to override the stupid 352 # (grid_)bbox method inherited from Tkinter.Frame.Grid. 353 def bbox(self, index): 354 return self._listbox.bbox(index) 355 356Pmw.forwardmethods(ScrolledListBox, tkinter.Listbox, '_listbox') 357 358# ====================================================================== 359 360_listboxCache = {} 361 362def _registerScrolledList(listbox, scrolledList): 363 # Register an ScrolledList widget for a Listbox widget 364 365 _listboxCache[listbox] = scrolledList 366 367def _deregisterScrolledList(listbox): 368 # Deregister a Listbox widget 369 del _listboxCache[listbox] 370 371def _handleEvent(event, eventType): 372 # Forward events for a Listbox to it's ScrolledListBox 373 374 # A binding earlier in the bindtags list may have destroyed the 375 # megawidget, so need to check. 376 if event.widget in _listboxCache: 377 _listboxCache[event.widget]._handleEvent(event, eventType) 378